Skip to main content
Maturity: Limited preview (0.3.0). Browser transport only.
The browser half of a relying-party (RP) integration. It builds a sign-in request, renders it, and receives the response for hand-off to your server. It holds no seed, never mints, and cannot verify. That is structural: the only runtime dependency is the dependency-free wire contract, so verification code cannot end up in your bundle. Verification belongs on your server. See RP server.

Exports

createSignInRequest

Builds the unsigned claim object. It is crypto-free, which is why it can live in a browser package. Run it on your backend anyway, since the result must be signed there and the nonce must be issued where the burn happens.
Sign the result with createPresentationRequestSigner. The browser only encodes the opaque compact JSON Web Signature (JWS) that comes back. idFacetProof is a request, not a decision: the device proves the family its subject mode implies whatever slot you name, and your verifier pins the slot from its own registry on the way back.
The idFacetProof wire shape is provisional and may change without a major version bump until the first external relying party integrates.

Rendering

A sign-in request fits in a single QR code, so render one static code. Encode the deep link, not the bare payload, because that is what lets the phone’s native camera open the app without an in-app scanner. A bare payload is a meaningless string to a camera.
buildDeepLink returns <scheme>://array-siop?request=<payload>. The scheme is required and belongs to the installed app build: vibecheck, vibecheck-testnet or vibecheck-dev. These are registered URL schemes rather than product names, so they still read vibecheck although the app is called Alive. There is no default, because any default would be the wrong app for someone. The same link carries rung-0 claim requests, and the app dispatches on the request’s typ. A compact JSON Web Signature is URL-safe in every character, so it goes in the query verbatim. Anything else is base64url-wrapped. The device applies the same test, so both shapes work against a single app build.
Three QR settings decide whether a code scans first time, and none is about capacity. Fix an integer scale rather than a target width, because a width makes the renderer derive a fractional scale and nearest-neighbour resample at it, giving an irregular mix of 2px and 3px modules. Use error-correction level L, since a code on a clean display is not defending against physical damage and the redundancy costs about 14% more modules. Keep the full 4-module quiet zone inside the canvas instead of relying on the surrounding CSS padding, or restyling the page silently degrades the code.

The frame helpers

encodeRequestFrames and animateFrames implement an animated multi-frame transport for payloads too large for one code, splitting into strings of the form array-req/1/<index>/<total>/<chunk>. You do not need them. Today’s requests fit in one QR, and the Alive app does not scan multi-frame codes: reassembling frames would mean building a scanner inside your own app instead of using the native camera. They are here for a future request shape that grows past a single symbol.

Receiving the response

Cross-device responses skip the browser entirely, because the device posts directly to your registered endpoint. Same-device responses arrive in the URL fragment, never the query string, so they never reach a server log. receiveFragmentResponse reads the fragment and immediately scrubs it from the address bar and history before returning. After the call the token exists only in the returned object.
handOffToBackend posts id_token and state form-encoded, and throws if your backend does not answer 2xx.
Do not host the callback page alongside untrusted third-party scripts. The helper scrubs the fragment on receipt, but anything running in that document beforehand can read the URL.