Skip to content

feat: W3C Digital Credentials API support via @sirosfoundation/dc-api - #504

Merged
masv3971 merged 7 commits into
SUNET:mainfrom
sirosfoundation:feat/dc-api-polyfill
Jul 29, 2026
Merged

feat: W3C Digital Credentials API support via @sirosfoundation/dc-api#504
masv3971 merged 7 commits into
SUNET:mainfrom
sirosfoundation:feat/dc-api-polyfill

Conversation

@leifj

@leifj leifj commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Adds W3C Digital Credentials API support to the verifier frontend using the @sirosfoundation/dc-api library.

When the browser supports the DC API, credential presentation happens natively. Otherwise the existing QR/redirect/SSE flow is used unchanged.

New files

  • dc-api.js — vendored ESM bundle of @sirosfoundation/dc-api (protocol constants, detection, native invocation, error helpers)
  • dc-api-polyfill.js — verifier integration layer: imports from the library, adds same-device redirect + cross-device SSE/poll fallbacks

Modified files

  • presentation-definition.html — importmap entries for library + polyfill; accessibility fixes (label/id for select, aria-label for toggle)
  • presentation-definition.js — tries DC API first via polyfill, falls back to QR/wallet links + SSE; refactored for reduced cognitive complexity
  • authorize_enhanced.html — importmap for the library; uses polyfill for credential requests

…-api

Integrates the @sirosfoundation/dc-api library for DC API protocol
constants, feature detection, and native invocation. The verifier
polyfill adds transport fallbacks (redirect, QR, SSE/poll).

New files:
- dc-api.js: vendored bundle of @sirosfoundation/dc-api (3.2 kB)
- dc-api-polyfill.js: verifier integration importing the library,
  adding same-device redirect + cross-device SSE/poll fallbacks

Updated:
- presentation-definition.html/js: tries DC API first, falls back to QR
- authorize_enhanced.html: uses polyfill for credential requests

Detection uses W3C-specified feature checks:
- typeof DigitalCredential !== 'undefined'
- DigitalCredential.userAgentAllowsProtocol(protocol)

Ref: W3C Digital Credentials API spec, CS-07
Dep: https://github.com/sirosfoundation/dc-api
@leifj
leifj force-pushed the feat/dc-api-polyfill branch from ac37cdc to 1488b74 Compare July 5, 2026 10:21
- dc-api.js: rebuild bundle with es2022 target, var→const
- dc-api-polyfill.js: use export...from, globalThis, RegExp.exec(),
  optional chaining, move SSE error handler to outer scope
- presentation-definition.html: add id/label for select (a11y),
  add aria-label for group toggle checkbox
- presentation-definition.js: remove unused imports, extract
  _tryNativeDCAPI() and _setupFallbackFlow() to reduce cognitive
  complexity, use optional chaining + globalThis
@leifj leifj changed the title feat: W3C Digital Credentials API polyfill for OpenID4VP feat: W3C Digital Credentials API support via @sirosfoundation/dc-api Jul 5, 2026
Adds newly exported: OID4VP_ALL_PROTOCOLS, OID4VP_SPEC_PROTOCOLS, isOID4VPProtocol

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds W3C Digital Credentials API (DC API) support in the verifier UI with a native-first flow (via a vendored @sirosfoundation/dc-api bundle + a verifier-specific polyfill), and introduces a new apigw datastore capability to generate pre-authorized OpenID4VCI credential offers tied to datastore documents.

Changes:

  • Add DC API integration + polyfill fallbacks in verifier static UI (importmaps, native invocation, QR/SSE/poll fallbacks).
  • Add apigw datastore pre-authorized offer endpoint (POST /api/v1/datastore/preauth_offer) and wire issuance to a new datastore auth provider.
  • Update Swagger artifacts + swagger generation inputs to include newly referenced packages/types.

Reviewed changes

Copilot reviewed 17 out of 18 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
pkg/model/auth_providers.go Adds AuthProviderDatastore constant.
Makefile Updates apigw swagger generation dirs (adds pkg/openid4vp).
internal/verifier/staticembed/presentation-definition.js Attempts native DC API first; falls back to existing QR/link/SSE flow.
internal/verifier/staticembed/presentation-definition.html Adds importmap entries; minor accessibility fixes.
internal/verifier/staticembed/dc-api.js Vendored ESM bundle for DC API helper library.
internal/verifier/staticembed/dc-api-polyfill.js New verifier-oriented DC API wrapper + redirect/SSE/poll fallback transport.
internal/verifier/staticembed/authorize_enhanced.html Switches verifier UI to use the new polyfill and vendored bundle.
internal/apigw/httpserver/service.go Registers new datastore endpoint /preauth_offer.
internal/apigw/httpserver/endpoints_datastore.go Adds httpserver handler for datastore pre-auth offer.
internal/apigw/httpserver/endpoints_datastore_test.go Extends mock apiv1 interface to compile with new method.
internal/apigw/httpserver/api.go Extends Apiv1 interface with DatastorePreAuthOffer.
internal/apigw/apiv1/handlers_issuer.go Allows datastore data source to omit identifier; treats datastore as session-based provider for doc lookup.
internal/apigw/apiv1/handlers_issuer_assertion_test.go Updates expectation: datastore allows empty identifier.
internal/apigw/apiv1/handlers_datastore.go Implements DatastorePreAuthOffer (pre-auth code + cached docs + offer URL).
internal/apigw/apiv1/handlers_datastore_test.go Adds tests for DatastorePreAuthOffer behavior and persistence.
docs/apigw/swagger.yaml Documents /api/v1/datastore/preauth_offer and request/reply schemas.
docs/apigw/swagger.json Same as above (JSON swagger).
docs/apigw/docs.go Same as above (embedded swagger template).
Files not reviewed (1)
  • docs/apigw/docs.go: Generated file

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/verifier/staticembed/dc-api-polyfill.js
Comment thread internal/verifier/staticembed/authorize_enhanced.html
Comment thread internal/apigw/httpserver/endpoints_datastore.go
Comment thread internal/apigw/httpserver/service.go

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 17 out of 18 changed files in this pull request and generated 4 comments.

Files not reviewed (1)
  • docs/apigw/docs.go: Generated file

Comment thread internal/verifier/staticembed/presentation-definition.js
Comment thread internal/verifier/staticembed/authorize_enhanced.html
Comment thread internal/apigw/apiv1/handlers_datastore.go Outdated
Comment thread internal/verifier/staticembed/dc-api-polyfill.js
The comment incorrectly referenced 'openid4vp-v1-unsigned' as the example
protocol check. The library actually prefers signed requests via
getBestProtocol() (signed > multisigned > unsigned).
@s-jairl

s-jairl commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Two comments:

  • Due to the current state of DC-API, we probably want configuration toggle to enable/disable the functionality
  • When a user presses "cancel" on the DC-API dialog ("Do you trust this site with your data?"), it doesn't always present the "traditional" page with wallet selection - I've observed this in both Chromium on Linux (150.0.7871.128) and Chromium/Vanadium on Android (151.0.7922.47.1) - it just get stuck with an endless loading spinner.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 17 out of 18 changed files in this pull request and generated no new comments.

Files not reviewed (1)
  • docs/apigw/docs.go: Generated file
Comments suppressed due to low confidence (2)

internal/verifier/staticembed/presentation-definition.js:498

  • requestCredential() is being called with presentationDefinition.authorization_request, which is an openid4vp://... authorization-request URI (client_id + request_uri). The DC API expects the OpenID4VP request data (e.g., { request_uri, client_id }) or a signed request JWT, so passing the full URI string will be treated as a JWT and the native DC API call will fail, making the “native” path effectively non-functional.
            const result = await requestCredential(
                this.presentationDefinition.authorization_request,
                { signal: abortController.signal },
            );

internal/verifier/staticembed/presentation-definition.js:493

  • this._dcAbort is assigned but never used anywhere in this module, so it becomes dead state and can confuse future maintenance. If cancellation is not implemented, keep the AbortController local (or wire _dcAbort.abort() into an existing UI cancel action).
            const abortController = new AbortController();
            this._dcAbort = abortController;

Addresses s-jairl's two comments on the PR:

- The DCQL playground UI (presentation-definition.js) always attempted
  the native W3C Digital Credentials API regardless of server config.
  Thread it through the existing verifier.digital_credentials.enable
  flag (already respected by authorize_enhanced.html) via a new
  dc_api_enabled field on /ui/metadata, instead of adding a duplicate
  toggle.

- When the user cancelled/declined the native DC API picker, the
  native call rejects with NotAllowedError. dc-api-polyfill.js's
  requestCredential() silently swallowed that and fell through to its
  own cross-device wait (SSE/poll, up to timeoutMs = 5min by default),
  even though both callers (presentation-definition.js,
  authorize_enhanced.html) already have their own immediate QR/wallet-
  link fallback UI. This left the page stuck on a loading spinner
  after cancelling, on both desktop Chromium and Android Chromium/
  Vanadium. Let native failures propagate directly to callers instead;
  the polyfill fallback path is now only reached when native DC API
  isn't available/usable at all, not as a retry after a completed
  native attempt.
@leifj

leifj commented Jul 28, 2026

Copy link
Copy Markdown
Contributor Author

Addressed both points (commit 123241c):

  1. Config toggle: the DCQL playground UI (presentation-definition.js) was always attempting the native DC API regardless of server config. Wired it up to the existing verifier.digital_credentials.enable flag (already respected by authorize_enhanced.html) rather than adding a new duplicate toggle — /ui/metadata now returns dc_api_enabled, and the playground only tries native DC API when it's true.

  2. Endless spinner on cancel: root cause was in dc-api-polyfill.js. When the user declines the native picker, the browser rejects with NotAllowedError — the polyfill's requestCredential() silently caught that and fell through to its own cross-device wait (SSE/poll, default 5 min timeout), even though both call sites already have their own immediate QR/wallet-link fallback UI that's designed to run on any rejection. So on cancel, the page got stuck waiting on an SSE listener that was never going to fire, instead of showing the fallback UI right away. Fixed by letting native failures (including cancellation) propagate straight to the callers, which already handle it correctly. This should fix it on both Chromium/Linux and Chromium/Vanadium/Android, since neither platform-specific code path is involved — it was a plain control-flow bug.

go build ./... and go test ./... (verifier packages) pass; added a config-propagation test case for dc_api_enabled. No existing JS test harness covers internal/verifier/staticembed, so this couldn't be covered by an automated JS test — recommend manually re-testing the cancel flow on both platforms you originally observed the bug on.

@s-jairl

s-jairl commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Perhaps you want to change the settings group ".verifier.digital_credentials" to something like ".verifier.dcapi" instead? While I guess it is correct, "digital credentials" is a slightly wider term.

While you're at it, consider running the documentation update script.

@leifj

leifj commented Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

Re-based onto current main and resolved the conflict (a small JS conflict — my `dcApiEnabled` field vs. a concurrent JSDoc type update, kept both). Ran `make gen-config-docs` per your comment (commit fce1aa5) — the diff includes some unrelated already-existing config that had drifted out of sync too (Kafka SASL/mTLS, rate limiting), not just this PR's changes.

On the `.verifier.digital_credentials` → `.verifier.dcapi` rename: I didn't make this change. digital_credentials predates this PR and is already merged into main (it backs the existing authorize_enhanced.html flow), so renaming it now would be a breaking config-key change for anything already deployed against it, not something scoped to this PR. Agree it's a slightly wide name for what it configures, but wanted to flag the blast radius before anyone decides — happy to do the rename as a separate, explicit PR if that's still wanted.

go build ./... and go test ./... pass repo-wide.

@sonarqubecloud

Copy link
Copy Markdown

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (3)

internal/verifier/staticembed/presentation-definition.js:506

  • requestCredential() is being called with presentationDefinition.authorization_request, which (per CreateAuthorizationRequestURI) is an openid4vp://... authorization-request URI containing client_id + request_uri. The dc-api library expects the OpenID4VP request data (e.g. {client_id, request_uri} or a signed request JWT), not the full URI string; passing the URI as request will likely make the native DC API invocation fail and always fall back.
            const result = await requestCredential(
                this.presentationDefinition.authorization_request,
                { signal: abortController.signal },
            );

internal/verifier/staticembed/presentation-definition.js:517

  • The catch (err) block assumes err always has .name and .message. If a non-Error is thrown (e.g. a string or arbitrary object), the logging line can itself throw and prevent the QR/links fallback from being set up.
        } catch (err) {
            if (err.name === 'AbortError') return true;
            console.log("DC API not available or failed, falling back to QR/links:", err.message);
            return false;

internal/verifier/staticembed/dc-api-polyfill.js:319

  • _waitViaPoll() registers an abort listener but doesn't remove it on the non-abort exit paths (success, failure, timeout). If requestCredential() is called multiple times, this can accumulate listeners and cause extra rejections on later aborts.
            if (Date.now() > deadline) {
                reject(new DOMException('Timeout waiting for wallet response', 'AbortError'));
                return;
            }

@masv3971
masv3971 merged commit f3b2864 into SUNET:main Jul 29, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants