Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions js/examples/nextjs/app/api/verify-proof/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,14 @@ export async function POST(request: Request): Promise<Response> {
console.log("Received response from Dev Portal:", {
payload,
status: response.status,
statusText: response.statusText,
});

return NextResponse.json(payload, {
status: response.status,
});
} catch (error) {
console.error("Failed to verify proof:", error);
return NextResponse.json(
{
error: error instanceof Error ? error.message : "Unknown server error",
Expand Down
18 changes: 11 additions & 7 deletions js/examples/nextjs/app/ui.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,10 +119,16 @@ async function verifyProof(payload: IDKitResult): Promise<unknown> {
});

const json = await response.json();
console.log("Verify proof response:", {
ok: response.ok,
status: response.status,
statusText: response.statusText,
payload: json,
});

if (!response.ok) {
throw new Error(json.error ?? "Verification failed");
}

return json;
}

Expand Down Expand Up @@ -529,10 +535,9 @@ export function DemoClient(): ReactElement {
rp_context={widgetRpContext}
allow_legacy_proofs={true}
{...widgetConstraintsOrPreset}
onSuccess={(result) => {
setWidgetIdkitResult(result);
}}
onSuccess={(result) => {}}
handleVerify={async (result) => {
setWidgetIdkitResult(result);
const verified = await verifyProof(result);
setWidgetVerifyResult(verified);
}}
Expand All @@ -552,10 +557,9 @@ export function DemoClient(): ReactElement {
rp_context={widgetRpContext}
allow_legacy_proofs={true}
{...widgetConstraintsOrPreset}
onSuccess={(result) => {
setWidgetIdkitResult(result);
}}
onSuccess={(result) => {}}
handleVerify={async (result) => {
setWidgetIdkitResult(result);
const verified = await verifyProof(result);
setWidgetVerifyResult(verified);
}}
Expand Down
101 changes: 97 additions & 4 deletions js/packages/core/src/transports/native.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ const baseConfig: BuilderConfig = {
action: "test-action",
};

const proofResponseProof = [1n, 2n, 3n, 4n, 5n]
.map((value) => value.toString(16).padStart(64, "0"))
.join("");

function proofResponseNullifier(value: string): string {
return `nil_${value.padStart(64, "0")}`;
}

describe("native transport request lifecycle", () => {
let listeners: Array<(event: MessageEvent) => void> = [];
let miniKitHandlers: Record<string, (payload: any) => void> = {};
Expand Down Expand Up @@ -120,18 +128,20 @@ describe("native transport request lifecycle", () => {
miniKitHandlers["miniapp-verify-action"]?.({
status: "success",
proof_response: {
id: "req_abc123",
version: 1,
responses: [
{
identifier: "proof_of_human",
proof: ["0x01"],
nullifier: "0x02",
proof: proofResponseProof,
nullifier: proofResponseNullifier("a"),
issuer_schema_id: 1,
expires_at_min: 0,
},
{
identifier: "face",
proof: ["0x11"],
nullifier: "0x12",
proof: proofResponseProof,
nullifier: proofResponseNullifier("b"),
issuer_schema_id: 11,
expires_at_min: 0,
},
Expand All @@ -151,6 +161,89 @@ describe("native transport request lifecycle", () => {
}
});

it("normalizes wrapped v4 ProofResponse fields from native host", async () => {
const req = createNativeRequest({}, baseConfig, {}, "");
activeRequest = req;

const completionPromise = req.pollUntilCompletion({ timeout: 1000 });

miniKitHandlers["miniapp-verify-action"]?.({
status: "success",
proof_response: {
id: "req_abc123",
version: 1,
responses: [
{
identifier: "proof_of_human",
proof: proofResponseProof,
nullifier: proofResponseNullifier("a"),
issuer_schema_id: 1,
expires_at_min: 0,
},
],
},
});

const completion = await completionPromise;
expect(completion.success).toBe(true);
if (completion.success) {
expect(completion.result.protocol_version).toBe("4.0");
expect(completion.result.responses[0]).toMatchObject({
proof: ["1", "2", "3", "4", "5"],
nullifier: `0x${"a".padStart(64, "0")}`,
});
}
});

it("maps v4 ProofResponse error codes from native host", async () => {
const req = createNativeRequest({}, baseConfig, {}, "");
activeRequest = req;

const completionPromise = req.pollUntilCompletion({ timeout: 1000 });

miniKitHandlers["miniapp-verify-action"]?.({
status: "success",
proof_response: {
id: "req_abc123",
version: 1,
error: "nullifier_replay",
responses: [],
},
});

const completion = await completionPromise;
expect(completion).toEqual({
success: false,
error: IDKitErrorCodes.NullifierReplayed,
});
});

it("rejects root v4 ProofResponse instead of treating it as legacy v3", async () => {
const req = createNativeRequest({}, baseConfig, {}, "");
activeRequest = req;

const completionPromise = req.pollUntilCompletion({ timeout: 1000 });

miniKitHandlers["miniapp-verify-action"]?.({
id: "req_abc123",
version: 1,
responses: [
{
identifier: "proof_of_human",
proof: proofResponseProof,
nullifier: proofResponseNullifier("a"),
issuer_schema_id: 1,
expires_at_min: 0,
},
],
});

await expect(completionPromise).resolves.toEqual({
success: false,
error: IDKitErrorCodes.UnexpectedResponse,
});
});

it("prefers response signal_hash over signal hashes map", async () => {
const signalHashes = { proof_of_human: hashSignal("from-constraints") };

Expand Down
109 changes: 60 additions & 49 deletions js/packages/core/src/transports/native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,8 @@ import type {
} from "../request";
import type { IDKitResult } from "../types/result";
import { IDKitErrorCodes } from "../types/result";
import type {
IDKitResultV3,
IDKitResultV4,
IDKitResultSession,
} from "../lib/wasm";
import type { IDKitResultV3 } from "../lib/wasm";
import { WasmModule } from "../lib/wasm";
import { isDebug } from "../lib/debug";

const MINIAPP_VERIFY_ACTION = "miniapp-verify-action";
Expand All @@ -37,6 +34,13 @@ type MiniKitBridge = {
unsubscribe?: (event: string) => void;
};

function toNativeErrorCode(error: unknown): IDKitErrorCodes {
const code = error instanceof Error ? error.message : String(error);
return (Object.values(IDKitErrorCodes) as string[]).includes(code)
? (code as IDKitErrorCodes)
: IDKitErrorCodes.GenericError;
}

// ─────────────────────────────────────────────────────────────────────────────
// Environment detection
// ─────────────────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -154,6 +158,9 @@ class NativeIDKitRequest implements IDKitRequest {
const handleIncomingPayload = (responsePayload: any) => {
if (this.completionResult) return;

if (isDebug())
console.debug("[IDKit] Native: received response", responsePayload);

if (responsePayload?.status === "error") {
if (isDebug())
console.warn(
Expand All @@ -167,15 +174,27 @@ class NativeIDKitRequest implements IDKitRequest {
return;
}

this.complete({
success: true,
result: nativeResultToIDKitResult(
try {
const result = nativeResultToIDKitResult(
responsePayload,
config,
signalHashes,
legacySignalHash,
),
});
);
if (isDebug())
console.debug(
"[IDKit] Native: mapped response",
result.protocol_version,
);
this.complete({ success: true, result });
} catch (error) {
if (isDebug())
console.warn("[IDKit] Native: failed to map response", error);
this.complete({
success: false,
error: toNativeErrorCode(error),
});
}
};

const handler = (event: MessageEvent) => {
Expand Down Expand Up @@ -219,12 +238,14 @@ class NativeIDKitRequest implements IDKitRequest {
if (isDebug())
console.debug(
`[IDKit] Native: sending verify command (version=${version}, platform=ios)`,
sendPayload,
);
w.webkit.messageHandlers.minikit.postMessage(sendPayload);
} else if (w.Android) {
if (isDebug())
console.debug(
`[IDKit] Native: sending verify command (version=${version}, platform=android)`,
sendPayload,
);
w.Android.postMessage(JSON.stringify(sendPayload));
} else {
Expand Down Expand Up @@ -253,7 +274,7 @@ class NativeIDKitRequest implements IDKitRequest {
if (isDebug())
console.debug(
"[IDKit] Native: request completed",
result.success ? "success" : `error=${result.error}`,
result.success === true ? "success" : `error=${result.error}`,
);
this.completionResult = result;
this.cleanup();
Expand Down Expand Up @@ -290,13 +311,14 @@ class NativeIDKitRequest implements IDKitRequest {
}

async pollOnce(): Promise<Status> {
if (!this.completionResult) {
const completionResult = this.completionResult;
if (!completionResult) {
return { type: "awaiting_confirmation" };
}
if (this.completionResult.success) {
return { type: "confirmed", result: this.completionResult.result };
if (completionResult.success === true) {
return { type: "confirmed", result: completionResult.result };
}
return { type: "failed", error: this.completionResult.error };
return { type: "failed", error: completionResult.error };
}

async pollUntilCompletion(
Expand Down Expand Up @@ -356,41 +378,30 @@ function nativeResultToIDKitResult(
// V4 response wrapped in `proof_response` envelope.
if ("proof_response" in p && p.proof_response != null) {
const proof_response = p.proof_response as Record<string, any>;
const items = (proof_response.responses ?? []) as Record<string, any>[];

if (proof_response.session_id) {
return {
protocol_version: "4.0" as const,
nonce: proof_response.nonce ?? rpNonce,
action_description: proof_response.action_description,
session_id: proof_response.session_id,
responses: items.map((item) => ({
identifier: item.identifier,
signal_hash: signalHashes[item.identifier],
proof: item.proof,
session_nullifier: item.session_nullifier,
issuer_schema_id: item.issuer_schema_id,
expires_at_min: item.expires_at_min,
})),
environment: config.environment ?? "production",
} satisfies IDKitResultSession;
}

return {
protocol_version: "4.0" as const,
nonce: proof_response.nonce ?? rpNonce,
action: proof_response.action ?? config.action ?? "",
action_description: proof_response.action_description,
responses: items.map((item) => ({
identifier: item.identifier,
signal_hash: signalHashes[item.identifier],
proof: item.proof,
nullifier: item.nullifier,
issuer_schema_id: item.issuer_schema_id,
expires_at_min: item.expires_at_min,
})),
if (isDebug())
console.debug("[IDKit] Native: mapping wrapped v4 proof_response", {
responseCount: proof_response.responses?.length,
responseIdentifiers: proof_response.responses?.map(
(item: Record<string, any>) => item.identifier,
),
});

return WasmModule.proofResponseToIDKitResult(proof_response, {
nonce: rpNonce,
action: config.action,
action_description: config.action_description,
environment: config.environment ?? "production",
} satisfies IDKitResultV4;
signal_hashes: signalHashes,
identity_attested: p.identity_attested,
}) as IDKitResult;
}

// Protocol ProofResponse must be nested under `proof_response`.
if (
Array.isArray(p.responses) &&
("id" in p || "version" in p || "error" in p)
) {
throw new Error(IDKitErrorCodes.UnexpectedResponse);
}

// Legacy multi-verification response (MiniKit v3 format).
Expand Down
Loading
Loading