Add simplified interact() API.#57
Conversation
Draft technical spec for a single-call CHAPI entry point that lets relying parties start an interaction from an interactionUrl, without composing the full web request object. Translates into the existing get() flow via the protocols mechanism, so no mediator changes are required for v1. Exposes the API both at navigator.chapi and via a standalone factory export. Open questions are left open to be resolved during draft-PR review. Addresses #50.
|
Let's leave |
Per review feedback on the draft PR, interact() always generates a credential request; the type parameter (and the deferred 'store' wiring) is removed. A store-style flow can be added later without changing the method signature. Renumbers the open questions left behind. Addresses #50.
|
Done — pushed a commit dropping Follow-up that this raises (open question 3 in the spec): which
Either way this is purely additive to the polyfill — I lean toward (2) to keep the polyfill a dumb broker, but do you have a target protocol in mind? That decides the request shape before I implement. |
|
It needs to use |
|
Got it — using |
Per review on the draft PR, carry interactionUrl in the protocols map
under the well-known `interact` meta-protocol key:
`{web: {protocols: {interact: interactionUrl}}}`. Any underlying
exchange protocol stays hidden behind the URL, so no protocol param is
needed; the polyfill treats interactionUrl as opaque (encoding is the
caller's concern). Closes the mapping-key open question and renumbers
the rest.
Addresses #50.
Explain why interact() sends a single-key `interact` protocols object instead of an inline multi-key protocols object: the interaction URL is a layer of indirection that lets the consuming side fetch the full protocols object over TLS, providing source authentication and support for disconnected systems (e.g. QR-code readers) by reusing existing TLS infrastructure. Note that the single-key form is expected to supersede the multi-key form. Addresses #50.
Add a single-call CHAPI entry point that starts a credential interaction from an opaque interaction URL, without the caller composing a full web request. interact() always generates a request, translating to the existing get() flow with the URL carried in the protocols map under the well-known `interact` meta-protocol key. Split into a pure functional core (createInteractRequest, independently unit-tested) and an imperative shell (interact(), built around an injected credentials container and secure-context assertion). The shell resolves to an empty object on completion and rejects with AbortError on user cancel or signal abort. Expose interact() both on the object returned by load()/loadOnce() and as navigator.chapi. Add node:test unit tests for the core and shell, a Playwright smoke test for the wiring, a navigator.chapi assertion to the existing load smoke test, and a README usage section. Addresses #50. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a CHANGELOG entry for the interact() API and its Node unit tests.
Update the spec status from Draft to In review and annotate the open
questions with what the initial implementation currently does: resolves
to {}, abandons (does not tear down) the in-flight RPC on signal abort,
and maps a null get() result (no credential selected) to AbortError.
These decisions remain open for review.
Addresses #50.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Stop attaching the interact() API to navigator. Add an `install` option to load()/loadOnce() (default true, preserving global-install behavior): when true it now also sets globalThis.chapi; when false the polyfill attaches nothing globally and only returns the polyfill, letting callers place the API themselves. Recommend `globalThis.chapi = (await loadOnce()).chapi` in the docs. Use signal.throwIfAborted() for the already-aborted pre-check so the signal's own reason surfaces; keep the mid-flight abort race. Improve interactRequest validation: attach error.cause and a clearer message on URL parse failure, distinguish the wrong-protocol message, and require recommendedHandlerOrigins to be an array of URL strings. Update the spec, README, CHANGELOG, and tests accordingly. Addresses #50. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Pushed
Spec, README, and CHANGELOG updated to match. Tests: 21 node + 20 browser (1 pre-existing WebKit skip), lint clean. Added coverage for Ready for another look. |
The recommended snippet reassigned globalThis.chapi to the value loadOnce() had already set under the default install:true, which was a no-op that contradicted the surrounding "control where it lives" prose. Show the default (zero-config global) and the explicit install:false path as the two real choices, and move the try/catch attach example under install:false where the caller's assignment is what places chapi. Addresses #50. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Restructure load() option handling per review: a string remains the legacy mediator-URL form; otherwise options must be an object, and each option (mediatorOrigin, install) is validated independently. This fixes a case where a non-string mediatorOrigin could fall through without throwing. Surface install: true in the default parameter for clarity. Also add the Oxford comma to the CHANGELOG install entry. Addresses #50. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Pushed
All tests green (21 node + 20 browser, 1 pre-existing WebKit skip), lint clean. |
Per review, "setGlobal" reads more clearly than "install" for the load()/loadOnce() option that controls whether the polyfill attaches to the global environment (and sets globalThis.chapi). Rename the option, the internal variable, and all references in the spec, README, and CHANGELOG. Behavior is unchanged: default true preserves the existing global-install behavior; false attaches nothing and returns the polyfill. Addresses #50. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
TallTed
left a comment
There was a problem hiding this comment.
English looks good. I cannot judge the code.
What
Adds the simplified, single-call CHAPI
interact()API — both the spec (docs/specs/interact-api.md) and an initial implementation.typeparam); translates into the existingget()flow, carryinginteractionUrlin theprotocolsmap under the well-knowninteractmeta-protocol key:web: {protocols: {interact: interactionUrl}}. No mediator/RPC changes.interactionUrlis treated as opaque — the polyfill does not fetch, parse, or encode it. The full protocols object is fetched from that URL over TLS by the selected handler, giving source authentication and disconnected-system (QR) support.{}on completion; rejectsAbortErroron user cancel orsignalabort; otherwise surfaces the same errors asget().navigator.chapi.interact()and thechapiobject on the value returned byload()/loadOnce().get()/store()/load()/loadOnce().Structure (functional core / imperative shell)
lib/interactRequest.js— purecreateInteractRequest()builder (validateshttps:, builds the request); nonavigator/network, independently unit-tested.lib/interact.js—interact()shell built around an injected credentials container + secure-context assertion; maps result/abort to the resolution contract.lib/index.js— wireschapionto the returned polyfill andnavigator.chapi.Why
Addresses #50. The current API requires relying parties to compose a full
web/VerifiablePresentationrequest object. A singleinteractionUrl-based entry point is much simpler for coordinator websites, and the URL indirection authenticates the protocols source over TLS.Testing
npm run test:node—node --testunit tests for the pure builder (9) and the shell (9): request shape, opaque pass-through, validation, abort/cancel mapping, secure-context.npm testruns the Node tests and the cross-browser Playwright suite (Chromium/Firefox/WebKit). Addedtest/interact.spec.js(wiring smoke tests) and anavigator.chapiassertion to the existing load smoke test.Open questions (for review)
Left open in the spec and called out inline: final method name;
{}vsundefinedresolution payload; first-call performance; whether abort must actively tear down the in-flight RPC vs. abandon it; and whether anullget()result (no credential selected) should map toAbortError(current behavior) or resolve empty. Theinteractprotocols-key and droppingtypewere resolved during review.Addresses #50.
🤖 Generated with Claude Code