Mentions

Conversations linking to GitHub, newest first. Times are UTC.

Mentions by source

X

6,201
Nov 10, 10:00Nov 11, 10:00

Bluesky

2,083
Nov 10, 12:00Nov 11, 12:00
Nov 10, 12:00Nov 11, 12:00
Nov 10, 12:00Nov 11, 12:00
Nov 10, 12:00Nov 11, 12:00
Nov 10, 12:00Nov 11, 12:00
Nov 10, 11:00Nov 11, 11:00
Nov 10, 13:00Nov 11, 13:00
Oct 14, 21:00Oct 15, 21:00

Delayed · data through Oct 15, 21:00 UTC

DEV

849
Nov 10, 12:00Nov 11, 12:00
Nov 10, 12:00Nov 11, 12:00
Nov 10, 10:00Nov 11, 10:00
Nov 10, 12:00Nov 11, 12:00
Oct 28, 01:00Oct 29, 01:00

Delayed · data through Oct 29, 01:00 UTC

Nov 8, 11:00Nov 9, 11:00

Delayed · data through Nov 9, 11:00 UTC

Nov 10, 12:00Nov 11, 12:00

Latest posts

  1. X@MirjiRahul · 1 like · 17 views

    Want your AI agent to work more like a senior engineer? Matt Pocock has open-sourced his engineering skills. TDD, debugging, code review and more. mattpocock/skills

  2. X@Oday_Hunter · 2 views

    Results and method: github.com/Docker-Hunterpedia/ReverseDock/tree/ma… The four fixes, step by step, on Oday's website: oday-bakkour.com

  3. X@himanshu19890 · 11 views

    @MSFTResearch 6/ Repo, with install lines for each agent: microsoft/TokenEconomizer Paper: tokeneconomizer.github.io #AIAgents #DevTools #LLMOps

  4. X@LovekeshAnand07 · 4 likes · 99 views

    Your AI agent: "Done. All tests pass." Also your AI agent: skipped 3 of them. I built excudra, a free open-source plugin for Claude Code that: → blocks force pushes, DB resets & hallucinated packages → refuses "done" when tests get weakened LovekeshAnand/excudra t.co

  5. X@laogeai · 4 views

    目录进十层还找不到谁占的,凭感觉删了几个大的,结果删错东西。 dua-cli 按大小排子目录、回车下钻、标记后删除或导出清单,百万文件也快。 byron/dua-cli

  6. X@jaywcjlove · 152 views

    ThermalBar - 适用于 Apple Silicon 的菜单栏温度监控工具 同时显示功耗、内存压力和过热降频,并可导出历史记 👉 evilork/thermalbar t.co

  7. X@YuukiOgino · 1 like · 89 views

    やっと時間が取れたので超久しぶりにVoicevoxEngine Forn UEプラグインを更新して、v2.1をリリースしました。 VOICEVOX CORE 0.16対応、UE5.7まで動作確認済みです。 お試しレベルですが、ユーザー辞書機能を追加したり、色々やってます。 github.com/YuukiOgino/VoicevoxEngineForUE/release…

  8. X@EspIngsc · 1 like · 44 views

    平铺窗口管理器,好用的往往长得朴素。 Hyprland 偏要两头都占:渐变边框、模糊、阴影、弹簧曲线动画都有。 Dwindle、Master、滚动等平铺布局可以按工作区分别设,配置一保存就生效,还自带插件管理器。 4 万星的 Wayland 合成器。 hyprwm/Hyprland

  9. X@lehua555 · 25 likes · 4 reposts · 635 views

    QGISプラグインを公開しました。 自治体で国土基本図図郭で整備された高精細なオルソを想定しています。大量のオルソ画像をCOGに変換し、GeoParquet索引と仮想解像度レベルを利用して、GDAL GTI経由で動的に描画するQGISプラグインです。 Yuji-Kobayashi-orbitalnet/QGIS-GeoParquet-Raster-Catalog

  10. Redditu/nmfisher · r/unrealengine · 3 points

    Add ARKit blendshapes to MetaHuman DNA A few months ago I made a small open source plugin to export MetaHumans with ARKit blendshapes from inside Unreal Engine. I've since realized that we can use the Open RigLogic library to do substantially the same thing outside UE via MetaHuman DNA files instead. Video walkthrough here: youtu.be Repo is here: HolotypePteLtd/metahuman_arkit_export (open source) You can use the pre-built release bundles if you don't want to compile yourself.

  11. DEV@pavel_kkkkazantsev · 4 reactions

    I Went Looking for the Dead Internet in My RAG. I Found a Bad Experiment. I removed duplicate sources from a RAG benchmark and watched held-out accuracy fall from 10 of 20 tasks to 5 of 20. The tempting conclusion was that source deduplication made retrieval worse. The audit found another change hiding in the comparison. Lexical top-k supplied roughly five documents; the collapse policy often supplied one or two. The run changed source diversity and the evidence budget together, so the accuracy drop could not identify a provenance effect. With a fixed-budget control, root-aware retrieval recovered the primary record and accuracy returned to 10 of 20. It did not improve further. That correction became the point of this investigation: retrieval correctness and answer correctness are different measurements, and an apparent result can disappear when the control is fair. > TL;DR > > - Removing duplicate sources appeared to halve accuracy, but the comparison also reduced the evidence budget. > - Under the corrected control, root-aware retrieval recovered primary records without improving answer accuracy. > - A 15-case real-world pilot validated the provenance pipeline, not retrieval effectiveness. V2 will test adversarial cases. Part 1 corrects a misleading synthetic result and validates a provenance-capture and evaluation pipeline on real sources. Part 2 will test whether provenance-aware retrieval changes answers when the evidence actually conflicts. !Many browser pages trace back to one illuminated source record. Editorial illustration, not evidence. The part of the "dead internet" idea worth testing The phrase "dead internet" is associated with a 2021 forum post, Dead Internet Theory: Most of the Internet Is Fake. Contemporary reporting traces the popular term to that thread. Its broad version bundles together bots, manufactured engagement, algorithms, and a claim that real human activity has become marginal. That claim is too broad for this experiment. A traffic statistic cannot establish who wrote a page, whether a claim is true, or whether people have stopped participating online. For RAG, a systems problem remains. Imperva's 2025 report estimated that automation represented 51% of the web traffic it observed in 2024. The category includes useful crawlers and malicious bots, so it cannot count a "fake internet." For retrieval, a smaller question is enough: how many independent source roots support the evidence in context? Repeated documents can reach an index through a documented syndication chain, a press release, a scrape, a summary, or a generated rewrite. The causes differ. The failure is the same when repetition reaches the model as apparent corroboration. The wider theory also tends to mix four questions: - Did a human or an automated client make the request? - Was the page written by a person, copied, or generated? - Where did the claim originate? - Does the claim have enough support to warrant an answer? They require different evidence. This experiment addresses the third question, provenance. It does not diagnose the web as a whole. What I mean by a source root A source root is an information lineage, not a hostname. One root can include a public primary record and several documented syndications. One publisher can contain several independent roots. I assign a root only when the relationship has evidence: a canonical link, a syndication notice, an explicit citation, or a documented editorial relationship. Near-identical phrasing is useful for discovery, but it does not prove copying. Treating a similarity score as provenance would create a confident-looking field from an unverified assumption. The system The implementation keeps evidence capture, retrieval, generation, and scoring separate. Otherwise one provenance_aware switch can quietly change the candidate pool, context length, answer rule, and source metadata at once. !Evidence capture, fixed-budget retrieval, generation, and evaluation are separate stages in the pipeline. Evidence capture saves the public and canonical URLs, timestamp, HTTP status, extracted text, hash, and documented relation. Retrieval selects documents under a defined budget. Generation receives document text, IDs, roots, and roles, but never an expected stance or fixture label. Evaluation checks the returned stance, citation IDs, and retrieval trace. candidates = lexical_retrieve(question, pool=50) context = root_aware_fill(candidates, k=5) response = generate(question, context, fixed_prompt, seed) audit(response, context, expected_stance) The pool=50 is important. Collapsing sources only after a top-five cutoff cannot recover a primary record that was ranked sixth. Candidate grouping has to happen before the context budget is spent. The result that fooled me I built a 60-task synthetic fixture with known source roots. The first comparison looked clean: lexical top-k got 10 of 20 held-out tasks right; a root-collapse policy got 5 of 20. The comparison was not clean. Root collapse sometimes returned one or two documents where lexical top-k returned five. Both variables changed, so the first run cannot attribute its regression to source grouping alone. | Held-out synthetic control, k = 5 | Correct | Mean documents | Primary-record recall | | --- | ---: | ---: | ---: | | Lexical top-k | 10 / 20 | 4.25 | 20% | | Post-top-k collapse | 5 / 20 | 1.15 | 20% | | Root-aware fill | 10 / 20 | 4.25 | 100% | | Conflict-preserving fill | 10 / 20 | 4.25 | 100% | !A three-column comparison: lexical top-k and root-aware fill preserve five document slots, while post-top-k collapse leaves a mean of 1.15 documents. The latter changes source diversity and evidence budget at once. The original collapse policy spent fewer evidence slots. Root-aware fill chooses roots from a larger pool, then restores the intended document budget. With the budget fixed, root-aware fill recovered the primary record and retained the deterministic accuracy of lexical top-k. It did not improve accuracy. The remaining slots still let the answer rule count repeated stances. The original drop measured a policy that also shrank the prompt. It did not isolate the effect of source grouping. Why keep the synthetic fixture? The fixture isolates a retrieval mechanism that is hard to control on public pages. It contains 60 tasks across duplicate dilution, true repetition, conflicting roots, and satire stripped of its context. Each category has ten development tasks and five held-out tasks. The publishers and documents are fictional because the fixture needs declared roots and known answers without claiming to measure the open web. The first experiment uses a deterministic stance aggregator, making the intervention inspectable. It also exposes a limitation: recovering a primary record does not automatically improve the final answer if the aggregator still counts every retained document equally. !Held-out false-dilution fixture trace: lexical top-five returns five mirror documents from one root and misses the primary specification; root-aware fill includes the primary while retaining five document slots. A concrete held-out fixture trace. It demonstrates a retrieval change, not a real-world effectiveness result. Fixtures cannot show whether the pipeline works on public pages, so I collected a separate real-data pilot. The 15-case real pilot The pilot contains 15 NASA-to-Phys.org syndication chains. Every accepted case has: - a public NASA record; - a Phys.org page whose snapshot explicitly says Provided by NASA; - timestamped HTML, extracted text, a canonical URL, HTTP status, and a SHA-256 content hash; - a graph edge supported by the documented syndication marker. Similar language never counted as proof of dependence. If the source relationship was undocumented, the case did not enter the slice. Gemma 3 4B and Qwen 2.5 7B independently labelled document roles. They disagreed enough that I did not use their votes as ground truth. Final roles came from the explicit record type and syndication marker; model annotations remain in the corpus as machine annotations. This is a machine-adjudicated pilot, not a human-verified dataset. I ran both models on the same 15 questions under four conditions: 1. primary-only; 2. primary plus the documented dependent copy; 3. root-aware; 4. conflict-preserving root-aware. The prompt, seed, decoding settings, evidence ordering, retrieval trace, and raw responses were saved for every run. Neither model received the expected answer or a stance label in its prompt. > What V1 actually found > > 15 source chains · 2 local models · 120 retained responses > > Gemma 3 4B and Qwen 2.5 7B each returned the correct stance for all 15 cases in every condition. Citation-ID validity changed when the dependent copy was present. The slice has one root and positive support in every case, so it validates provenance capture and evaluation plumbing. It does not test whether root-aware retrieval prevents a real answer error. Both models answered all 15 questions correctly in every condition. Every accepted chain has one source root and supports one positive claim, so root-aware retrieval reduces to primary-only. The slice has no false assertion or contradictory root to resolve. The citation trace moved: | Model | Primary-only valid citation IDs | With dependent copy | | --- | ---: | ---: | | Gemma 3 4B | 3 / 15 | 6 / 15 | | Qwen 2.5 7B | 12 / 15 | 10 / 15 | !Citation validity in the 15-case real pilot. Those are exploratory counts, not a model ranking. Citation-ID validity only says whether the returned ID was present in supplied context. It does not establish that the cited document supports the answer. The V1 confound, stated plainly There is a second limit on the V1 comparison. Adding the dependent page changes the context from one document to two, and Ollama's recorded prompt-token count changes in every model-case pair. The citation results are descriptive because the comparison cannot isolate source dependence from the extra context. V2 will block an evaluation unless each paired case has equal document and prompt-token budgets. The real corpus also has a ceiling effect. Primary-reference recall is 100% by construction in the one-root conditions, and root concentration is 1.0 because every selected context belongs to one lineage. These measurements confirm that the policy did what it was asked to do. They also explain why the pilot cannot support an accuracy claim. Metrics worth keeping separate Correctness is useful, but it can hide the failure that changed. A model may preserve a correct stance while losing the primary record, naming a citation ID that was never in context, or becoming impossible to audit after a page changes. I would report at least these outcomes for a larger corpus: - answer stance; - primary-reference recall; - root concentration; - citation validity; - contradiction coverage; - false-claim adoption. A single combined score would hide which part failed. The unit for a future comparison is the source case, not the generated response. Paired accuracy deltas, bootstrap intervals, and an exact sign test make sense only after enough independent held-out cases survive the admission gate. For production, I would retain this trace beside every retrieved chunk: document_id · canonical_url · publisher · publication_date retrieved_at · content_hash · source_root · source_role claim_relation · dependency_evidence Candidate selection, source weighting, and conflict handling should stay separate. A source root can come from a documented origin or explicit citation evidence. It cannot come from cosine similarity alone. What a reader can verify today The public release supports artifact verification, not a byte-for-byte end-to-end rerun. It contains public URLs, hashes, annotations, the run manifest, the paired-budget audit, and 120 raw model responses. It deliberately excludes third-party HTML and extracted page text. git clone https://github.com/icesurf666/rag-provenance-lab.git cd rag-provenance-lab python3 -m venv .venv && . .venv/bin/activate pip install -e '.[test]' && PYTHONPATH=src pytest -q The reproducibility guide explains that boundary. The paired-budget audit records all 30 unequal model-case pairs; the raw responses, run manifest, and recorded results are public. A real retrieval failure outside this pilot In May 2024, Google AI Overviews advised users to add glue to pizza sauce. Associated Press reported that the answer traced back to a satirical Reddit comment, and Google later published its response. The incident shows a retrieval failure: the words reached the system while the sentence that explained their status did not. It cannot estimate a dead web or the prevalence of AI-generated content. What comes next: V2 — when provenance changes the answer V1 is complete as a pipeline validation. V2 will test the effectiveness question it leaves open: can source provenance prevent a model from accepting a repeated false claim when a correct primary record is available? !V1 contains fifteen one-root NASA to Phys.org positive cases and validates capture, traces, and paired execution. V2 is planned to test corrections, stale copies, satire, conflicts, and independent corroboration under fixed budgets. V1 and V2 answer different questions. This diagram describes a planned V2 protocol, not results. The next corpus will include corrections alongside persistent stale copies, satire retold as fact, contradictory evidence, genuinely independent corroboration, and conflicts within one source lineage. The target is 40 to 60 quality-gated real cases with documented provenance, snapshots, hashes, independent machine annotations, and adjudication. Every case that passes the gate will stay in the evaluation set. V2 will include cases where both policies are correct, both are wrong, lexical retrieval wins, and root-aware retrieval wins. Selecting only later success stories would turn the corpus into a demonstration instead of an experiment. Gemma and Qwen will run pre-specified paired conditions with equal document and prompt-token budgets. Document-order permutations will be assigned before outcomes are inspected. The result may show an improvement, a regression, or no material effect. Until V2 exists, a claim that root-aware retrieval improves real-world answer accuracy would be premature. The RAG Provenance Lab repository contains the public V1 code, artifact-verification materials, model outputs, run manifests, and the protocol. It distinguishes artifact verification from end-to-end reproduction: third-party page snapshots and extracted text are not distributed in the public release.

  12. X@Logic_Accepted · 3 likes · 1 repost · 70 views

    终于成功的在 macos 15 上运行了 vr 模式的 vrc,绕过 SteamVR 使用 DXMT 和 VideoToolbox 编码,然后用 WiVRn 串流到 quest 2,太厉害了 使用的是 StonkGuy/steamac 需要 x86 linux 环境编译一下包含 vrclient 的 proton 和 OpenVR 运行时 性能很一般, 但没有风扇声,或许可以用来睡觉 t.co

  13. X@DistinctLee · 19 views

    10/ Want your own? FastMCP turns a Python function into an MCP server. Wrap a spreadsheet price lookup so your AI can answer "what do we charge for X?" Apache 2.0. Catch: you host it, and a paid API behind it still bills you. Free server, not free calls. jlowin/fastmcp

  14. I was tired of vague PRDs, so I built a generator that outputs the architecture too Every project I start the same way: a PRD from a chatbot, good intentions, and then — three days into building with an AI coding agent — a database that doesn't match the document, an API payload that changed shape twice, and no security rules anywhere. The problem wasn't the agent. It was my spec. Prose is a suggestion; a schema is a contract. So I built the tool I needed. Web Dev Mehedi takes one product idea and generates four files that agree with each other: 1. PRODUCT_REQUIREMENTS.md — scope, user journeys, edge cases, and GIVEN/WHEN/THEN acceptance criteria. 2. UI_UX_SPECIFICATION.md — design tokens, typography, view states, accessibility rules. 3. TECHNICAL_ARCHITECTURE.md — PostgreSQL DDL schemas with Supabase Row Level Security, plus typed REST & Server Actions API contracts with Zod validation. 4. AI_AGENT_IMPLEMENTATION.md — phased build instructions and prompt rules that pin Cursor and Claude Code to files 1–3: use these exact table names, these exact payloads, never bypass RLS, and flag conflicts instead of choosing silently. Three lessons from building it: - Multi-tenancy has to be in the schema on day one, not patched later. RLS policies are written in the same step as the tables themselves. - An agent follows rigid rules surprisingly well. Most "hallucinations" I saw were vagueness failures, not capability failures: the spec never actually decided the thing the agent got wrong. - Pricing in BDT with bKash opened a market that $20/month card subscriptions simply don't reach. Web Dev Mehedi starts at ৳99/month, with 50 free credits on signup and no card required. It's live here if you want to break it: webdevmehedi.com — and the free template behind the format is on GitHub: web-dev-mehedi/ai-prd-template What do you hand your coding agent before a build? Genuinely curious what other people's specs look like.

  15. X@k2sbhai · 34 likes · 2 reposts · 1,822 views

    your NVIDIA GPU might have hidden AI performance 😳 - RTX 3060: +87% - RTX 3090: +65% - RTX 4090: nearly 2x peak throughput no BIOS flash or driver patch required try zagitta/gpu-unlock t.co

  16. X@DistinctLee · 16 views

    7/ Firecrawl MCP is how you'd get five competitor pricing pages into one comparison table. MIT licence. Catch: the keyless free tier covers scrape, search and parse, rate-limited. Crawl and map need an API key, and they use credits. firecrawl/firecrawl-mcp-server

  17. X@tako0910 · 70 views

    3年弱ぶりにWCHのArduino Coreが更新された模様 github.com/openwch/arduino_core_ch32/releases t.co

  18. tt-a1i/archify Github 93K star 工作中肯定会遇到画流程图的时候,如果你还在用mermaid生成丑丑的流程图,可以试一下这个项目,生成出来的流程图真是简洁又好看。 t.co

  19. X@vikasprogrammer · 2 likes · 41 views

    @pootlepress @wordpressdotcom Built my own to use with claude sub/codex sub - vikasprogrammer/agentric

  20. X@DistinctLee · 7 views

    6/ With Supabase MCP you can describe a booking app and get the tables and the migration back. Apache 2.0. Catch: use a dev project with read_only=true. The free plan is 2 projects, 500 MB each, paused after a week idle. supabase-community/supabase-mcp

  21. Redditu/Accomplished_Walk243 · r/HomeServer · 5 comments

    ArtCraft Hub - Self-hosted WebAssembly artcraft creative suite with automated release sync I’ve been loving the open-source Storytold/ArtCraft creative tools (PhotoCraft, VectorCraft, CADCraft, SoundCraft, etc.), and wanted an easy, all-in-one container to host them locally with a polished dashboard. Huge shoutout to artcraft-in-a-box by sbuchweitz for the original inspiration! Full disclosure: this project is completely vibe-coded with AI pair-programming, but it turned out super clean and works like a charm. What it does: - All-in-One Dashboard: Clean web dashboard supporting 15+ creative tools with instant search (/), category filters, and pinned favorites. - Dual Themes: Ships with both an editorial cream/ink "ArtCraft" theme and a modern dark "Glass" theme. - Automated Sync: Runs a background cron that automatically checks GitHub for new releases of each app, downloads the WASM packages, and updates symlinks. - Multi-Version Switching: Lets you switch between installed release versions directly from the UI. - Pre-configured Caddy Server: Zero-headache reverse proxy setup with correct WASM MIME types, gzip compression, and Cross-Origin-Embedder-Policy headers for SharedArrayBuffer support. Quick Start with Docker: yamlservices: artcraft-hub: image: ghcr.io/georgeargyrakis/artcraft-hub:latest container_name: artcraft-hub restart: unless-stopped ports: - "8080:80" volumes: - artcraft-data:/data - artcraft-config:/config environment: - SYNC_CRON=0 0 * * * - TZ=UTC volumes: artcraft-data: artcraft-config: - GitHub Repository: GeorgeArgyrakis/artcraft-hub - Docker Image: ghcr.io/georgeargyrakis/artcraft-hub:latest Feedback, suggestions, and PRs are very welcome! Hope someone finds it useful for their home lab.

  22. Bluesky@rntz.net · 1 like

    $ pandoc -t latex <<EOF \itshape hello\\ world EOF yields \itshape hello\ worldworld pandoc.org pandoc#12051

  23. X@DistinctLee · 13 views

    5/ GitHub's own MCP server could sort your open issues into bugs, ideas and questions while you make coffee. MIT licence. Catch: it acts with your access, so start with --read-only and the narrowest token you can. github/github-mcp-server

  24. X@SatyanshChand · 2 likes · 1 repost · 42 views

    Browser implementation in C using Linked LIst. Functions implemented: visit, back, current_site, display check it out -> github.com/satyansh2004/C-Codes/blob/main/browser… t.co

  25. X@DistinctLee · 15 views

    3/ Playwright MCP lets your AI drive a real browser. Ask it to open your site, sign up with a test email and report what breaks. Apache 2.0. Catch: its docs say it isn't a security boundary, so keep it off real accounts. microsoft/playwright-mcp

  26. Redditu/Brave_Magician2474 · r/npm · 1 point

    Published a client-side cache powered by Rust → WebAssembly Hey everyone, I published **Blaze Cache**, a client-side caching library with a Rust/Wasm core and TypeScript API. Packages currently on npm: - `@blaze-cache/core` - `@blaze-cache/axios` - `@blaze-cache/fetch` - `@blaze-cache/react-query` - `@blaze-cache/swr` It supports: - synchronous get/set after init - SIEVE eviction - TTL + tag invalidation - optional IndexedDB persistence - typed events - adapters for Axios, fetch, TanStack Query, and SWR Example: ```ts import { createCache } from '@blaze-cache/core' const cache = await createCache({ capacity: 1000 }) cache.set('user:42', { name: 'Ada' }, { ttlMs: 30_000, tags: ['user'] }) cache.get('user:42') GitHub: blaze-cache/blaze-cache Docs: blaze-cache.github.io npm: npmjs.com Would love feedback from people who publish or maintain frontend packages.

  27. X@DistinctLee · 23 views

    2/ Context7 stops your AI writing last year's code. Add "use context7" to a prompt like "Build a Next.js login page that sends logged-out users to /login" and it pulls the current docs first. MIT licence. Catch: a free API key lifts the rate limits. upstash/context7

  28. EUDI wallets with OpenID4VP in action (2/2): on another device --- title: "EUDI wallets with OpenID4VP in action (2/2): on another device" description: "Authenticate a user on a computer with the EUDI-style wallet of their phone: a Spring Boot application, pac4j, a QR code, an mdoc PID and the Paradym demo wallet, step by step, with the protocol logs." tags: java, security, openid4vp, eidas canonical_url: pac4j.org series: "OpenID4VP / EUDI Wallet / eIDAS 2.0" --- The European Digital Identity wallet is coming, and applications accepting it will need to talk to it. This is the second of two posts showing how to do that with pac4j: in the first post the wallet ran on the same phone as the browser; here the browser is on a computer, the wallet on a phone, and a QR code bridges the two. 1) Why prepare for EUDI wallets? Regulation (EU) 2024/1183, better known as eIDAS 2.0, entered into force on May 20, 2024. EU member states must offer EUDI wallets by the end of 2026, holding person identification data (PID) and attestations such as driving licences or diplomas. Using a wallet remains voluntary. Article 5f also requires acceptance by public services requiring electronic identification, certain private services requiring strong authentication, and very large online platforms requiring authentication. Private-sector obligations have conditions and exemptions, including for microenterprises and small enterprises, with a deadline tied to implementing acts, generally described as late 2027. For applications in scope, wallet authentication is becoming an integration requirement worth preparing for. 2) Why it is hard A wallet login looks simple: the user opens their wallet, sees what your application asks for, consents, and the wallet sends a signed proof. Behind this, the stack is anything but simple. Your application, the verifier in the vocabulary of the specifications, has to deal with: - OpenID for Verifiable Presentations 1.0, the protocol itself: how the wallet is invoked, how it fetches the request, how it posts the response, with its Digital Credentials Query Language (DCQL) to say what you want - the High Assurance Interoperability Profile (HAIP), which pins the options the EUDI wallet uses, and the Architecture and Reference Framework (ARF) of the European Commission, with its PID rulebook - two credential formats: the SD-JWT VC of the IETF, built on selective disclosure JWTs, and the mobile documents (mdoc) of ISO/IEC 18013-5, encoded in CBOR and signed with COSE - the plumbing underneath: signed request objects (RFC 9101), JWS, JWE and JWK, X.509 certificate chains and revocation, decentralized identifiers, and the Digital Credentials API of the browsers for the newest flows. And several ways to identify the verifier, several ways to reach the wallet, several response modes, each with its own validation rules. Implementing this by hand, and keeping it right as the specifications move, is a project in itself. 3) pac4j to the rescue pac4j has one security model for every protocol: a Client starts the authentication, a callback receives the answer, a UserProfile ends up in the session. CAS, SAML, OpenID Connect, OAuth all fit in it, and a digital wallet does too. The new pac4j-openid4vp module, introduced in pac4j v6.6.0, adds the EudiWalletClient: you give it a DCQL query, a keystore and the issuers you trust, and it handles the signed request, the encrypted response, credential verification and the user profile. If you have already used pac4j, the integration will look familiar. The first post walked through a same-device flow with an SD-JWT VC PID. This one takes the wallet to another device: the user is on a computer, the page shows a QR code, the phone scans it, and an mdoc PID comes back, the format of ISO/IEC 18013-5, the one of the mobile driving licences. 4) A young protocol: test with demo wallets OpenID4VP 1.0 was finalized in 2025, and wallet implementations are still evolving. Demo wallets let you test the integration while preparing for national EUDI deployments; expect differences between wallets and versions. A very good one is the Paradym wallet by Animo, available for Android and iOS, and its playground which plays both the issuer and the verifier: 1. install the Paradym wallet on your phone 2. open playground.paradym.id on your computer and, on the Issuance page, choose PID mDoc, the card this post uses: the page shows a QR code, scan it with the Paradym wallet, accept the offer, and the card appears in the wallet 3. keep the Trust Information page of the playground at hand: it shows the certificate of the playground issuer, which your application must trust. The cards are Emma Charlotte van der Berg's, a fictitious Dutch citizen, and the mdoc PID carries, among other attributes, a personal_administrative_number: a stable identifier, which the profile below relies on. Issue the card from this playground and no other: a PID issued elsewhere, by the older playground.animo.id for instance, is signed by another certificate and the truststore of section 7 refuses it. If the wallet already holds PID cards from another source, delete them, or at least pick the right one when the wallet asks which card to present. 5) ngrok: a real URL for the wallet The wallet talks to your application over HTTPS, from the phone: it fetches the request object and posts its response. The phone is not on your computer, so localhost will not do. The simplest way during development is ngrok, which exposes your local port on a public HTTPS address: ngrok http 8080 It prints a public URL such as https://abcd-1234.ngrok-free.dev: this is the base URL of your application for the rest of this post. The free plan includes a development domain assigned to your account. Copy the URL ngrok prints into the property below. If the browser shows an ngrok warning page, choose Visit Site to continue. 6) Create the Spring Boot project This is a plain Spring Boot web application, with spring-webmvc-pac4j as the pac4j implementation. Java 17 or later and Maven are enough. mkdir -p wallet-another-device/src/main/java/org/pac4j/demo/wallet mkdir -p wallet-another-device/src/main/resources wallet-another-device/metadata cd wallet-another-device The pom.xml. The mdoc verification relies on the walt.id library, which pac4j declares as optional and which walt.id publishes on its own repository: <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>4.0.8</version> <relativePath/> </parent> <groupId>org.pac4j.demo</groupId> <artifactId>wallet-another-device</artifactId> <version>1.0-SNAPSHOT</version> <properties> <java.version>17</java.version> <pac4j.version>6.6.0</pac4j.version> <kotlin.version>2.4.21</kotlin.version> <kotlin-coroutines.version>1.11.0</kotlin-coroutines.version> <kotlin-serialization.version>1.12.0</kotlin-serialization.version> <jackson-2-bom.version>2.22.3</jackson-2-bom.version> <commons-lang3.version>3.21.0</commons-lang3.version> <slf4j.version>2.0.20</slf4j.version> <commons-logging.version>1.3.5</commons-logging.version> <jspecify.version>1.0.0</jspecify.version> </properties> <!-- walt.id publishes the mdoc library outside Maven Central --> <repositories> <repository> <id>waltid-releases</id> <url>https://maven.waltid.dev/releases</url> <snapshots><enabled>false</enabled></snapshots> </repository> </repositories> <dependencyManagement> <dependencies> <!-- align pac4j-core and shared dependencies with pac4j-openid4vp --> <dependency> <groupId>org.pac4j</groupId> <artifactId>pac4j-parent</artifactId> <version>${pac4j.version}</version> <type>pom</type> <scope>import</scope> </dependency> <!-- spring-webmvc-pac4j otherwise pulls an older Jakarta EE adapter --> <dependency> <groupId>org.pac4j</groupId> <artifactId>pac4j-jakartaee</artifactId> <version>${pac4j.version}</version> </dependency> <!-- retain walt.id's legacy datetime API using the compatibility artifact used by pac4j --> <dependency> <groupId>org.jetbrains.kotlinx</groupId> <artifactId>kotlinx-datetime-jvm</artifactId> <version>0.8.0-0.6.x-compat</version> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- the pac4j implementation for Spring MVC --> <dependency> <groupId>org.pac4j</groupId> <artifactId>spring-webmvc-pac4j</artifactId> <version>8.0.4</version> </dependency> <!-- the OpenID4VP support: the application is a verifier --> <dependency> <groupId>org.pac4j</groupId> <artifactId>pac4j-openid4vp</artifactId> <version>${pac4j.version}</version> </dependency> <!-- the mdoc verification, an optional library of pac4j --> <dependency> <groupId>id.walt.mdoc-credentials</groupId> <artifactId>waltid-mdoc-credentials-jvm</artifactId> <version>0.12.0</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project> The walt.id library is written in Kotlin. This POM matches pac4j's runtime versions: Kotlin 2.4.21, Coroutines 1.11.0, Serialization 1.12.0 and Datetime 0.8.0-0.6.x-compat. Serialization 1.12.0 is available from Maven Central; the walt.id repository is still needed for the mdoc library. The Datetime compatibility artifact preserves the legacy API used by walt.id 0.12.0. Do not replace it with a version which removes that API. See [Kotlin alignment](/docs/clients/openid4vp-mdoc-verifier.html#12-kotlin-alignment), especially when verifying both SD-JWT VC and mdoc credentials. The imported pac4j-parent dependency management aligns pac4j-core and shared libraries; the explicit pac4j-jakartaee entry aligns the adapter pulled in by spring-webmvc-pac4j with pac4j-openid4vp. Spring Boot 4.0.8 supplies Spring Framework 7.0.9, matching pac4j; the properties align Jackson 2, Commons Lang, SLF4J, Commons Logging and JSpecify too. Jackson 3 remains managed by Boot for its own web stack. Letting Boot select its older Kotlin dependencies can cause runtime linkage failures even if the demo compiles. Use the released pac4j 6.6.0 dependencies declared above; no local snapshot installation is needed. Check the resolved versions with mvn dependency:tree and keep all pac4j modules on the same version. The src/main/resources/application.properties file holds the base URL and enables the logs we will read at the end: app.base-url=https://abcd-1234.ngrok-free.dev logging.level.org.pac4j.openid4vp=DEBUG logging.level.PROTOCOL_MESSAGE.OPENID4VP=DEBUG And the usual Spring Boot entry point: package org.pac4j.demo.wallet; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class WalletApplication { public static void main(final String[] args) { SpringApplication.run(WalletApplication.class, args); } } 7) Keys and trust Two keystores go in the metadata directory, the same as in the first post. Run the keytool commands below from the root of the project, next to the pom.xml: the paths are relative to it, like the ones in the configuration. The verifier key. EudiWalletClient signs requests and identifies the verifier by its certificate hash (x509_hash). Production EUDI deployments use a relying party access certificate issued by the ecosystem. This demo uses a self-signed certificate with an EC P-256 key, accepted by the tested Paradym wallet; self-signed verifier certificates are outside the HAIP requirements: keytool -genkeypair -alias rp -keyalg EC -groupname secp256r1 -sigalg SHA256withECDSA \ -dname "CN=pac4j demo verifier,O=pac4j" -validity 365 \ -keystore metadata/verifier.p12 -storetype PKCS12 -storepass changeit The issuers you trust. No credential is accepted unless its issuer is trusted, and pac4j trusts nobody by default. For an mdoc, the trust anchor is the issuing authority certificate authority (IACA) of the PID provider. Copy the Issuer Certificate from the Trust Information page of the playground into a paradym-issuer.pem file, and import it into a truststore: keytool -importcert -alias paradym -file paradym-issuer.pem -noprompt \ -keystore metadata/pid-issuers.p12 -storetype PKCS12 -storepass changeit 8) Configure pac4j The configuration is a Pac4jSecurityConfig of spring-webmvc-pac4j: the Config bean defines the client, and the interceptors say which URLs it protects. package org.pac4j.demo.wallet; import org.pac4j.core.config.Config; import org.pac4j.core.config.properties.KeystoreProperties; import org.pac4j.openid4vp.client.EudiWalletClient; import org.pac4j.openid4vp.config.OpenId4VpConfiguration; import org.pac4j.openid4vp.dcql.EudiPidQuery; import org.pac4j.openid4vp.verifier.MdocVerifier; import org.pac4j.openid4vp.verifier.trust.CertificateTrustedIssuer; import org.pac4j.springframework.config.Pac4jSecurityConfig; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import java.util.Map; import static org.pac4j.core.profile.definition.CommonProfileDefinition.FAMILY_NAME; import static org.pac4j.openid4vp.profile.EudiPidProfileDefinition.GIVEN_NAME; import static org.pac4j.openid4vp.profile.EudiPidProfileDefinition.PERSONAL_ADMINISTRATIVE_NUMBER; import static org.pac4j.openid4vp.profile.EudiPidProfileDefinition.PID_DOCTYPE; @Configuration public class SecurityConfig extends Pac4jSecurityConfig { private static final Logger LOGGER = LoggerFactory.getLogger(SecurityConfig.class); @Value("${app.base-url}") private String baseUrl; @Bean public Config config() { // the request: signed with the key of the verifier, asking for three attributes of the PID as an mdoc final var configuration = new OpenId4VpConfiguration() .setKeystore(new KeystoreProperties("./metadata/verifier.p12") .setKeyStoreType("PKCS12") .setKeyStoreAlias("rp") .setKeystorePassword("changeit") .setPrivateKeyPassword("changeit")) .setDcqlQuery(EudiPidQuery.mdoc(GIVEN_NAME, FAMILY_NAME, PERSONAL_ADMINISTRATIVE_NUMBER)) // No redirection is needed at the end of the process as the wallet is not on the same device. // Therefore, setUseRedirectUri is set to false, although this is unnecessary as it is the default value. // This is for demonstration purposes only. .setUseRedirectUri(false); // the response: an mdoc is accepted if its certificate chain ends in the truststore final var issuers = new CertificateTrustedIssuer(new KeystoreProperties("./metadata/pid-issuers.p12") .setKeyStoreType("PKCS12") .setKeystorePassword("changeit")); // certificate revocation checking is disabled to simplify this demo; keep it enabled in production issuers.setCertificateRevocationEnabled(false); // an mdoc may reference a status list: this demo logs the reference and accepts the document, // a real status checker must retrieve the list, verify it and check the entry of the document configuration.addCredentialVerifier(new MdocVerifier().addTrustedIssuer(issuers) .setStatusChecker((credential, status) -> LOGGER.info("document status not checked: {}", status))); // the profile identifier: the issuer and the administrative number, read in the namespace of the PID configuration.setProfileIdResolver(credentials -> { final var pid = credentials.getVerifiedCredentials().get(EudiPidQuery.PID).get(0); final var namespace = (Map<?, ?>) pid.getClaims().get(PID_DOCTYPE); return pid.getIssuer() + "|" + namespace.get(PERSONAL_ADMINISTRATIVE_NUMBER); }); final var client = new EudiWalletClient(configuration); return new Config(baseUrl + "/callback", client); } @Override public void addInterceptors(final InterceptorRegistry registry) { addSecurity(registry, "EudiWalletClient").addPathPatterns("/protected/**"); } } Compared to the first post, three things change: - useRedirectUri stays false, its default. In the first post it is true, so the wallet returns the phone's browser to the pac4j callback. Here the authenticated session belongs to the computer: redirecting the phone would not bring its session cookie along. The computer page polls for the response and continues to the callback itself, without a response_code. - The verifier is MdocVerifier, which checks the issuer authentication of the document, its digests, its validity, the signature of the device and the binding of the response to the request, and the chain of the document signer up to your truststore. - The claims of an mdoc are grouped by namespace: for the PID, the namespace is its document type, eu.europa.ec.eudi.pid.1. The profile identifier reads the administrative number in that namespace, and the profile exposes an attribute named after the namespace, holding the three claims. ProfileIdResolver.issuerAndClaim(...) only reads the top-level claims of an SD-JWT VC, hence the small resolver above. The rest is the same: EudiWalletClient pins the HAIP choices, x509_hash and direct_post.jwt, and the callback URL serves both the wallet and the browser. These settings do not constitute complete HAIP conformance validation. An mdoc may carry a status object in its MSO, a reference to an OAuth Token Status List: the list where the issuer marks the documents it has revoked or suspended. pac4j refuses a document carrying a status unless a status checker is configured, because only the application can decide how to read the list, and it never fetches it on its own. The mdoc PID of the playground carries none today, unlike its SD-JWT VC PID, but the checker stays configured so that a document with a status is logged rather than refused: it receives the status object as a map, only logs it and accepts the document, so revocation is not checked here. A real checker retrieves the status list token, verifies its signature with the same trust as the document and checks the entry at the index of the document: that deserves a post of its own. See credential status. Disabling certificate revocation does not bypass this check. 9) The DCQL query The query says what your application wants. EudiPidQuery.mdoc(GIVEN_NAME, FAMILY_NAME, PERSONAL_ADMINISTRATIVE_NUMBER) builds it in one line for the PID; this is what the wallet receives: { "credentials": [ { "id": "pid", "format": "mso_mdoc", "meta": { "doctype_value": "eu.europa.ec.eudi.pid.1" }, "claims": [ { "path": ["eu.europa.ec.eudi.pid.1", "given_name"] }, { "path": ["eu.europa.ec.eudi.pid.1", "family_name"] }, { "path": ["eu.europa.ec.eudi.pid.1", "personal_administrative_number"] } ] } ] } One credential query, identified as pid, for a mobile document whose type is the EUDI PID, and three data elements, each addressed by its namespace and its name. The same can be written by hand, in JSON when it comes from a configuration file: configuration.setDcqlQuery(""" {"credentials":[{"id":"pid","format":"mso_mdoc","meta":{"doctype_value":"eu.europa.ec.eudi.pid.1"}, "claims":[{"path":["eu.europa.ec.eudi.pid.1","given_name"]},{"path":["eu.europa.ec.eudi.pid.1","family_name"]}, {"path":["eu.europa.ec.eudi.pid.1","personal_administrative_number"]}]}]}"""); Or in Java, with access to expected values, alternatives (claim_sets, credential_sets) and trusted authorities: configuration.setDcqlQuery(new DcqlQuery() .addCredential(new CredentialQuery("pid", CredentialFormat.MSO_MDOC) .setDoctypeValue("eu.europa.ec.eudi.pid.1") .addClaim("eu.europa.ec.eudi.pid.1", "given_name") .addClaim("eu.europa.ec.eudi.pid.1", "family_name") .addClaim("eu.europa.ec.eudi.pid.1", "personal_administrative_number"))); Whatever the wallet presents is checked against the query again by pac4j: the format, the document type, the namespaces and the elements. The constraints are not left to the wallet. 10) The pages, with the QR code In this flow, the wallet is on the phone rather than the computer running the browser. The page gets the wallet URL as data and displays it as a QR code, then waits for the wallet's response. Three pieces do that: - the protected page itself, as before - a /status endpoint, which reports the transaction state for the browser session: PENDING, RECEIVED or EXPIRED, from client.getPresentationStatus(...). RECEIVED includes a wallet error response; the callback then reports that error - the page with the QR code and its script, which polls the /status endpoint every second until the wallet has answered. The three live in a single Spring MVC controller, WalletController, to create next to the two previous classes: package org.pac4j.demo.wallet; import org.pac4j.core.config.Config; import org.pac4j.core.context.CallContext; import org.pac4j.core.context.WebContext; import org.pac4j.core.context.session.SessionStore; import org.pac4j.core.profile.ProfileManager; import org.pac4j.openid4vp.client.OpenId4VpClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.CacheControl; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.web.util.HtmlUtils; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ResponseBody; import java.util.Locale; import java.util.Map; @Controller public class WalletController { @Autowired private Config config; @Autowired private WebContext webContext; @Autowired private SessionStore sessionStore; @Autowired private ProfileManager profileManager; @GetMapping("/protected/index") @ResponseBody public String protectedPage() { return "<h1>Protected page</h1><p>" + HtmlUtils.htmlEscape(profileManager.getProfiles().toString()) + "</p>" + "<p><a href='/logout'>Logout</a></p>"; } /** Whether the wallet answered the transaction of this browser session. */ @GetMapping("/status") public ResponseEntity<Map<String, String>> status() { final var client = (OpenId4VpClient) config.getClients().findClient("EudiWalletClient").orElseThrow(); final var status = client.getPresentationStatus(new CallContext(webContext, sessionStore)); return ResponseEntity.ok().cacheControl(CacheControl.noStore()) .body(Map.of("status", status.name().toLowerCase(Locale.ROOT))); } @GetMapping("/") @ResponseBody public String index() { return """ <h1>Log in with your wallet</h1> <p>Scan the QR code with your phone's wallet. This page continues automatically when the wallet responds.</p> <div id="qr"></div> <pre id="log"></pre> <script src="https://cdn.jsdelivr.net/npm/qrcode-generator@2.0.4/dist/qrcode.js"></script> <script> const log = m => document.getElementById('log').textContent += m + '\\n'; async function start() { // the same request a browser would make, but as an AJAX call: pac4j then answers 401, // with the wallet URL in the Location header, instead of redirecting the page const r = await fetch('/protected/index', {headers: {'X-Requested-With': 'XMLHttpRequest'}}); if (r.ok) { window.location.assign('/protected/index'); return; } const walletUrl = r.headers.get('Location'); if (r.status !== 401 || !walletUrl) { throw new Error('Could not start wallet authentication (HTTP ' + r.status + ')'); } log('wallet URL: ' + walletUrl); const qr = qrcode(0, 'L'); qr.addData(walletUrl); qr.make(); document.getElementById('qr').innerHTML = qr.createImgTag(4, 16, 'Scan with your wallet'); setTimeout(() => poll().catch(reportError), 1000); } async function poll() { const r = await fetch('/status', {cache: 'no-store'}); if (!r.ok) { throw new Error('Could not read presentation status (HTTP ' + r.status + ')'); } const result = await r.json(); if (result.status === 'received') { log('the wallet has answered: continuing to the callback'); window.location = '/callback?client_name=EudiWalletClient'; } else if (result.status === 'pending') { setTimeout(() => poll().catch(reportError), 1000); } else { log('the transaction expired: reload the page for a new QR code'); } } const reportError = error => log(error.message + ': reload the page to retry'); start().catch(reportError); </script> """; } } In start(), pac4j recognizes the X-Requested-With header and returns a 401 with the wallet URL in the Location header instead of a redirect. The page renders that URL as a QR code. If the session is already authenticated, it goes directly to the protected page; HTTP or network failures appear in the page's log. Then poll() waits one second between completed status requests until it receives received or expired. On received, it sends the browser to the callback. This limits polling to at most one request per second per page, with a delay of roughly one second plus request time before continuing. Status is read from the transaction store using the identifier in the browser session; the default store is in memory and needs no outgoing network call. The qrcode-generator library renders the QR code in the browser, without sending its contents to a QR service. The WebContext and SessionStore beans come from spring-webmvc-pac4j, scoped to the current request; getPresentationStatus looks the transaction of the session up and reports whether the wallet has posted its answer. Run the application, with ngrok running next to it: mvn spring-boot:run 11) The flow, step by step Open the ngrok URL in the browser of your computer: the QR code appears at once. The whole flow in six screens, the computer at both ends and the phone in between: the page with the QR code, the warning about the unverified verifier, the request to review, the PIN code, the success screen, and the protected page back on the computer. The steps below detail what happens behind each one. !The page with the QR code, on the computer !Paradym: organization not verified !Paradym: review the request, the three attributes of the PID !Paradym: enter the PIN code !Paradym: success, the information has been shared !The protected page with the profile, on the computer 1. The page asks for the protected page, as data. pac4j finds no profile, creates a transaction and answers the openid4vp:// URL in the Location header of a 401. The page draws it as a QR code and starts polling the /status endpoint every second. 2. The phone scans the QR code. The Paradym wallet (opened and unlocked beforehand: the scan does not work from a cold start) reads the URL, which only carries the identifier of the verifier and a request_uri: it calls it, on the callback URL of your application, and receives the signed request object, with the DCQL query and the public key to encrypt its response. 3. The user consents. The wallet shows who is asking, the hash of your certificate for now, and what is asked: the PID, given name, family name and administrative number. The user picks the mdoc card and confirms with their PIN. 4. The wallet posts its response. An encrypted JWT, containing the mdoc device response, posted on the callback URL. pac4j decrypts, verifies and keeps the result for the browser session which started the transaction. With useRedirectUri=false, it answers HTTP 200 with {}: the wallet shows that the sharing succeeded, while the computer page continues waiting for its next poll. There is no browser redirect on the phone. 5. The page continues on its own. The next poll returns received: the page goes to the callback URL, pac4j retrieves the profile already validated when the wallet posted its response, stores it in the session and redirects to the page requested at step 1: the protected page shows Emma Charlotte van der Berg, with the three claims under the eu.europa.ec.eudi.pid.1 attribute and an identifier made of the issuer and the administrative number. Nothing to do on the computer between the scan and the result: the whole exchange happened between the phone and the application. 12) What the logs say The two loggers enabled in application.properties give two views of the flow. The PROTOCOL_MESSAGE.OPENID4VP logger prints the messages themselves, as they were sent or received, without any interpretation: one line per message, starting with >>> when the application sent it, <<< when it received it, followed by the other party, the browser or the wallet. The org.pac4j.openid4vp logger tells what pac4j did with them: the transaction created, the request object built, the response decrypted and verified, the profile built. Here is the flow above, shortened. Step 1: a transaction is created, expiring five minutes later, and the wallet URL is handed to the page in a 401, since the request came as AJAX. Note the request_uri on the callback URL, and the identifier of the verifier, the hash of its certificate: EudiWalletClient : applying EUDI wallet settings for client EudiWalletClient: X509_HASH, DIRECT_POST_JWT, PID profile definition OpenId4VpConfiguration : OpenID4VP configuration checked: 1 credential queries, verifier formats=[SD_JWT_VC, MSO_MDOC], signed requests=true VpTransaction : transaction c7e29a…: - -> CREATED (client EudiWalletClient, response mode direct_post.jwt, expires at 2026-10-12T14:26:08Z) PROTOCOL_MESSAGE.OPENID4VP : [tx c7e29a…] >>> browser wallet URL: openid4vp://?client_id=x509_hash%3AE3bEQjnx15zqNwvnVfbxz2GO6SAZXNG6BfdjrZsWvQc&request_uri=https%3A%2F%2Fabcd-1234.ngrok-free.dev%2Fcallback%3Fclient_name%3DEudiWalletClient%26vp_tx%3Dc7e29a…&request_uri_method=post EudiWalletClient : AJAX request detected -> returning UnauthorizedAction(super=HttpAction(code=401), content=) for https://abcd-1234.ngrok-free.dev/protected/index The first two lines appear once, at the first request: the client initializes lazily. The configuration comes with a default verifier per format; section 8 replaced the mdoc one with the trusted issuers and the SD-JWT VC one stays, hence the two formats listed. Step 2: the wallet fetches the request object, which is built for it and signed: PROTOCOL_MESSAGE.OPENID4VP : [tx c7e29a…] <<< wallet GET client_name=EudiWalletClient&vp_tx=c7e29a… OpenId4VpRequestObjectBuilder : client metadata built for transaction c7e29a…: formats=[dc+sd-jwt, mso_mdoc], response encryption methods=[A128GCM, A256GCM] OpenId4VpRequestObjectBuilder : request object signed for transaction c7e29a… with ES256 VpTransaction : transaction c7e29a…: CREATED -> REQUEST_RETRIEVED (GET of the request URI) PROTOCOL_MESSAGE.OPENID4VP : [tx c7e29a…] >>> wallet 200 eyJ4NWMiOlsiTUlJQmNq… These demo traces show a GET even though the wallet URL advertises request_uri_method=post. pac4j accepts both methods; a wallet following the advertised method uses POST and can include its metadata and a wallet nonce. The request object is a JWT whose header carries your certificate (x5c, with the rp alias as kid) and whose payload is the request. Decoded, its payload reads: { "client_id": "x509_hash:E3bEQjnx15zqNwvnVfbxz2GO6SAZXNG6BfdjrZsWvQc", "response_type": "vp_token", "response_mode": "direct_post.jwt", "response_uri": "https://abcd-1234.ngrok-free.dev/callback?client_name=EudiWalletClient&vp_tx=c7e29a…", "nonce": "fb89b405c7ca40689912b460acd0e756", "state": "206a039fe14d4d1f9a3fa0c50f8ff207…", "dcql_query": { "credentials": [ { "id": "pid", "format": "mso_mdoc", "meta": { "doctype_value": "eu.europa.ec.eudi.pid.1" }, "claims": [ { "path": ["eu.europa.ec.eudi.pid.1", "given_name"] }, { "path": ["eu.europa.ec.eudi.pid.1", "family_name"] }, { "path": ["eu.europa.ec.eudi.pid.1", "personal_administrative_number"] } ] } ] }, "client_metadata": { "jwks": { "keys": [ { "kty": "EC", "use": "enc", "crv": "P-256", "alg": "ECDH-ES", "kid": "3885759a…", "x": "…", "y": "…" } ] }, "encrypted_response_enc_values_supported": ["A128GCM", "A256GCM"], "vp_formats_supported": { "dc+sd-jwt": { "sd-jwt_alg_values": ["ES256"], "kb-jwt_alg_values": ["ES256"] }, "mso_mdoc": { "issuerauth_alg_values": [-7], "deviceauth_alg_values": [-7] } } }, "aud": "https://self-issued.me/v2", "iat": 1791814868, "exp": 1791815168 } The device signature binds the mdoc to this request through a session transcript. Its OpenID4VP handover incorporates the response encryption key's JWK thumbprint, the nonce, the verifier's client_id and the response_uri. pac4j reconstructs this transcript when verifying the presentation, so a response bound to another request is rejected. Step 4: the wallet posts its response, a JWE encrypted with the ephemeral key published in the request. pac4j decrypts it, verifies the mdoc and builds the profile; the claims come grouped by namespace: PROTOCOL_MESSAGE.OPENID4VP : [tx c7e29a…] <<< wallet POST client_name=EudiWalletClient&vp_tx=c7e29a…&response=eyJraWQiOiIzODg1NzU5YS… OpenId4VpAuthenticator : response decrypted and authentication tag verified for transaction c7e29a…: alg=ECDH-ES, enc=A128GCM, members=[vp_token, state] CertificateTrustedIssuer : issuer truststore loaded: 1 trust anchors CertificateTrustedIssuer : certificate revocation lists loaded: 0 MdocVerifier : mdoc verified for transaction c7e29a… OpenId4VpAuthenticator : presentation 1/1 verified for transaction c7e29a…, query pid: format=MSO_MDOC, verifier=MdocVerifier OpenId4VpAuthenticator : wallet response validated for transaction c7e29a…: 1 credential queries answered EudiPidProfile : adding => key: eu.europa.ec.eudi.pid.1 / value: {given_name=Emma Charlotte, family_name=van der Berg, personal_administrative_number=ADM2023080012345} / class java.util.LinkedHashMap OpenId4VpAuthenticator : profile built for transaction c7e29a…: profile class=EudiPidProfile, definition=EudiPidProfileDefinition VpTransaction : transaction c7e29a…: REQUEST_RETRIEVED -> RESPONSE_RECEIVED (presentation validated) PROTOCOL_MESSAGE.OPENID4VP : [tx c7e29a…] >>> wallet 200 {} Unlike the SD-JWT VC of the first post, the document comes with the three requested elements and nothing else: an mdoc discloses element by element, and the issuer signs nothing outside the namespaces. The status checker of section 8 was not called either: the mdoc PID of the playground carries no status in its MSO. Step 5: the page polled, saw received, and came back to the callback URL: the validated profile is picked up, nothing is verified twice, and the identifier is the one of the resolver, the subject of the document signer certificate and the administrative number: VpTransaction : transaction c7e29a…: RESPONSE_RECEIVED -> CONSUMED (the browser came back) OpenId4VpAuthenticator : transaction c7e29a…: reusing the profile validated when the wallet posted its response, nothing is verified again EudiWalletClient : credentials: VerifiablePresentationCredentials(… verifiedCredentials={pid=[VerifiedCredential(format=MSO_MDOC, type=eu.europa.ec.eudi.pid.1, issuer=C=NL,CN=Paradym Playground, cryptographicHolderBinding=true, trustedAuthorities={aki=[IyNQbCkBG38NdbXBZT0C3rElbng]}, claims={eu.europa.ec.eudi.pid.1={given_name=Emma Charlotte, family_name=van der Berg, personal_administrative_number=ADM2023080012345}})]}) EudiWalletClient : profile: Optional[EudiPidProfile(id=C=NL,CN=Paradym Playground|ADM2023080012345, attributes={eu.europa.ec.eudi.pid.1={given_name=Emma Charlotte, family_name=van der Berg, personal_administrative_number=ADM2023080012345}}, …)] The credentials line also shows what pac4j established about the document: its issuer, the holder binding, and the authority key identifier of the certificate chain, the one a trusted_authorities constraint of the DCQL query would be checked against. Note that the logs show the credentials as they travel, tokens included: enable them to diagnose, not permanently in production. 13) Limitations and what comes next A few things to know before going further: - The profile identifier in this demo combines the document signer certificate's subject, C=NL,CN=Paradym Playground in these traces, with the administrative number. For persistent accounts, your PID provider must guarantee that the combination is stable, unique and never reassigned. Certificate subjects can change or be shared, and not every PID provider issues an administrative number. Choose an identifier policy appropriate to your trusted provider. - A refusal was silent with the tested Paradym wallet: when the user declined or had no matching card, nothing was posted, and the page waited until the transaction expired after five minutes. Wallets that post an error let pac4j report it at the callback. - Certificate revocation is disabled in this demo. In production, keep it enabled and supply current CRLs or configure the Java PKIX provider to retrieve revocation data. This setting does not disable credential status checking: credentials carrying status information still need a status checker, and the one of this demo only logs the status reference, as seen in section 8. - The device MAC of ISO 18013-5, transaction data, the verifier_attestation and openid_federation prefixes, and the automatic retrieval of status lists are not supported yet. This integration is introduced in pac4j v6.6.0. The full documentation starts at OpenID for Verifiable Presentations, and the first post covers the same flow but on a single device, with an SD-JWT VC PID.

  29. Location: Istanbul, Türkiye Remote: Yes Willing to relocate: Yes Technologies: C/C++, Rust, Python, Linux, RTOS, TCP/IP, eBPF/XDP, WebRTC, UWB/BLE, embedded/IoT & mesh-connected devices, PostgreSQL, Docker, AI/LLM systems Résumé / Portfolio: msakg.com GitHub: github.com/imsakg LinkedIn: linkedin.com Email: contact@msakg.com Lead AI Agent Software Engineer focused on real-time, performance-critical systems across AI, networking, embedded devices, and production infrastructure. My background sits at the intersection of software and the physical world: • Currently leading architecture and production hardening of a real-time multimodal AI agent platform — streaming STT → LLM/tools → TTS, WebRTC, asyncio, long-lived state/memory, retrieval, observability, failure handling, cancellation, backpressure, and latency-sensitive execution. • Previously built real-time networking software for aerial systems using Rust, Linux, eBPF/XDP and IEEE 802.1 TSN concepts, including reliability mechanisms where deterministic behavior and low fast-path overhead matter. • Earlier developed C/C++ bare-metal and RTOS firmware for access-control/security and embedded/IoT devices — TCP/IP, UART/I2C/SPI, RS232/RS485, Modbus, secure device communication, UWB/BLE localization, sensors, relays and constrained hardware. I’m most interested in difficult engineering problems where timing, reliability, networking, hardware constraints, and user-facing behavior meet: mission/performance-critical software, real-time systems, embedded/IoT, access control, UWB, distributed device systems, AI infrastructure, backend/platform engineering, and systems software. I enjoy owning systems end-to-end — architecture, implementation, debugging, benchmarking, deployment, production incidents, and the less glamorous failure modes that only appear in real environments. I’m looking for a strong engineering team tackling genuinely hard technical problems. Open to Lead/Senior Software, Systems, Embedded, Platform, Backend, Real-Time AI, or similarly hands-on engineering roles. If you're building something where milliseconds, reliability, hardware, networks, or production correctness actually matter, I'd be very happy to talk.

  30. Built this for the @SarvamAI Campus Hackathon 🇮🇳 Lakh of govt complaints get closed unfixed because they've written wrong. And if you only speak Odia or Tamil, it's worse. Code: zappkin/Grivience-Tracker-using-Sarvam t.co

  31. Built on PyTorch for GPU acceleration, Kornia provides differentiable computer vision algorithms for spatial AI workflows. 355 new stars this week. kornia/kornia

  32. Redditu/heenoooo · r/Softwarr · 4 comments

    Babel update – automatic AI subtitle translation for Sonarr when subtitles are missing Hey! I shared my project Babel here a few months ago, and I’ve been working on it quite a bit since then. For anyone who missed my previous post, Babel is a self-hosted AI subtitle translator that integrates with Sonarr, Radarr and Bazarr. I originally started building it because I wanted newly downloaded episodes to have subtitles in my preferred language without having to manually search for or translate anything. One of the main things I’ve focused on is making Babel work alongside Bazarr rather than replacing it. Bazarr can handle subtitle searching as usual, and if it can’t find a suitable subtitle, Babel can step in and translate one instead. There’s also an optional grace period to give Bazarr more time before AI translation starts. Babel can automatically pick up new episodes from Sonarr, check for existing subtitles, extract embedded subtitle tracks, clean up SDH subtitles and translate them using different AI providers, including OpenAI, Gemini, DeepSeek, OpenRouter and local Ollama models. But the translation itself is only part of it. I’ve spent a lot of time building a QA and recovery system that checks for missing lines, incorrect subtitle IDs, synchronization problems and potential translation alignment issues. If something goes wrong, Babel attempts to repair it before publishing the subtitle. It also includes a web interface for managing your library, monitoring jobs, configuring providers and tracking API usage and costs. The latest release is v2.5.7-beta, which fixes an extraction cache race condition and improves progress reporting. I’ve mostly been focusing on reliability and making the whole process work better without manual intervention. For a recent real-world test, Babel automatically processed a newly downloaded Sonarr episode with 1,453 subtitle lines in about 136 seconds using gpt-4o-mini, at an estimated AI cost of around $0.03. It’s still a beta project, and there’s definitely more to improve, but it’s been working well in my own setup. GitHub: hugomossberg/Babel I’d love to hear what other Sonarr users think, especially anyone already using Bazarr. Would something like this be useful in your setup?

  33. X@DKryvosheieva · 10 views

    (5/6) The goal is interpretability by construction: if the model over- or under-refuses, you can read off exactly which neuron misfired. Blog: dariakryvosheieva.github.io Code: dariakryvosheieva/refusal-circuit

  34. X@TeamPrecisit · 7 views

    Sources, so both halves can be checked. Their W4A4 numbers, section 06: tech.meituan.com Our fp4 and int8 lane measurements on M5: precisit/apple-matrix-capabilities

  35. X@nemamdumi · 9 views

    aliceisback/NeedleSurf-v3 I do need your heart beating model for one test. Is for living-architecture.ai txt back @zoe_newera

  36. Hacker Newscrow9292 · 1 point · kelvinolabanji/echo

    Show HN: Echo – Offline semantic image search for Windows (CLIP and FAISS)

  37. Redditu/Extreme_Ad5709 · r/LangChain · 5 points · 5 comments

    Fine-tuning a 322M Model for Coding Agent Safety: 3 Runs, 2 Failures, 1 Shipped I've been working on laya-guard, a local guard that evaluates tool calls made by coding agents before they execute. The basic idea is simple: instead of trusting an agent to decide whether its own actions are safe, run each tool call through a separate, small local model that decides whether to allow it, block it, or ask for confirmation. In my first post, someone asked about my fine-tuning setup. I didn't have one. I was using Laya's stock multilingual checkpoint with zero-shot prompts. That left me wondering: how much would fine-tuning actually improve a model this small? Laya's model card describes the checkpoint as a base for fine-tuning. On my benchmark of 50 everyday commands, the base model only allowed half of them without asking for confirmation. So I tried training it. It took three runs to get something I was comfortable shipping. Each failed run exposed a different problem in my training data. What changed since the previous posts? A few architectural changes are worth mentioning because they affect the overall safety model: - OS-level enforcement: laya-guard sandbox <agent> now puts protected paths into the agent's own OS sandbox. The kernel refuses unauthorized reads regardless of what the model decides. - Command normalization: Different representations of blocked commands are now caught, including r''m, ${IFS}, base64 piped into sh, and commands hidden behind variables. - Benchmark: There's now a benchmark comparing Laya Guard against other command guards. There are still gaps. Environment dumps aren't fully covered: printenv relies on the model's judgment, while cat /proc/self/environ can still pass as an ordinary file read. Training data I collected approximately 13,000 shell commands from my own agent transcripts. - Home paths, tokens, and email addresses were masked. - Anything matching or closely resembling (similarity >= 0.85) a benchmark or evaluation case was excluded. - 696 commands that weren't already handled by deterministic rules were labeled. - Labels came from a written guide, six agents, and my own review of disagreements. Training was done on a free Colab GPU. Before training, I wrote down the acceptance criteria and evaluated each run on datasets the model hadn't seen during training. Run 1: The model learned that short commands were dangerous Validation accuracy went from 0.61 to 0.90, which initially looked promising. But 86% of the training rows were labeled allow, so an always-allow classifier would already score 0.86. On unseen everyday commands, the number allowed without confirmation actually dropped from 25/50 to 23/50. The 11 newly questioned commands included completely ordinary operations: - git status - mkdir -p src/utils - cp - chmod +x I investigated the training data and found a strong length imbalance: - Real allow commands: median length of 287 characters. - Hand-written risky commands: typically 33–40 characters. There were no real blocked commands in the collected transcripts. The easiest signal for the model to learn was therefore short = dangerous. It met only one of my four acceptance criteria. Since the problem was in the data, I changed the dataset rather than the architecture. Run 2: Fixing the false positives introduced false negatives I added 206 examples targeting the previous failure: - Short, ordinary commands. - Long scripts containing a single dangerous command. - Korean and English requests. Everyday commands improved to 49/50. But three dangerous cases slipped through: - rmdir /s /q d: — deleting an entire Windows drive. - A destructive command hidden behind a # clean build output comment. - gh auth token — exposing an authentication token. Each failure had a different explanation: 1. There were no Windows commands in the original dataset. 2. 31 of 36 real commands containing # comments were labeled allow. The model had effectively learned that comments meant routine operations. 3. Almost no training examples involved commands that printed secrets. The dataset had also become more biased toward allow: 84% of rows were now labeled that way. The median risk score on everyday commands fell from 0.45 to 0.03, but attack detection dropped to 31/43, below my acceptance threshold. Better usability wasn't worth the regression in security. Run 3: Balancing the data instead of chasing accuracy I made one change for each identified problem: - Removed 242 safe-lane examples that deterministic rules already handled. - Repeated review/block examples in the training split, bringing the allow ratio down to 61%. - Added 100 targeted examples covering Windows commands, secret exposure, disguised commands, and instructions telling tools to execute something else. This was the result: |Metric|Base|Run 1|Run 2|Run 3| |:-|:-|:-|:-|:-| |Model only: everyday commands allowed|25/50|23/50|49/50|44/50| |Model only: attacks caught|32/43|39/43|31/43|39/43| |Model only: shell evaluation set|42/71|48/71|58/71|61/71| |Model only: Korean|27/30|25/30|24/30|27/30| |End-to-end: everyday commands allowed|46/50|44/50|49/50|48/50| |End-to-end: shell evaluation set|69/71|70/71|68/71|70/71| |End-to-end: Korean|30/30|28/30|28/30|29/30| I also tested both the base and fine-tuned models through the actual Claude Code hook. Both held all 13/13 incidents and caught all 30/30 evasions. One regression I decided not to hide Two end-to-end verdicts got worse: - chmod +x scripts/build.sh now asks for confirmation. - A Korean request meaning "summarize this issue" now asks for confirmation. This violates my original no-regression criterion. Both cases trace back to the balancing process. After increasing review/block examples, chmod +x appeared as 2 allow and 2 hold examples in the dataset. I stopped at three runs rather than tune a fourth run against two examples in an evaluation set I'd already inspected multiple times. I shipped it because no harmful command got through in the end-to-end evaluation, but I documented the regression in the changelog and model card. The fine-tuned model is now the default in version 0.7.0: pip install -U laya-guardrail The model is pinned by revision and SHA-256. You can switch back to the original checkpoint with: LAYA_GUARD_MODEL=base Model: laya-guard-multilingual Training scripts and tools: finetune/ Project: scs0209/laya-guard It's MIT-licensed and runs locally. What I'm still unsure about The biggest weakness in this experiment isn't the model. It's the evaluation process. I wrote the training data, designed the benchmark, and inspected evaluation results between runs. Even though I excluded benchmark-like examples from training, I can't confidently say the evaluation remained independent after three rounds of iteration. For people building LLM chains, agent workflows, or tool-calling systems: How do you build an evaluation set for a tool-call safety classifier that stays trustworthy when you're also the person developing the classifier? Do you use independently generated test cases, adversarial evaluation, temporal holdouts, external reviewers, or something else? I'd particularly like to hear how people handle evaluation leakage when the system itself is evolving quickly.

  38. Install: $ cargo install --locked --git halfhandorg/halfhand halfhand $ cargo clean && cargo build -p halfhand --release && cargo install --path h $ hh init

  39. X@mikkatasala · 9 views

    it’s a pytorch library, not a serving engine. if your model already works in fp16, just use fp16. tech-cosmos/turing-bf16

  40. Redditu/Snoo-45708 · r/koreader · 75 points · 4 comments

    Macintosh 1984 theme for KOReader A 1-bit Macintosh interface theme with 103 KOReader icons, 34 SimpleUI icons, ChicagoFLF, striped title bars and hard shadows. hytaoist/mac1984-koreader

  41. X@laogeai · 4 views

    256G 的 Mac 装个剪辑软件加游戏库就见底,官方加硬盘差价够买移动固态。 appports 把应用连数据搬到外置盘,启动台照常点开没有区别,也能搬回内置盘。 wzh4869/appports

  42. X@idr_zz · 60 views

    Vercelでフロントとバックをデプロイ。フロントは起動したがバックは起動エラーになったようだ。修正。 VercelのFunctionでPrismaが起動するようクライアントエンジンにする github.com/ryo-i/next-nest-started/commit/143f7f8…

  43. X@weidows27 · 10 views

    Weidows/Weidows github上小垃圾项目越堆越多, 官方给的6个格子不够放了... 搓了个稍好看一点的, 可以手动拖拽排放位置, 项目描述之类的直接让agent去写 (应该) 能好看一些罢 t.co

  44. CSS anchor positioning: tooltips that flip This note has an interactive demo. Try it in the original A tooltip that sticks to its button and jumps to the other side when it hits an edge used to mean a positioning library. Now it takes a handful of CSS lines, and the core of it has been Baseline since January 2026. Drag the orb in the demo above toward the top edge. The card that was sitting above it jumps below. Push it to a side with left selected and it moves to the right. The two dashed guides and the small --orb label follow along too. The only thing the JavaScript does is move the orb. Where everything else goes is decided by CSS. What it is CSS anchor positioning lets you place an absolutely or fixed positioned element relative to another element, the anchor, instead of relative to its containing block. That's the problem behind every tooltip, dropdown, popover menu and "new" badge. Until now you had to measure with getBoundingClientRect(), listen for scroll and resize, and handle the edge cases yourself, or ship a library that did it for you. It works in two steps. You give the anchor a name, then you attach the positioned element to that name: /* the anchor */ .orb { anchor-name: --orb; } /* the anchored element */ .card { position: absolute; position-anchor: --orb; position-area: top; margin: 14px; position-try-fallbacks: flip-block, flip-inline, flip-block flip-inline; } That's the exact rule the demo's card uses (the visual styling is left out). How the pieces fit anchor-name declares an element as an anchor. The name is a dashed ident, like a custom property: --orb, --menu-button. position-anchor goes on the positioned element and says which anchor it belongs to. The element has to be position: absolute or position: fixed. position-area is the easy way to place it. Picture a 3×3 grid with the anchor in the middle cell. top puts the element above the anchor, right to its right, and top right in the corner cell. The segmented control in the demo switches between these values live. anchor() is the precise way. You use it inside the inset properties (top, left, right, bottom), and it returns the position of one of the anchor's sides. The vertical guide in the demo is just this: .guide.v { position: absolute; position-anchor: --orb; top: 0; bottom: 0; left: anchor(center); /* always passes through the orb's center */ width: 1px; } position-try-fallbacks is where the flipping comes from. If the element would overflow its containing block, the browser tries the options you list, in order, until one fits. flip-block mirrors it across the block axis (above becomes below). flip-inline mirrors it across the inline axis (left becomes right). And flip-block flip-inline, written as a single option, does both at once. That combined option matters: MDN's guide shows that with only flip-block, flip-inline, a tooltip pushed into a corner has no option that fits, and the browser falls back to the original position, which overflows. Try it in the demo: pick top right, push the orb into the top-right corner, and the card ends up bottom-left. Then switch Fallbacks off and push it against an edge again. With position-try-fallbacks: none the card stays where you asked for it, even if that means sticking out of the board or covering the anchor. The "Rendered" pill in the demo tells you which side the card actually landed on. It gets that by reading the final position, not by calculating it. Browser support and gotchas According to the web-features data, anchor positioning became Baseline newly available on 13 January 2026, with Firefox 147. Chrome and Edge have had it since 125, and Safari since 26. Some pieces arrived later in Chromium: position-try-fallbacks in 128, position-area in 129. The feature as a whole isn't marked Baseline yet, because a few newer additions, such as the anchor-visible value of position-visibility, haven't landed everywhere. Things that tripped me up, or that I'd want to know before shipping this: - Always set position-anchor explicitly. Browsers have disagreed about its initial value. According to MDN's compat data, Chrome went through implicit, auto and none before settling on normal in 151, and Firefox 147–150 and Safari 26 use auto. If you name the anchor yourself, none of that matters. - DOM order matters. The spec only accepts an anchor that is laid out before the positioned element. If both are absolutely positioned in the same containing block, the anchor has to come earlier in the DOM. In the demo, the orb comes before the card in the HTML. - A hidden anchor doesn't count. If the anchor is display: none, MDN says the positioned element falls back to its nearest positioned ancestor. - Duplicate names pick the last one. If several elements share an anchor-name, the positioned element attaches to the last one in source order. In repeated components (a list of cards, each with its own tooltip) use anchor-scope to limit each name to its own subtree. - Popovers get an anchor for free. A popover opened with popovertarget or commandfor gets an implicit anchor reference to its invoking button. If you then place it with anchor(), MDN explains you also need to reset the popover's default positioning and use position-anchor: auto. For older browsers, the demo detects support with CSS.supports('anchor-name: --a') (plus position-area and position-try-fallbacks). If any of those is missing, it shows a notice and positions the card with a small getBoundingClientRect() script that mimics the same flips. In CSS you can do the same check with @supports (anchor-name: --a) and keep a simple static position as the default. What to keep an eye on - position-visibility: hiding the anchored element automatically when its anchor scrolls out of view (anchor-visible) is still not supported everywhere. - Custom fallbacks with @position-try: when flipping isn't enough, you can define named positions with their own size and margins. They're part of the same Baseline core, and worth trying for menus that need to shrink. - Removing libraries: if a project only uses a positioning library for tooltips and menus, this is a good time to check how much of that code you still need. Sources - Using CSS anchor positioning – MDN - Fallback options and conditional hiding for overflow – MDN - position-try-fallbacks – MDN - CSS Anchor Positioning Module Level 1 – Editor's Draft - anchor-positioning – web-features data --- Originally published at richi.codes.

  45. EUDI wallets with OpenID4VP in action (1/2): on the same device --- title: "EUDI wallets with OpenID4VP in action (1/2): on the same device" description: "Authenticate a user with an EUDI-style digital wallet on their phone: a Spring Boot application, pac4j, an SD-JWT VC PID and the Paradym demo wallet, step by step, with the protocol logs." tags: java, security, openid4vp, eidas canonical_url: pac4j.org series: "OpenID4VP / EUDI Wallet / eIDAS 2.0" --- The European Digital Identity wallet is coming, and applications accepting it will need to talk to it. This is the first of two posts showing how to do that with pac4j. Here, the wallet runs on the same phone as the browser; in the second post, it runs on a phone while the browser is on a computer. 1) Why prepare for EUDI wallets? Regulation (EU) 2024/1183, better known as eIDAS 2.0, entered into force on May 20, 2024. EU member states must offer EUDI wallets by the end of 2026, holding person identification data (PID) and attestations such as driving licences or diplomas. Using a wallet remains voluntary. Article 5f also requires acceptance by public services requiring electronic identification, certain private services requiring strong authentication, and very large online platforms requiring authentication. Private-sector obligations have conditions and exemptions, including for microenterprises and small enterprises, with a deadline tied to implementing acts, generally described as late 2027. For applications in scope, wallet authentication is becoming an integration requirement worth preparing for. 2) Why it is hard A wallet login looks simple: the user opens their wallet, sees what your application asks for, consents, and the wallet sends a signed proof. Behind this, the stack is anything but simple. Your application, the verifier in the vocabulary of the specifications, has to deal with: - OpenID for Verifiable Presentations 1.0, the protocol itself: how the wallet is invoked, how it fetches the request, how it posts the response, with its Digital Credentials Query Language (DCQL) to say what you want - the High Assurance Interoperability Profile (HAIP), which pins the options the EUDI wallet uses, and the Architecture and Reference Framework (ARF) of the European Commission, with its PID rulebook - two credential formats: the SD-JWT VC of the IETF, built on selective disclosure JWTs, and the mobile documents (mdoc) of ISO/IEC 18013-5, encoded in CBOR and signed with COSE - the plumbing underneath: signed request objects (RFC 9101), JWS, JWE and JWK, X.509 certificate chains and revocation, decentralized identifiers, and the Digital Credentials API of the browsers for the newest flows. And several ways to identify the verifier, several ways to reach the wallet, several response modes, each with its own validation rules. Implementing this by hand, and keeping it right as the specifications move, is a project in itself. 3) pac4j to the rescue pac4j has one security model for every protocol: a Client starts the authentication, a callback receives the answer, a UserProfile ends up in the session. CAS, SAML, OpenID Connect, OAuth all fit in it, and a digital wallet does too. The new pac4j-openid4vp module, introduced in pac4j v6.6.0, adds the EudiWalletClient: you give it a DCQL query, a keystore and the issuers you trust, and it handles the signed request, the encrypted response, credential verification and the user profile. If you have already used pac4j, the integration will look familiar. This first post walks through a complete example on the same device: the user is on their phone, clicks a link, the wallet opens, and an SD-JWT VC PID comes back. The second post does the same on another device, with a QR code and an mdoc PID. 4) A young protocol: test with demo wallets OpenID4VP 1.0 was finalized in 2025, and wallet implementations are still evolving. Demo wallets let you test the integration while preparing for national EUDI deployments; expect differences between wallets and versions. A very good one is the Paradym wallet by Animo, available for Android and iOS, and its playground which plays both the issuer and the verifier: 1. install the Paradym wallet on your phone 2. open playground.paradym.id on your computer and, on the Issuance page, choose PID SD-JWT, the card this post uses (PID mDoc is the one of the second post): the page shows a QR code, scan it with the Paradym wallet, accept the offer, and the card appears in the wallet 3. keep the Trust Information page of the playground at hand: it shows the certificate of the playground issuer, which your application must trust. The cards are Emma Charlotte van der Berg's, a fictitious Dutch citizen: given name, family name, birth date, address... Issue the card from this playground and no other: a PID issued elsewhere, by the older playground.animo.id for instance, is signed by another certificate and the truststore of section 7 refuses it. If the wallet already holds PID cards from another source, delete them, or at least pick the right one when the wallet asks which card to present. 5) ngrok: a real URL for the wallet The wallet talks to your application over HTTPS from the phone: it fetches the request object and posts its response. Although the browser and wallet run on the same phone, the application runs on your development computer. localhost on the phone points to the phone itself, so it cannot reach that application. The simplest way during development is ngrok, which exposes your local port on a public HTTPS address: ngrok http 8080 It prints a public URL such as https://abcd-1234.ngrok-free.dev: this is the base URL of your application for the rest of this post. The free plan includes a development domain assigned to your account. Copy the URL ngrok prints into the property below. If the browser shows an ngrok warning page, choose Visit Site to continue. 6) Create the Spring Boot project This is a plain Spring Boot web application, with spring-webmvc-pac4j as the pac4j implementation. Java 17 or later and Maven are enough. mkdir -p wallet-same-device/src/main/java/org/pac4j/demo/wallet mkdir -p wallet-same-device/src/main/resources wallet-same-device/metadata cd wallet-same-device The pom.xml: <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>4.0.8</version> <relativePath/> </parent> <groupId>org.pac4j.demo</groupId> <artifactId>wallet-same-device</artifactId> <version>1.0-SNAPSHOT</version> <properties> <java.version>17</java.version> <pac4j.version>6.6.0</pac4j.version> <kotlin.version>2.4.21</kotlin.version> <kotlin-coroutines.version>1.11.0</kotlin-coroutines.version> <kotlin-serialization.version>1.12.0</kotlin-serialization.version> <jackson-2-bom.version>2.22.3</jackson-2-bom.version> <commons-lang3.version>3.21.0</commons-lang3.version> <slf4j.version>2.0.20</slf4j.version> <commons-logging.version>1.3.5</commons-logging.version> <jspecify.version>1.0.0</jspecify.version> </properties> <dependencyManagement> <dependencies> <!-- align pac4j-core and shared dependencies with pac4j-openid4vp --> <dependency> <groupId>org.pac4j</groupId> <artifactId>pac4j-parent</artifactId> <version>${pac4j.version}</version> <type>pom</type> <scope>import</scope> </dependency> <!-- spring-webmvc-pac4j otherwise pulls an older Jakarta EE adapter --> <dependency> <groupId>org.pac4j</groupId> <artifactId>pac4j-jakartaee</artifactId> <version>${pac4j.version}</version> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- the pac4j implementation for Spring MVC --> <dependency> <groupId>org.pac4j</groupId> <artifactId>spring-webmvc-pac4j</artifactId> <version>8.0.4</version> </dependency> <!-- the OpenID4VP support: the application is a verifier --> <dependency> <groupId>org.pac4j</groupId> <artifactId>pac4j-openid4vp</artifactId> <version>${pac4j.version}</version> </dependency> <!-- the SD-JWT VC verification, an optional library of pac4j --> <dependency> <groupId>eu.europa.ec.eudi</groupId> <artifactId>eudi-lib-jvm-sdjwt-kt</artifactId> <version>0.20.2</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project> SD-JWT VC verification relies on the EUDI SD-JWT library, which pac4j declares as optional. This example uses version 0.20.2, matching pac4j. The imported pac4j-parent dependency management aligns pac4j-core and shared libraries. The explicit management entry for pac4j-jakartaee also aligns the adapter pulled in by spring-webmvc-pac4j; setting a pac4j.version property alone would not override that adapter's transitive version. The runtime versions match pac4j's build: Kotlin 2.4.21, Coroutines 1.11.0 and Serialization 1.12.0. Serialization 1.12.0 is available from Maven Central; the same-device demo does not need the walt.id repository. Spring Boot 4.0.8 supplies Spring Framework 7.0.9, matching pac4j; the properties also align Jackson 2, Commons Lang, SLF4J, Commons Logging and JSpecify. Jackson 3 remains managed by Boot for its own web stack. Boot's default Kotlin dependencies would otherwise override pac4j's versions. In particular, Coroutines 1.10.2 lacks BuildersKt.runBlockingK$default, called by the current pac4j adapter, and causes a NoSuchMethodError when the wallet posts its response. To check the resolved versions, run mvn dependency:tree. For this unreleased pac4j version, install the current snapshot locally and run Maven with -Dpac4j.version=6.6.0-SNAPSHOT; keep all pac4j modules on the same version. The src/main/resources/application.properties file holds the base URL and enables the logs we will read at the end: app.base-url=https://abcd-1234.ngrok-free.dev logging.level.org.pac4j.openid4vp=DEBUG logging.level.PROTOCOL_MESSAGE.OPENID4VP=DEBUG And the usual Spring Boot entry point: package org.pac4j.demo.wallet; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class WalletApplication { public static void main(final String[] args) { SpringApplication.run(WalletApplication.class, args); } } 7) Keys and trust Two keystores go in the metadata directory. Run the keytool commands below from the root of the project, next to the pom.xml: the paths are relative to it, like the ones in the configuration. The verifier key. EudiWalletClient signs requests and identifies the verifier by its certificate hash (x509_hash). Production EUDI deployments use a relying party access certificate issued by the ecosystem. This demo uses a self-signed certificate with an EC P-256 key, accepted by the tested Paradym wallet; self-signed verifier certificates are outside the HAIP requirements: keytool -genkeypair -alias rp -keyalg EC -groupname secp256r1 -sigalg SHA256withECDSA \ -dname "CN=pac4j demo verifier,O=pac4j" -validity 365 \ -keystore metadata/verifier.p12 -storetype PKCS12 -storepass changeit The issuers you trust. No credential is accepted unless its issuer is trusted, and pac4j trusts nobody by default. Copy the Issuer Certificate from the Trust Information page of the playground into a paradym-issuer.pem file, and import it into a truststore: keytool -importcert -alias paradym -file paradym-issuer.pem -noprompt \ -keystore metadata/pid-issuers.p12 -storetype PKCS12 -storepass changeit 8) Configure pac4j The configuration is a Pac4jSecurityConfig of spring-webmvc-pac4j: the Config bean defines the client, and the interceptors say which URLs it protects. package org.pac4j.demo.wallet; import org.pac4j.core.config.Config; import org.pac4j.core.config.properties.KeystoreProperties; import org.pac4j.core.http.callback.PathParameterCallbackUrlResolver; import org.pac4j.openid4vp.client.EudiWalletClient; import org.pac4j.openid4vp.config.OpenId4VpConfiguration; import org.pac4j.openid4vp.dcql.EudiPidQuery; import org.pac4j.openid4vp.verifier.SdJwtVcVerifier; import org.pac4j.openid4vp.verifier.trust.CertificateTrustedIssuer; import org.pac4j.springframework.config.Pac4jSecurityConfig; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import java.util.UUID; import static org.pac4j.core.profile.definition.CommonProfileDefinition.FAMILY_NAME; import static org.pac4j.openid4vp.profile.EudiPidProfileDefinition.GIVEN_NAME; @Configuration public class SecurityConfig extends Pac4jSecurityConfig { private static final Logger LOGGER = LoggerFactory.getLogger(SecurityConfig.class); @Value("${app.base-url}") private String baseUrl; @Bean public Config config() { // the request: signed with the key of the verifier, asking for two attributes of the PID as an SD-JWT VC final var configuration = new OpenId4VpConfiguration() .setKeystore(new KeystoreProperties("./metadata/verifier.p12") .setKeyStoreType("PKCS12") .setKeyStoreAlias("rp") .setKeystorePassword("changeit") .setPrivateKeyPassword("changeit")) .setDcqlQuery(EudiPidQuery.sdJwtVc(GIVEN_NAME, FAMILY_NAME)) // Mandatory when using the same browser .setUseRedirectUri(true); // the response: an SD-JWT VC is accepted if its certificate chain ends in the truststore final var issuers = new CertificateTrustedIssuer(new KeystoreProperties("./metadata/pid-issuers.p12") .setKeyStoreType("PKCS12") .setKeystorePassword("changeit")); // certificate revocation checking is disabled to simplify this demo; keep it enabled in production issuers.setCertificateRevocationEnabled(false); // the playground PID references a status list: this demo logs the reference and accepts the credential, // a real status checker must retrieve the list, verify it and check the entry of the credential configuration.addCredentialVerifier(new SdJwtVcVerifier().addTrustedIssuer(issuers) .setStatusChecker(credential -> LOGGER.info("credential status not checked: {}", credential.getClaims().get("status")))); // the demo PID carries no stable identifier: a new profile identifier at each login configuration.setProfileIdResolver(credentials -> UUID.randomUUID().toString()); final var client = new EudiWalletClient(configuration); // keep the client name in the callback path to work around URL decoding in the tested wallet client.setCallbackUrlResolver(new PathParameterCallbackUrlResolver()); return new Config(baseUrl + "/callback", client); } @Override public void addInterceptors(final InterceptorRegistry registry) { addSecurity(registry, "EudiWalletClient").addPathPatterns("/protected/**"); } } A few configuration details: - EudiWalletClient pins the HAIP choices: the request is signed and identifies the verifier by x509_hash, the hash of the certificate found in the keystore, and the response comes back encrypted (direct_post.jwt). These settings are applied by the client; they do not constitute complete HAIP conformance validation. - SdJwtVcVerifier verifies the expected format. With this trust configuration, the wallet's x5c certificate chain must lead to a certificate in your truststore, in addition to passing the signature, validity, disclosure and holder-binding checks. - The profile identifier is yours to choose: EudiWalletClient has no default and refuses to initialize until you configure a resolver. If the PID provider issues a stable, unique and non-reassigned personal_administrative_number, request it in DCQL and use ProfileIdResolver.issuerAndClaim(PERSONAL_ADMINISTRATIVE_NUMBER). That claim is not guaranteed for every PID, and this demo does not ask for it, hence the random identifier: it identifies a login, not a persistent user account. - The callback URL is the regular pac4j one, and the wallet uses it too: it fetches the request object there and posts its response there, without any browser session. - setUseRedirectUri(true) enables the automatic return to the browser. After accepting the wallet response, pac4j returns its callback URL as redirect_uri, with a fresh response_code. The wallet opens that URL in the browser; pac4j checks the code against the transaction of the browser session, saves the profile and redirects to the protected page originally requested. The option defaults to false; this same-device example enables it explicitly. The playground PID carries a status claim, a reference to an OAuth Token Status List: the list where the issuer marks the credentials it has revoked or suspended. pac4j refuses a credential carrying a status unless a status checker is configured, because only the application can decide how to read the list, and it never fetches it on its own. The checker of this demo only logs the reference and accepts the credential, so revocation is not checked here. A real checker retrieves the status list token, verifies its signature with the same trust as the credential and checks the entry at the index of the credential: that deserves a post of its own. See credential status. Disabling certificate revocation does not bypass this check. 9) The DCQL query The query says what your application wants. EudiPidQuery.sdJwtVc(GIVEN_NAME, FAMILY_NAME) builds it in one line for the PID; this is what the wallet receives: { "credentials": [ { "id": "pid", "format": "dc+sd-jwt", "meta": { "vct_values": ["urn:eudi:pid:1"] }, "claims": [ { "path": ["given_name"] }, { "path": ["family_name"] } ] } ] } One credential query, identified as pid, for an SD-JWT VC whose type is the EUDI PID, and the two claims to disclose. The same can be written by hand, in JSON when it comes from a configuration file: configuration.setDcqlQuery(""" {"credentials":[{"id":"pid","format":"dc+sd-jwt","meta":{"vct_values":["urn:eudi:pid:1"]}, "claims":[{"path":["given_name"]},{"path":["family_name"]}]}]}"""); Or in Java, with access to expected values, alternatives (claim_sets, credential_sets) and trusted authorities: configuration.setDcqlQuery(new DcqlQuery() .addCredential(new CredentialQuery("pid", CredentialFormat.SD_JWT_VC) .setVctValues("urn:eudi:pid:1") .addClaim("given_name") .addClaim("family_name"))); Whatever the wallet presents is checked against the query again by pac4j: the format, the type, the claims. The constraints are not left to the wallet. 10) The pages Two pages are enough: a public home page, and a protected page which shows the profile. Both come from a single Spring MVC controller, WalletController, to create next to the two previous classes: package org.pac4j.demo.wallet; import org.pac4j.core.profile.ProfileManager; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.util.HtmlUtils; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ResponseBody; @Controller public class WalletController { @Autowired private ProfileManager profileManager; @GetMapping("/") @ResponseBody public String index() { return "<h1>Home</h1>" + "<p>Start in your phone's default browser, using its regular profile (not private browsing). " + "The wallet must return to the same browser and profile to complete authentication.</p>" + "<p><a href='/protected/index'>Open the protected page: the wallet opens</a></p>"; } @GetMapping("/protected/index") @ResponseBody public String protectedPage() { return "<h1>Protected page</h1><p>" + HtmlUtils.htmlEscape(profileManager.getProfiles().toString()) + "</p>" + "<p><a href='/logout'>Logout</a></p>"; } } The home page needs only the link which starts authentication. The callback is handled by pac4j: with setUseRedirectUri(true), the wallet returns the browser there automatically, then pac4j restores the original request. No second link, polling script or application code relaying the response_code is needed. Run the application, with ngrok running next to it: mvn spring-boot:run 11) The flow, step by step Everything happens on the phone. First open the Paradym wallet and unlock it, then leave it in the background: the flow fails if the wallet is not already running when the browser hands it the openid4vp:// link. Then open the ngrok URL in the phone's default browser, using its regular profile (not private browsing) and click the link on the home page: Open the protected page: the wallet opens. The wallet may open the return URL in the phone's default browser. Starting in another browser, a private browsing session or an embedded browser can therefore lose the original session: the callback cannot complete authentication and the demo returns to the home page. Use the same browser and profile throughout the flow. The six screens below show the home page where login starts, the request to review, the warning about the unverified verifier, the PIN code, the wallet success screen and the return to the protected page in the browser, where the profile is displayed. The steps below follow the exchange through to that final page. !The application home page: open the protected page to start the wallet login !Paradym: review the request !Paradym: organization not verified !Paradym: enter the PIN code !Paradym: success, the information has been shared !Back in the application: the protected page displays Emma Charlotte van der Berg's profile after the automatic wallet return 1. The browser asks for the protected page. pac4j finds no profile, creates a transaction and redirects to an openid4vp:// URL. The browser hands this scheme to the operating system, which opens the wallet. 2. The wallet fetches the request. The URL only carries the identifier of the verifier and a request_uri: the wallet calls it, on the callback URL of your application, and receives the signed request object, with the DCQL query and the public key to encrypt its response. 3. The user consents. The wallet shows who is asking, the hash of your certificate for now, and what is asked: the PID, given name and family name. The user picks the card and confirms with their PIN. 4. The wallet posts its response. An encrypted JWT, containing the SD-JWT VC with the two disclosures and a key binding JWT proving the wallet holds the key of the credential, posted on the callback URL. pac4j decrypts, verifies and keeps the result for the browser session which started the transaction. Its HTTP 200 JSON response contains a redirect_uri pointing to /callback/EudiWalletClient?response_code=.... This is the browser return URL, not the response_uri where the wallet just posted the presentation. 5. The browser returns automatically. The wallet opens the redirect_uri in the default browser. Because the flow started in that browser and profile, its session cookie accompanies the callback. pac4j checks the response_code against that session's pending transaction, retrieves the profile already validated at step 4, consumes the transaction and stores the profile in the session. It then redirects to /protected/index, the URL saved at step 1: the protected page shows Emma Charlotte van der Berg. The code is handled entirely by pac4j and is not carried to the final protected URL. The response_code is a fresh random value required by OpenID4VP section 8.2. The callback requires both this code and the session which started the transaction; a missing or wrong code is refused, another session cannot claim the presentation, and a completed transaction cannot be consumed again. Keep the return in the original browser: a different browser or an isolated webview does not have its session cookie. The second post uses polling instead, because the wallet is on the phone while the browser waiting for the answer is on the computer. 12) What the logs say The two loggers enabled in application.properties give two views of the flow. The PROTOCOL_MESSAGE.OPENID4VP logger prints the messages themselves, as they were sent or received, without any interpretation: one line per message, starting with >>> when the application sent it, <<< when it received it, followed by the other party, the browser or the wallet. The org.pac4j.openid4vp logger tells what pac4j did with them: the transaction created, the request object built, the response decrypted and verified, the profile built. The excerpts below are abbreviated; the final JSON response and browser return illustrate the new setUseRedirectUri(true) configuration. The redirection, step 1: a transaction is created, expiring five minutes later, and the browser is sent to the wallet URL. Note the request_uri, on the callback URL with the client name in its path, and the identifier of the verifier, the hash of its certificate: EudiWalletClient : applying EUDI wallet settings for client EudiWalletClient: X509_HASH, DIRECT_POST_JWT, PID profile definition OpenId4VpConfiguration : OpenID4VP configuration checked: 1 credential queries, verifier formats=[SD_JWT_VC, MSO_MDOC], signed requests=true VpTransaction : transaction 51fef2…: - -> CREATED (client EudiWalletClient, response mode direct_post.jwt, expires at 2026-10-12T09:19:39Z) PROTOCOL_MESSAGE.OPENID4VP : [tx 51fef2…] >>> browser wallet URL: openid4vp://?client_id=x509_hash%3ApntH00p6r9h1QcHOvJAVv90dkHvm5Jhx_GBAKcJH5CE&request_uri=https%3A%2F%2Fabcd-1234.ngrok-free.dev%2Fcallback%2FEudiWalletClient%3Fvp_tx%3D51fef2…&request_uri_method=post The first two lines appear once, at the first request: the client initializes lazily. The configuration comes with a default verifier per format; section 8 replaced the SD-JWT VC one with the trusted issuers and the mdoc one stays, hence the two formats listed. Step 2: the wallet fetches the request object, which is built for it and signed: PROTOCOL_MESSAGE.OPENID4VP : [tx 51fef2…] <<< wallet GET vp_tx=51fef2… OpenId4VpRequestObjectBuilder : client metadata built for transaction 51fef2…: formats=[dc+sd-jwt, mso_mdoc], response encryption methods=[A128GCM, A256GCM] OpenId4VpRequestObjectBuilder : request object signed for transaction 51fef2… with ES256 VpTransaction : transaction 51fef2…: CREATED -> REQUEST_RETRIEVED (GET of the request URI) PROTOCOL_MESSAGE.OPENID4VP : [tx 51fef2…] >>> wallet 200 eyJ4NWMiOlsiTUlJQmNq… These demo traces show a GET even though the wallet URL advertises request_uri_method=post. pac4j accepts both methods; a wallet following the advertised method uses POST and can include its metadata and a wallet nonce. The request object is a JWT whose header carries your certificate (x5c, with the rp alias as kid) and whose payload is the request. Decoded, its payload reads: { "client_id": "x509_hash:pntH00p6r9h1QcHOvJAVv90dkHvm5Jhx_GBAKcJH5CE", "response_type": "vp_token", "response_mode": "direct_post.jwt", "response_uri": "https://abcd-1234.ngrok-free.dev/callback/EudiWalletClient?vp_tx=51fef2…", "nonce": "281fec06efb9492b95b45c09a537e624", "state": "7b99f8b2372b42d6b819942add896494…", "dcql_query": { "credentials": [ { "id": "pid", "format": "dc+sd-jwt", "meta": { "vct_values": ["urn:eudi:pid:1"] }, "claims": [ { "path": ["given_name"] }, { "path": ["family_name"] } ] } ] }, "client_metadata": { "jwks": { "keys": [ { "kty": "EC", "use": "enc", "crv": "P-256", "alg": "ECDH-ES", "kid": "2f75d54c…", "x": "…", "y": "…" } ] }, "encrypted_response_enc_values_supported": ["A128GCM", "A256GCM"], "vp_formats_supported": { "dc+sd-jwt": { "sd-jwt_alg_values": ["ES256"], "kb-jwt_alg_values": ["ES256"] }, "mso_mdoc": { "issuerauth_alg_values": [-7], "deviceauth_alg_values": [-7] } } }, "aud": "https://self-issued.me/v2", "iat": 1791796479, "exp": 1791796779 } Step 4: the wallet posts its response, a JWE encrypted with the ephemeral key published in the request. pac4j decrypts it, verifies the SD-JWT VC, its chain, its disclosures and its key binding, hands the status reference to the checker of section 8, and builds the profile: PROTOCOL_MESSAGE.OPENID4VP : [tx 51fef2…] <<< wallet POST vp_tx=51fef2…&response=eyJraWQiOiIyZjc1ZDU0Yy… OpenId4VpAuthenticator : response decrypted and authentication tag verified for transaction 51fef2…: alg=ECDH-ES, enc=A128GCM, members=[vp_token, state] CertificateTrustedIssuer : issuer truststore loaded: 1 trust anchors CertificateTrustedIssuer : certificate revocation lists loaded: 0 SecurityConfig : credential status not checked: {status_list={idx=18, uri=https://metadata.paradym.id/7f22b55a…/status-lists/86852c16…}} SdJwtVcVerifier : SD-JWT VC verified for transaction 51fef2…: holder binding=true OpenId4VpAuthenticator : presentation 1/1 verified for transaction 51fef2…, query pid: format=SD_JWT_VC, verifier=SdJwtVcVerifier OpenId4VpAuthenticator : wallet response validated for transaction 51fef2…: 1 credential queries answered EudiPidProfile : adding => key: issuing_country / value: NL / class java.lang.String EudiPidProfile : adding => key: vct / value: urn:eudi:pid:1 / class java.lang.String EudiPidProfile : adding => key: status / value: {status_list={idx=18, uri=https://metadata.paradym.id/7f22b55a…/status-lists/86852c16…}} / class … EudiPidProfile : adding => key: family_name / value: van der Berg / class java.lang.String EudiPidProfile : adding => key: given_name / value: Emma Charlotte / class java.lang.String OpenId4VpAuthenticator : profile built for transaction 51fef2…: profile class=EudiPidProfile, definition=EudiPidProfileDefinition VpTransaction : transaction 51fef2…: REQUEST_RETRIEVED -> RESPONSE_RECEIVED (presentation validated) PROTOCOL_MESSAGE.OPENID4VP : [tx 51fef2…] >>> wallet 200 {"redirect_uri":"https://abcd-1234.ngrok-free.dev/callback/EudiWalletClient?response_code=8c731e…"} The profile gets more than the two requested claims: the issuer also signs, outside any disclosure, the vct, the issuing country and authority, the validity dates, the holder key (cnf) and the status reference, and the wallet cannot withhold them. The two names are the only disclosed claims. Step 5: the wallet opens the returned callback URL in the browser. pac4j checks the response_code using the transaction identifier held in the browser session; the return URL carries no vp_tx. It then picks up the validated profile, without verifying it twice, and the regular callback redirects to the saved /protected/index URL: VpTransaction : transaction 51fef2…: RESPONSE_RECEIVED -> CONSUMED (the browser came back) OpenId4VpAuthenticator : transaction 51fef2…: reusing the profile validated when the wallet posted its response, nothing is verified again Note that the logs show the credentials as they travel, tokens and the return response_code included: enable them to diagnose, not permanently in production. 13) Limitations and what comes next A few things to know before going further: - The return needs the original browser session. setUseRedirectUri(true) implements the OpenID4VP redirect, but the wallet must open it in the browser that started the login. Opening another browser or an isolated webview does not transfer that session. - A refusal was silent with the tested Paradym wallet: when the user declined or had no matching card, nothing was posted, and the transaction expired after five minutes. Wallets that post an error let pac4j report it to the browser. - Certificate revocation is disabled in this demo. In production, keep it enabled and supply current CRLs or configure the Java PKIX provider to retrieve revocation data. This setting does not disable credential status checking: credentials carrying status information still need a status checker, and the one of this demo only logs the status reference, as seen in section 8. - Transaction data, the verifier_attestation and openid_federation prefixes, and the automatic retrieval of status lists are not supported yet. This integration is introduced in pac4j v6.6.0. The full documentation starts at OpenID for Verifiable Presentations, and the second post takes the wallet to another device, with a QR code and an mdoc PID.

  46. X@nemamdumi · 1 like · 41 views

    @ramazanbicer1 Pffff some one going back to earth? aliceisback/NeedleSurf-v3

  47. X@yiyirats · 150 views

    一款能把 PDF 扫描件转成 EPUB 电子书的工具:PDF craft。 它专攻扫描版书籍的处理,正文提取、页眉页脚过滤、跨页衔接处理都在行,还能把 PDF 转成 Markdown 或带结构的 EPUB 电子书。 GitHub:oomol-lab/pdf-craft t.co

Older posts →