From 43f61d4e48cda945b65414621351e0369b2b1cc4 Mon Sep 17 00:00:00 2001 From: Derek Scruggs Date: Fri, 26 Jun 2026 14:07:26 -0500 Subject: [PATCH 1/4] Reorder README to lead with interact(). Promote interact() to the recommended relying-party entry point, directly after polyfill loading, and move get()/store() into a clearly deprecated section below it. Explain that an interaction URL covers credential request, storage, or both -- the operation is deferred to the exchange layer and is never expressed to or via CHAPI -- so callers no longer make the get/store distinction. Documentation only; the code does not yet mark get()/store() deprecated. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 129 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 72 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index 392ba10..8f92a66 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,78 @@ await polyfill.loadOnce(); console.log('Ready to work with credentials!'); ```` -### Requesting and Storing Credentials +### Requesting and Storing Credentials with `interact()` + +`interact()` is the recommended entry point for relying parties (issuers and +verifiers). A coordinator website hands the polyfill a single **interaction +URL** and the user's selected credential handler drives the rest. The +interaction can perform a credential request, a credential store, or both — +that choice is deferred to the exchange layer behind the interaction URL and is +never expressed to or via CHAPI. As a result, apps using `interact()` no longer +need to make the `get()` / `store()` distinction themselves; a single call +covers all combinations. + +> **Note:** `interact()` is a new, simplified entry point. See the +> [design spec](docs/specs/interact-api.md). The method name and return shape +> may still change. + +The coordinator does not compose a full `web` request object itself. Under the +hood, `interact()` translates the interaction URL into a single +`navigator.credentials.get()` flow, carrying the URL in the `protocols` map +under the well-known `interact` key. Whether the exchange behind that URL ends +up requesting credentials, storing them, or both is opaque to CHAPI. + +`interact()` lives on the `chapi` object. The polyfill does **not** attach it to +`navigator`. By default (`setGlobal: true`), `load()`/`loadOnce()` set +`globalThis.chapi` for you, so you can just call `globalThis.chapi.interact(...)` +after loading. + +If you'd rather control where the API lives, pass `setGlobal: false` — the +polyfill then attaches nothing to the global environment and only returns the +polyfill, so you place `chapi` yourself: + +```js +try { + globalThis.chapi = (await loadOnce({setGlobal: false})).chapi; +} catch(e) { + console.log('CHAPI failed to load.'); +} +``` + +Then call it: + +```js +await chapi.interact({ + // required: an `https:` URL the coordinator already trusts; the polyfill + // treats it as opaque (it does not fetch, parse, or encode it). The full + // "protocols" object is fetched from this URL over TLS by the selected + // handler, which authenticates its source and supports disconnected + // systems (e.g. QR-code readers). + interactionUrl: 'https://coordinator.example/exchanges/z1A2b3C4', + // optional: an AbortSignal to cancel the interaction + signal, + // optional: credential handler origins to recommend to the user + recommendedHandlerOrigins: ['https://wallet.example.chapi.io'] +}); +``` + +The returned promise: + +- **resolves** to an empty object (`{}`) when the interaction completes; no + credential data is returned to the coordinator (data minimization), +- **rejects** with a `DOMException` named `AbortError` when the user cancels or + the caller aborts via `signal`, +- otherwise rejects with the same errors surfaced by + [`get()`](#get) (e.g. `SecurityError` outside a secure context). + +### Deprecated: `get()` and `store()` + +> **⚠️ Deprecated.** Relying parties should use +> [`interact()`](#requesting-and-storing-credentials-with-interact) instead. A +> single interaction URL covers credential request, storage, or both (the +> operation is deferred to the exchange layer), so the `get()` / `store()` +> distinction is no longer needed. These lower-level entry points remain +> available for now and are documented here for existing integrations. A web application can `get()` and `store()` credentials without knowing anything about the user's wallet. This is intentional; for privacy reasons, the client @@ -160,62 +231,6 @@ if(!result) { } ``` -#### `interact()` - -> **Note:** `interact()` is a new, simplified entry point. See the -> [design spec](docs/specs/interact-api.md). The method name and return shape -> may still change. - -A coordinator website (a Relying Party such as an issuer or verifier) can start -a credential interaction by handing the polyfill a single **interaction URL**, -without composing a full `web` request object itself. `interact()` always -generates a credential _request_; under the hood it translates into the same -`navigator.credentials.get()` flow, carrying the URL in the `protocols` map -under the well-known `interact` key. - -`interact()` lives on the `chapi` object. The polyfill does **not** attach it to -`navigator`. By default (`setGlobal: true`), `load()`/`loadOnce()` set -`globalThis.chapi` for you, so you can just call `globalThis.chapi.interact(...)` -after loading. - -If you'd rather control where the API lives, pass `setGlobal: false` — the -polyfill then attaches nothing to the global environment and only returns the -polyfill, so you place `chapi` yourself: - -```js -try { - globalThis.chapi = (await loadOnce({setGlobal: false})).chapi; -} catch(e) { - console.log('CHAPI failed to load.'); -} -``` - -Then call it: - -```js -await chapi.interact({ - // required: an `https:` URL the coordinator already trusts; the polyfill - // treats it as opaque (it does not fetch, parse, or encode it). The full - // "protocols" object is fetched from this URL over TLS by the selected - // handler, which authenticates its source and supports disconnected - // systems (e.g. QR-code readers). - interactionUrl: 'https://coordinator.example/exchanges/z1A2b3C4', - // optional: an AbortSignal to cancel the interaction - signal, - // optional: credential handler origins to recommend to the user - recommendedHandlerOrigins: ['https://wallet.example.chapi.io'] -}); -``` - -The returned promise: - -- **resolves** to an empty object (`{}`) when the interaction completes; no - credential data is returned to the coordinator (data minimization), -- **rejects** with a `DOMException` named `AbortError` when the user cancels or - the caller aborts via `signal`, -- otherwise rejects with the same errors surfaced by - [`get()`](#get) (e.g. `SecurityError` outside a secure context). - #### WebCredential TODO: Discuss creating and receiving WebCredential instances From f26eabac3eb2b6baf79b60a98ecff38055dcdded Mon Sep 17 00:00:00 2001 From: Derek Scruggs Date: Fri, 26 Jun 2026 14:36:20 -0500 Subject: [PATCH 2/4] Apply review feedback to interact() README section. Wrap loadOnce() examples in try/catch, add a setGlobal: false load example, tighten the interact() intro wording, and expand the interactionUrl comment to explain trusted-origin and TLS-authentication behavior. Addresses review comments on PR #58. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 65 +++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 44 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 8f92a66..f1902fd 100644 --- a/README.md +++ b/README.md @@ -58,9 +58,13 @@ access to the `navigator.credentials` and `credentialHandlerPolyfill` globals. ```js const polyfill = window.credentialHandlerPolyfill; -// must be run from an async function if top-level await is unavailable -await polyfill.loadOnce(); -console.log('Ready to work with credentials!'); +try { + // must be run from an async function if top-level await is unavailable + await polyfill.loadOnce(); + console.log('Ready to work with credentials!'); +} catch(e) { + console.error('Failed to load CHAPI.', e); +} ``` Otherwise (if you're developing on Node.js and using Webpack, for example), @@ -68,21 +72,38 @@ import it in the usual manner: ```js import * as polyfill from 'credential-handler-polyfill'; -// must be run from an async function if top-level await is unavailable -await polyfill.loadOnce(); -console.log('Ready to work with credentials!'); -```` +try { + // must be run from an async function if top-level await is unavailable + await polyfill.loadOnce(); + console.log('Ready to work with credentials!'); +} catch(e) { + console.error('CHAPI failed to load.', e); +} +``` + +If you would prefer to not add any global accessors to the API, you can load it +this way: + +```js +import * as polyfill from 'credential-handler-polyfill'; +try { + const api = await polyfill.loadOnce({setGlobal: false}); + // do something, like call `api.chapi.interact(...)` +} catch(e) { + console.error('CHAPI failed to load.', e); +} +``` ### Requesting and Storing Credentials with `interact()` -`interact()` is the recommended entry point for relying parties (issuers and -verifiers). A coordinator website hands the polyfill a single **interaction -URL** and the user's selected credential handler drives the rest. The -interaction can perform a credential request, a credential store, or both — -that choice is deferred to the exchange layer behind the interaction URL and is -never expressed to or via CHAPI. As a result, apps using `interact()` no longer -need to make the `get()` / `store()` distinction themselves; a single call -covers all combinations. +`interact()` is the recommended entry point for relying parties (issuer and +verifier coordinator websites). A coordinator website hands this API a +single **interaction URL** and the user's selected credential handler (e.g., +digital wallet) drives the rest. The interaction can perform a credential +request, a credential store, or both; the choice is deferred to the exchange +layer behind the interaction URL and is never expressed to or via CHAPI. As +a result, apps using `interact()` no longer need to make the `get()` / `store()` +distinction themselves; a single call covers all combinations. > **Note:** `interact()` is a new, simplified entry point. See the > [design spec](docs/specs/interact-api.md). The method name and return shape @@ -107,7 +128,7 @@ polyfill, so you place `chapi` yourself: try { globalThis.chapi = (await loadOnce({setGlobal: false})).chapi; } catch(e) { - console.log('CHAPI failed to load.'); + console.log('CHAPI failed to load.', e); } ``` @@ -115,11 +136,13 @@ Then call it: ```js await chapi.interact({ - // required: an `https:` URL the coordinator already trusts; the polyfill - // treats it as opaque (it does not fetch, parse, or encode it). The full - // "protocols" object is fetched from this URL over TLS by the selected - // handler, which authenticates its source and supports disconnected - // systems (e.g. QR-code readers). + // required: an `https:` URL the coordinator already trusts, from its + // own origin or another origin they expect the end user to trust; + // CHAPI treats it as opaque (it does not fetch, parse, or encode it). The + // full "protocols" object is fetched from this URL over TLS by the + // user-selected credential handler (e.g., a digital wallet), enabling + // TLS-authentication of its source, even if the URL is delivered via + // a disconnected system (e.g., via QR code). interactionUrl: 'https://coordinator.example/exchanges/z1A2b3C4', // optional: an AbortSignal to cancel the interaction signal, From d25d348e33367de47a15baec9d23c3e198448128 Mon Sep 17 00:00:00 2001 From: Dave Longley Date: Fri, 26 Jun 2026 15:43:44 -0400 Subject: [PATCH 3/4] Clean up bullet point in README. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f1902fd..52a15d8 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Credential Handler API (CHAPI) is: * a browser API * lets web apps securely `get()` and `store()` credentials, without - having to roll their own wallet infrastructure +* lets web apps securely exchange credentials with digital wallets * provides a secure _trusted UI_ for users to manage those credentials * gives users ability to choose service providers for wallets From f42e1fdb86be4b549187f472f36ad8c68589cff8 Mon Sep 17 00:00:00 2001 From: Dave Longley Date: Fri, 26 Jun 2026 15:44:41 -0400 Subject: [PATCH 4/4] Remove erroneous bullet point from README. --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 52a15d8..08a8eb5 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,6 @@ See the [feature videos](#features) for more animations of CHAPI in action. Credential Handler API (CHAPI) is: * a browser API -* lets web apps securely `get()` and `store()` credentials, without * lets web apps securely exchange credentials with digital wallets * provides a secure _trusted UI_ for users to manage those credentials * gives users ability to choose service providers for wallets