diff --git a/Cargo.lock b/Cargo.lock index 733c4dd..a6fe7a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -486,7 +486,7 @@ dependencies = [ "alloy-rlp", "alloy-serde", "alloy-sol-types 1.5.7", - "itertools 0.13.0", + "itertools 0.14.0", "serde", "serde_json", "serde_with", @@ -709,7 +709,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa501ad58dd20acddbfebc65b52e60f05ebf97c52fa40d1b35e91f5e2da0ad0e" dependencies = [ "alloy-transport", - "itertools 0.13.0", + "itertools 0.14.0", "url", ] @@ -5344,9 +5344,9 @@ checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "world-id-primitives" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13939e35b7f9a77ad4f7cf268a1a5cd29597a4cb60377395457eafd8df0b5839" +checksum = "e180c481c1c9dcc8a9a2e9f20800454a9307f44f416c00e58bfdce87b4630e67" dependencies = [ "alloy", "alloy-primitives 1.5.7", diff --git a/Cargo.toml b/Cargo.toml index 4b4d083..e8ccca1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ authors = ["World Contributors"] [workspace.dependencies] # World ID protocol types -world-id-primitives = { version = "0.5.0", default-features = false } +world-id-primitives = { version = "0.5.1", default-features = false } # Serialization serde = { version = "1.0", features = ["derive"] } diff --git a/js/examples/nextjs/app/debug-proof-request/page.tsx b/js/examples/nextjs/app/debug-proof-request/page.tsx new file mode 100644 index 0000000..5830a74 --- /dev/null +++ b/js/examples/nextjs/app/debug-proof-request/page.tsx @@ -0,0 +1,15 @@ +import type { ReactElement } from "react"; +import { DemoClient } from "./ui"; + +export default function HomePage(): ReactElement { + return ( +
+

IDKit Next.js Example

+

+ This example shows the widget request flow with the same legacy presets + as the browser example. +

+ +
+ ); +} diff --git a/js/examples/nextjs/app/debug-proof-request/ui.tsx b/js/examples/nextjs/app/debug-proof-request/ui.tsx new file mode 100644 index 0000000..a1ee636 --- /dev/null +++ b/js/examples/nextjs/app/debug-proof-request/ui.tsx @@ -0,0 +1,514 @@ +"use client"; + +import { + useCallback, + useEffect, + useMemo, + useRef, + useState, + type ReactElement, +} from "react"; +import { + documentLegacy, + deviceLegacy, + selfieCheckLegacy, + useIDKitRequest, + orbLegacy, + secureDocumentLegacy, + type Preset, + type RpContext, +} from "@worldcoin/idkit"; + +const APP_ID = process.env.NEXT_PUBLIC_APP_ID as `app_${string}` | undefined; +const RP_ID = process.env.NEXT_PUBLIC_RP_ID; +const STAGING_CONNECT_BASE_URL = "https://staging.world.org/verify"; +const DEFAULT_BRIDGE_URL = "https://bridge.worldcoin.org"; +const CONNECT_URL_OVERRIDE_TOOLTIP = + "Enable this to change the deeplink base URL to the staging verify endpoint. Useful when testing with a Staging iOS World App build that supports this override."; + +type PresetKind = "orb" | "secure_document" | "document" | "device" | "selfie"; + +function createPreset(kind: PresetKind, signal: string) { + switch (kind) { + case "orb": + return orbLegacy({ signal }); + case "secure_document": + return secureDocumentLegacy({ signal }); + case "document": + return documentLegacy({ signal }); + case "device": + return deviceLegacy({ signal }); + case "selfie": + return selfieCheckLegacy({ signal }); + default: { + const exhaustive: never = kind; + throw new Error(`Unsupported preset: ${String(exhaustive)}`); + } + } +} + +async function fetchRpContext(action: string): Promise { + const response = await fetch("/api/rp-signature", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ action }), + }); + + if (!response.ok) { + const payload = await response.json(); + throw new Error(payload.error ?? "Failed to fetch RP signature"); + } + + const data = (await response.json()) as { + sig: string; + nonce: string; + created_at: number; + expires_at: number; + }; + + if (!RP_ID) { + throw new Error("Missing NEXT_PUBLIC_RP_ID"); + } + + console.log(data); + + return { + rp_id: RP_ID, + nonce: data.nonce, + created_at: data.created_at, + expires_at: data.expires_at, + signature: data.sig, + }; +} + +function base64ToBytes(base64: string): Uint8Array { + const binStr = atob(base64); + const bytes = new Uint8Array(binStr.length); + for (let i = 0; i < binStr.length; i++) { + bytes[i] = binStr.charCodeAt(i); + } + return bytes; +} + +async function decryptBridgePayload( + keyBase64: string, + ivBase64: string, + payloadBase64: string, +): Promise { + const keyBytes = base64ToBytes(keyBase64); + const ivBytes = base64ToBytes(ivBase64); + const ciphertext = base64ToBytes(payloadBase64); + + const cryptoKey = await crypto.subtle.importKey( + "raw", + keyBytes.buffer.slice( + keyBytes.byteOffset, + keyBytes.byteOffset + keyBytes.byteLength, + ) as ArrayBuffer, + "AES-GCM", + false, + ["decrypt"], + ); + + const decrypted = await crypto.subtle.decrypt( + { + name: "AES-GCM", + iv: ivBytes.buffer.slice( + ivBytes.byteOffset, + ivBytes.byteOffset + ivBytes.byteLength, + ) as ArrayBuffer, + }, + cryptoKey, + ciphertext.buffer.slice( + ciphertext.byteOffset, + ciphertext.byteOffset + ciphertext.byteLength, + ) as ArrayBuffer, + ); + + return JSON.parse(new TextDecoder().decode(decrypted)); +} + +function parseConnectorURI(uri: string): { + requestId: string; + key: string; + bridgeUrl: string; +} | null { + try { + const url = new URL(uri); + const requestId = url.searchParams.get("i"); + const key = url.searchParams.get("k"); + const bridgeUrl = url.searchParams.get("b") ?? DEFAULT_BRIDGE_URL; + + if (!requestId || !key) return null; + return { requestId, key, bridgeUrl }; + } catch { + return null; + } +} + +// ── Inner component: only mounted once all config is ready ────────────── + +function IDKitFlow({ + appId, + action, + rpContext, + preset, + environment, + overrideConnectBaseUrl, + onTryAgain, +}: { + appId: `app_${string}`; + action: string; + rpContext: RpContext; + preset: Preset; + environment: "production" | "staging"; + overrideConnectBaseUrl: string | undefined; + onTryAgain: () => void; +}): ReactElement { + const flow = useIDKitRequest({ + app_id: appId, + action, + rp_context: rpContext, + allow_legacy_proofs: true, + preset, + environment, + override_connect_base_url: overrideConnectBaseUrl, + }); + + const [decodedPayload, setDecodedPayload] = useState(null); + const [isFetchingPayload, setIsFetchingPayload] = useState(false); + + // Auto-open immediately on mount + const hasOpened = useRef(false); + useEffect(() => { + if (!hasOpened.current) { + hasOpened.current = true; + flow.open(); + } + }, [flow]); + + // Fetch and decrypt bridge payload when connectorURI appears + const lastFetchedURI = useRef(null); + useEffect(() => { + if (!flow.connectorURI || flow.connectorURI === lastFetchedURI.current) + return; + lastFetchedURI.current = flow.connectorURI; + + const parsed = parseConnectorURI(flow.connectorURI); + if (!parsed) return; + + const controller = new AbortController(); + + void (async () => { + setIsFetchingPayload(true); + setDecodedPayload(null); + + try { + const url = `${parsed.bridgeUrl}/request/${parsed.requestId}`; + const res = await fetch(url, { signal: controller.signal }); + + if (!res.ok) { + console.error("Bridge request fetch failed:", res.status); + return; + } + + const data = await res.json(); + + if (data.iv && data.payload) { + const decrypted = await decryptBridgePayload( + parsed.key, + data.iv, + data.payload, + ); + setDecodedPayload(JSON.stringify(decrypted, null, 2)); + } + } catch (err) { + if (!controller.signal.aborted) { + console.error("Bridge payload fetch/decrypt failed:", err); + } + } finally { + setIsFetchingPayload(false); + } + })(); + + return () => controller.abort(); + }, [flow.connectorURI]); + + const getStatusText = () => { + if (flow.isError) return `Error: ${flow.errorCode}`; + if (flow.isSuccess) return "Success"; + if (flow.isAwaitingUserConfirmation) return "Awaiting user confirmation..."; + if (flow.isAwaitingUserConnection) return "Awaiting user connection..."; + if (flow.isOpen) return "Connecting..."; + return "Idle"; + }; + + return ( + <> + {flow.connectorURI && ( +
+

Connector URI

+