feat: invite-code mode for cross-device RP flows (WDP-73 / APP-9428)#239
Open
SeanROlszewski wants to merge 6 commits intomainfrom
Open
feat: invite-code mode for cross-device RP flows (WDP-73 / APP-9428)#239SeanROlszewski wants to merge 6 commits intomainfrom
SeanROlszewski wants to merge 6 commits intomainfrom
Conversation
Implements the idkit side of WDP-73. The RP shows the user a 6-character code instead of a QR; the user types it into World App on a different device. Same proof flow, same poll loop, same Status enum — only the discovery channel changes. Wire shape (matches wallet-bridge APP-9425 and world-app-ios APP-9424): - Code is canonical 6-char Crockford Base32 (5 random data chars + 1 mod-32 weighted check digit, weights 1/3/5/7/9 — all coprime to 32, so 100% of single-char substitutions are caught). UI may format as "ABC-DEF" but the canonical form has no separator. - HKDF-SHA256, no salt, 32-byte output, IKM = canonical code's UTF-8 bytes. info="dx" → lookup index (lowercase hex on the wire — matches world-app-ios's WalletBridgeInviteCodeRedeemer); info="key" → AES-256-GCM key. The encryption key never reaches the bridge — only the index and ciphertext do. - POST /request body has the new `request_code_enabled: true, iv, payload, index` shape with `iv`/`payload` base64url (no padding) to satisfy wallet-bridge's URL_SAFE_NO_PAD validator. Legacy URL/QR path is byte-identical to before — same `EncryptedPayload { iv, payload }` in standard base64. - Response carries `session_nonce` and `code_expires_at`. Polling attaches `Authorization: Bearer <session_nonce>` so we're forward- compatible with the bridge's session_nonce gate (a follow-up to APP-9425); URL-mode connections keep `session_nonce: None` and the header is omitted, so existing bridge behavior is unchanged. Cross-device implications resolved with the WDP-73 author: - The mermaid diagram in the spec describes a `delivery_token` minted on /code/redeem and ferried back via universal-link to gate /response. That mechanism is fundamentally same-device — universal links route to the device that opened them, so a desktop browser ↔ phone flow never receives the token. - The product is dominantly cross-device (Reddit × World), so we drop delivery_token from idkit's surface entirely. Anti-collusion security in the code path now degrades to "10-min TTL + one-shot redeem" — weaker than the spec mermaid implied, but it's the only model that actually works for the product. - The spec's V1 Proposal section (bridge holds the connector_url) is superseded by the Additional Security Flow; needs a follow-up edit on the spec to mark V1 stale. API surface added (mirrors existing URL-mode): - Rust: `BridgeConnection::create_for_invite_code` (pub(crate), retry- once-on-409). `BridgeConnection.invite_code()` / `.code_expires_at()` accessors. New private fields on `BridgeConnection` keep the public type clean — wrappers expose them, internal type doesn't surface Option<...> ergonomic hazards. - crypto.rs: `generate_invite_code`, `parse_invite_code` (private to the module — consumed only by tests today; world-app-ios has its own Swift port), `hkdf_invite_index_hex`, `hkdf_invite_key`, `generate_nonce`. - FFI: `IDKitInviteCodeRequest` sibling to `IDKitRequestWrapper`. Method names mirror the URL wrapper exactly (request_id, poll_status, poll_status_once) — adopters write the same poll loop. New builder methods `constraints_with_invite_code` / `preset_with_invite_code` on `IDKitBuilder`. - WASM: `IDKitInviteCodeRequest` sibling to `IDKitRequest`, same Rc<RefCell<Option<…>>> close semantics. `constraintsWithInviteCode` / `presetWithInviteCode` on the WASM builder. - TS: `IDKitInviteCodeRequest` interface + impl, `IDKitInviteCodeBuilder`, `IDKit.requestWithInviteCode(...)` entry point. Code mode is bridge-only by definition (user is on a different device than World App), so the builder skips the `isInWorldApp()` branch `IDKitBuilder` performs. - Shared `pollUntilCompletionLoop` helper extracted from `IDKitRequestImpl` so both impls share the loop without duplication. Tests: - 9 new rust unit tests covering code generation, parser (round-trip, separator stripping, lowercase normalization, Crockford ambiguity collapse, U-rejection, length validation, check-digit validation, exhaustive single-char substitution detection across all positions × all alphabet chars), HKDF determinism, hex output shape, index/key differentiation across info strings. - 86 existing rust tests pass; 54 core JS tests pass; 23 React tests pass. - Clippy clean on native + ffi feature combos. Pre-existing `cast_possible_truncation`/`cast_sign_loss` lints in `rp_signature.rs` and `types.rs` against the wasm32 target are unchanged (introduced by e516ddb in January). Adopter diff is two lines per integration: // before const req = await IDKit.request(config).constraints(...); showQR(req.connectorURI); const result = await req.pollUntilCompletion(); // after const req = await IDKit.requestWithInviteCode(config).constraints(...); showCode(req.code); const result = await req.pollUntilCompletion(); Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a "Use invite code" checkbox to the browser demo's config panel. When off, behavior is unchanged (QR mode). When on, the same 5 preset buttons go through `IDKit.requestWithInviteCode()` and display the 6-character code formatted as ABC-DEF with a live expiry countdown. Polling lifecycle is byte-identical between modes. Cross-device note: the demo can render the code without a deployed wallet-bridge with APP-9425, but the full round-trip (user types code into World App, proof returns) requires bridge-side support which is forthcoming. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…P-9428) Adds sibling APIs to the React widget package, mirroring the core's invite-code mode addition. The existing IDKitRequestWidget / useIDKitRequest / useIDKitFlow / QRState are byte-identical — strict additive change. New in @worldcoin/idkit: - useIDKitInviteCodeRequest hook (sibling to useIDKitRequest) - useIDKitInviteCodeFlow hook helper with HookState that tracks code + codeExpiresAt instead of connectorURI - IDKitInviteCodeRequestWidget component - IDKitInviteCodeWidgetBase (sibling base; the existing IDKitWidgetBase reads flow.connectorURI directly so a near-copy was needed) - InviteCodeState body component (large monospace ABC-DEF display, live "Expires in Xs" countdown, instruction line) - New types: IDKitInviteCodeRequestHookConfig, UseIDKitInviteCodeRequestHookResult, IDKitInviteCodeRequestWidgetProps Next.js example: adds a "Use invite code" checkbox to the config panel and conditionally renders IDKitInviteCodeRequestWidget instead of IDKitRequestWidget. Same config object — invite-code mode adds no new input fields. Same onSuccess / handleVerify / onError callbacks. Tests: 23/23 react package tests pass. Pre-existing type-check errors in __tests__/{hooks,widgets}.test.tsx remain (using "All" as a defunct credential type) — unrelated to this change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a Swift wrapper for the new uniffi IdKitInviteCodeRequest type and wires it into the IDKitSampleApp. Swift package (swift/Sources/IDKit/IDKit.swift): - New IDKitInviteCodeRequest class — wraps uniffi-generated IdKitInviteCodeRequest. Exposes code: String, expiresAt: Date, requestID: UUID, plus pollStatusOnce() / pollUntilCompletion(options:) mirroring IDKitRequest. Reuses IDKitStatus, IDKitPollOptions, IDKitCompletionResult, IDKitErrorCode, IDKitClientError — no new shared types. - IDKitBuilder.presetWithInviteCode(_:) extension method. - constraintsWithInviteCode left as a TODO matching the existing "Re-enable when World ID 4.0 is live" comments around .constraints. iOS sample app (swift/Examples/IDKitSampleApp): - New SampleMode enum (.connectUrl / .inviteCode) and Picker in the form's Request section. - Hides the "Connect URL mode" picker (App Clip) when in code mode — irrelevant. - New form section "Invite Code" displays the code formatted as ABC-DEF in large monospace, with the expiry timestamp and a Copy-to-clipboard button. Visible only when in code mode and a code has been generated. - The "Connector URL" section is hidden in code mode. - generate() (renamed from generateRequestURL) branches on mode and routes to either the existing path or .presetWithInviteCode. - Polling loop extracted to a generic helper that takes () async -> IDKitStatus, so both request types reuse the same loop. The deep-link callback handler is unchanged; it's a no-op in code mode (cross-device, no universal-link round-trip in this design). Verification: - scripts/package-swift.sh ran successfully (regenerated bindings, rebuilt IDKitFFI.xcframework for 5 Apple targets). - swift build (Swift package): build complete. - xcodebuild -scheme IDKitSampleApp -destination 'generic/platform=iOS Simulator': ** BUILD SUCCEEDED **. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The invite-code path was emitting base64url-no-pad to satisfy wallet-bridge's URL_SAFE_NO_PAD validator. The URL/QR path uses standard base64; aligning code-mode on the same encoding eliminates the asymmetry. Requires the matching wallet-bridge change to swap URL_SAFE_NO_PAD for STANDARD on /request iv/payload validation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
You must have Developer access to commit code to Worldcoin on Vercel. If you contact an administrator and receive Developer access, commit again to see your changes. Learn more: https://vercel.com/docs/accounts/team-members-and-roles/access-roles#team-level-roles |
) Lets you point the example at a local wallet-bridge during development without hardcoding. App ID is no longer readonly, and a Bridge URL input is plumbed into the request config (defaults to undefined when blank, preserving the prod default). Adds a hint that local-bridge URLs require an app_staging_ App ID — idkit's BridgeUrl validator only relaxes the localhost check for staging app IDs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a code-based discovery channel to the existing Bridge proof flow. Instead of clicking a deeplink or scanning a QR code, an RP shows the user a 6-character code; the user types it into World App on their phone and completes the existing Selfie Check / proof flow. Same proof flow, same poll loop, same Status enum — only the discovery channel changes.
info="dx"→ lookup index (lowercase hex),info="key"→ AES-256-GCM key. The encryption key never reaches Bridge — only the index and ciphertext do.ivandpayloadare sent as standard base64, matching the URL/QR path.delivery_tokenmechanism is fundamentally same-device (universal links route to the device that opened them), so we drop it from idkit's surface entirely. Anti-collusion in the code path now degrades to "10-min TTL + one-shot redeem + per-IP rate limit" — see the spec for the full discussion.session_nonceBearer auth sent onGET /response/:idfor forward compatibility with wallet-bridge's session-nonce gate (release-blocking follow-up on Bridge). URL/QR connections keepsession_nonce: Noneand the header is omitted, so existing bridge behavior is unchanged.API surface added (mirrors existing URL-mode)
BridgeConnection::create_for_invite_codewith retry-once-on-409.BridgeConnection.invite_code()/.code_expires_at()accessors. Newcrypto.rsprimitives:generate_invite_code,parse_invite_code,hkdf_invite_index_hex,hkdf_invite_key,generate_nonce.IDKitInviteCodeRequestsibling toIDKitRequestWrapper. New builder methodsconstraints_with_invite_code/preset_with_invite_codeonIDKitBuilder.IDKitInviteCodeRequestsibling toIDKitRequest, sameRc<RefCell<Option<…>>>close semantics.constraintsWithInviteCode/presetWithInviteCodeon the WASM builder.IDKitInviteCodeRequestinterface + impl,IDKitInviteCodeBuilder,IDKit.requestWithInviteCode(...)entry point. Code mode is bridge-only by definition (user is on a different device than World App), so the builder skips theisInWorldApp()branch.IDKitInviteCodeRequestWidget,useIDKitInviteCodeRequest,useIDKitInviteCodeFlowhooks. NewInviteCodeStateUI component.presetWithInviteCode(_:)/constraintsWithInviteCode(_:)on the builder;IDKitInviteCodeRequestwrapper exposingcodeandexpiresAt: Date.Tests
Test plan
pnpm -C js/examples/browser dev); toggle "Use invite code" and confirm a 6-char code is rendered with countdown.pnpm -C js/examples/nextjs dev); switch the demo toggle to invite-code and confirm the widget renders the code.Confirmed.GET /response/:id).Dependencies
session_nonceenforcement onGET /response/:idis a release-blocking follow-up on the Bridge side (currently stored but not verified — see spec §10).WalletBridgeInviteCodeRedeemerthat decrypts the/code/redeempayload and runs the iOS Selfie Check flow.🤖 Generated with Claude Code