diff --git a/README.md b/README.md index 392ba10..08a8eb5 100644 --- a/README.md +++ b/README.md @@ -30,8 +30,7 @@ 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 - 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 @@ -58,9 +57,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,12 +71,102 @@ 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); +} +``` -### Requesting and Storing Credentials +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 (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 +> 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.', e); +} +``` + +Then call it: + +```js +await chapi.interact({ + // 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, + // 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 +253,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