Skip to content
Open
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
13 changes: 13 additions & 0 deletions js/examples/nextjs/app/ui.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ export function DemoClient(): ReactElement {
const [useReturnTo, setUseReturnTo] = useState(false);
const [returnTo, setReturnTo] = useState("");
const [isReturnToTooltipOpen, setIsReturnToTooltipOpen] = useState(false);
const [requireUserPresence, setRequireUserPresence] = useState(false);

const genesisIssuedAtMin =
genesisEnabled && genesisDate
Expand Down Expand Up @@ -485,6 +486,17 @@ export function DemoClient(): ReactElement {
placeholder="googlechromes://"
/>
</div>
<div className="config-row">
<label htmlFor="cfgRequireUserPresence">
{"User presence (Face Auth)"}
</label>
<input
type="checkbox"
id="cfgRequireUserPresence"
checked={requireUserPresence}
onChange={(e) => setRequireUserPresence(e.target.checked)}
/>
</div>
</section>

<div className="stack">
Expand Down Expand Up @@ -514,6 +526,7 @@ export function DemoClient(): ReactElement {
action={action || "test-action"}
rp_context={widgetRpContext}
allow_legacy_proofs={true}
require_user_presence={requireUserPresence}
{...widgetConstraintsOrPreset}
onSuccess={(result) => {
setWidgetIdkitResult(result);
Expand Down
31 changes: 31 additions & 0 deletions js/packages/core/src/__tests__/smoke.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,35 @@ describe("IDKitRequest API", () => {
).toThrow("rp_context is required");
});

it("should default require_user_presence to false in request config", () => {
const builder = IDKit.request({
app_id: "app_staging_test",
action: "test-action",
rp_context: TEST_SESSION_CONFIG.rp_context,
allow_legacy_proofs: false,
});

expect((builder as any).config.require_user_presence).toBe(false);
});

it("should preserve require_user_presence in request and session configs", () => {
const requestBuilder = IDKit.request({
app_id: "app_staging_test",
action: "test-action",
rp_context: TEST_SESSION_CONFIG.rp_context,
allow_legacy_proofs: false,
require_user_presence: true,
});

const sessionBuilder = IDKit.createSession({
...TEST_SESSION_CONFIG,
require_user_presence: true,
});

expect((requestBuilder as any).config.require_user_presence).toBe(true);
expect((sessionBuilder as any).config.require_user_presence).toBe(true);
});

it("should reject malformed session_id values in proveSession", () => {
expect(() =>
IDKit.proveSession(
Expand Down Expand Up @@ -173,6 +202,7 @@ describe("IDKitRequest API", () => {
null,
null,
true,
false,
null,
null,
"production",
Expand All @@ -196,6 +226,7 @@ describe("Enums", () => {
expect(IDKitErrorCodes.CredentialUnavailable).toBe(
"credential_unavailable",
);
expect(IDKitErrorCodes.UserPresenceFailed).toBe("user_presence_failed");
expect(IDKitErrorCodes.WorldId4NotAvailable).toBe(
"world_id_4_not_available",
);
Expand Down
6 changes: 6 additions & 0 deletions js/packages/core/src/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,7 @@ function createWasmBuilderFromConfig(
config.action_description ?? null,
config.bridge_url ?? null,
config.allow_legacy_proofs ?? false,
config.require_user_presence ?? false,
config.override_connect_base_url ?? null,
config.return_to ?? null,
config.environment ?? null,
Expand All @@ -385,6 +386,7 @@ function createWasmBuilderFromConfig(
rpContext,
config.action_description ?? null,
config.bridge_url ?? null,
config.require_user_presence ?? false,
config.override_connect_base_url ?? null,
config.return_to ?? null,
config.environment ?? null,
Expand All @@ -397,6 +399,7 @@ function createWasmBuilderFromConfig(
rpContext,
config.action_description ?? null,
config.bridge_url ?? null,
config.require_user_presence ?? false,
config.override_connect_base_url ?? null,
config.return_to ?? null,
config.environment ?? null,
Expand Down Expand Up @@ -618,6 +621,7 @@ function createRequest(config: IDKitRequestConfig): IDKitBuilder {
bridge_url: config.bridge_url,
return_to: config.return_to,
allow_legacy_proofs: config.allow_legacy_proofs,
require_user_presence: config.require_user_presence ?? false,
override_connect_base_url: config.override_connect_base_url,
environment: config.environment,
});
Expand Down Expand Up @@ -667,6 +671,7 @@ function createSession(config: IDKitSessionConfig): IDKitBuilder {
action_description: config.action_description,
bridge_url: config.bridge_url,
return_to: config.return_to,
require_user_presence: config.require_user_presence ?? false,
override_connect_base_url: config.override_connect_base_url,
environment: config.environment,
});
Expand Down Expand Up @@ -728,6 +733,7 @@ function proveSession(
action_description: config.action_description,
bridge_url: config.bridge_url,
return_to: config.return_to,
require_user_presence: config.require_user_presence ?? false,
override_connect_base_url: config.override_connect_base_url,
environment: config.environment,
});
Expand Down
79 changes: 79 additions & 0 deletions js/packages/core/src/transports/native.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,85 @@ describe("native transport request lifecycle", () => {
expect(completion.success).toBe(true);
});

it("defaults missing user_presence_completed to false on success", async () => {
const req = createNativeRequest({ payload: 1 }, baseConfig, {}, "");
activeRequest = req;

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

miniKitHandlers["miniapp-verify-action"]?.({
status: "success",
protocol_version: "3.0",
verification_level: "orb",
signal_hash: "0xabc",
proof: "0x01",
merkle_root: "0x02",
nullifier_hash: "0x03",
});

const completion = await completionPromise;
expect(completion.success).toBe(true);
if (completion.success) {
expect(completion.result.user_presence_completed).toBe(false);
}
});

it("fails when user presence is required but not completed", async () => {
const req = createNativeRequest(
{ payload: 1 },
{ ...baseConfig, require_user_presence: true },
{},
"",
);
activeRequest = req;

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

miniKitHandlers["miniapp-verify-action"]?.({
status: "success",
protocol_version: "3.0",
verification_level: "orb",
signal_hash: "0xabc",
proof: "0x01",
merkle_root: "0x02",
nullifier_hash: "0x03",
});

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

it("preserves completed user presence on success", async () => {
const req = createNativeRequest(
{ payload: 1 },
{ ...baseConfig, require_user_presence: true },
{},
"",
);
activeRequest = req;

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

miniKitHandlers["miniapp-verify-action"]?.({
status: "success",
user_presence_completed: true,
protocol_version: "3.0",
verification_level: "orb",
signal_hash: "0xabc",
proof: "0x01",
merkle_root: "0x02",
nullifier_hash: "0x03",
});

const completion = await completionPromise;
expect(completion.success).toBe(true);
if (completion.success) {
expect(completion.result.user_presence_completed).toBe(true);
}
});

it("uses per-identifier signal hashes when response omits signal_hash", async () => {
const signalHashes = {
proof_of_human: hashSignal("poh-signal"),
Expand Down
26 changes: 26 additions & 0 deletions js/packages/core/src/transports/native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export interface BuilderConfig {
bridge_url?: string;
return_to?: string;
allow_legacy_proofs?: boolean;
require_user_presence?: boolean;
override_connect_base_url?: string;
environment?: string;
}
Expand Down Expand Up @@ -167,13 +168,24 @@ class NativeIDKitRequest implements IDKitRequest {
return;
}

const userPresenceCompleted = getUserPresenceCompleted(responsePayload);

if (config.require_user_presence === true && !userPresenceCompleted) {
this.complete({
success: false,
error: IDKitErrorCodes.UserPresenceFailed,
});
return;
}

this.complete({
success: true,
result: nativeResultToIDKitResult(
responsePayload,
config,
signalHashes,
legacySignalHash,
userPresenceCompleted,
),
});
};
Expand Down Expand Up @@ -349,6 +361,7 @@ function nativeResultToIDKitResult(
config: BuilderConfig,
signalHashes: Record<string, string>,
legacySignalHash: string,
userPresenceCompleted: boolean,
): IDKitResult {
const p = payload as Record<string, any>;
const rpNonce = config.rp_context?.nonce ?? "";
Expand All @@ -372,6 +385,7 @@ function nativeResultToIDKitResult(
issuer_schema_id: item.issuer_schema_id,
expires_at_min: item.expires_at_min,
})),
user_presence_completed: userPresenceCompleted,
environment: config.environment ?? "production",
} satisfies IDKitResultSession;
}
Expand All @@ -389,6 +403,7 @@ function nativeResultToIDKitResult(
issuer_schema_id: item.issuer_schema_id,
expires_at_min: item.expires_at_min,
})),
user_presence_completed: userPresenceCompleted,
environment: config.environment ?? "production",
} satisfies IDKitResultV4;
}
Expand All @@ -414,6 +429,7 @@ function nativeResultToIDKitResult(
merkle_root: v.merkle_root,
nullifier: v.nullifier_hash,
})),
user_presence_completed: userPresenceCompleted,
environment: config.environment ?? "production",
} satisfies IDKitResultV3;
}
Expand All @@ -435,6 +451,16 @@ function nativeResultToIDKitResult(
nullifier: p.nullifier_hash,
},
],
user_presence_completed: userPresenceCompleted,
environment: config.environment ?? "production",
} satisfies IDKitResultV3;
}

function getUserPresenceCompleted(payload: unknown): boolean {
const p = payload as Record<string, any>;
return (
p?.user_presence_completed === true ||
(p?.proof_response as Record<string, any> | undefined)
?.user_presence_completed === true
);
}
5 changes: 5 additions & 0 deletions js/packages/core/src/types/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ export type IDKitRequestConfig = {
*/
allow_legacy_proofs: boolean;

/** Require World App to perform a user-presence check before verification. Defaults to false. */
require_user_presence?: boolean;

/** Optional override for the connect base URL (e.g., for staging environments) */
override_connect_base_url?: string;

Expand All @@ -84,6 +87,8 @@ export type IDKitSessionConfig = {
bridge_url?: string;
/** Optional deep-link callback URL appended as `return_to` on the connector URL. */
return_to?: string;
/** Require World App to perform a user-presence check before verification. Defaults to false. */
require_user_presence?: boolean;
/** Optional override for the connect base URL (e.g., for staging environments) */
override_connect_base_url?: string;

Expand Down
1 change: 0 additions & 1 deletion js/packages/core/src/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
export * from "./bridge";
export * from "./config";
export * from "./result";
1 change: 1 addition & 0 deletions js/packages/core/src/types/result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export enum IDKitErrorCodes {
ConnectionFailed = "connection_failed",
MaxVerificationsReached = "max_verifications_reached",
FailedByHostApp = "failed_by_host_app",
UserPresenceFailed = "user_presence_failed",
InvalidRpSignature = "invalid_rp_signature",
NullifierReplayed = "nullifier_replayed",
DuplicateNonce = "duplicate_nonce",
Expand Down
Loading
Loading