diff --git a/js/examples/nextjs/app/ui.tsx b/js/examples/nextjs/app/ui.tsx
index a8ca40aa..ca91afcf 100644
--- a/js/examples/nextjs/app/ui.tsx
+++ b/js/examples/nextjs/app/ui.tsx
@@ -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
@@ -485,6 +486,17 @@ export function DemoClient(): ReactElement {
placeholder="googlechromes://"
/>
+
@@ -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);
diff --git a/js/packages/core/src/__tests__/smoke.test.ts b/js/packages/core/src/__tests__/smoke.test.ts
index 39f75fc8..7b7b744b 100644
--- a/js/packages/core/src/__tests__/smoke.test.ts
+++ b/js/packages/core/src/__tests__/smoke.test.ts
@@ -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(
@@ -173,6 +202,7 @@ describe("IDKitRequest API", () => {
null,
null,
true,
+ false,
null,
null,
"production",
@@ -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",
);
diff --git a/js/packages/core/src/request.ts b/js/packages/core/src/request.ts
index 303eacc5..aeb878a8 100644
--- a/js/packages/core/src/request.ts
+++ b/js/packages/core/src/request.ts
@@ -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,
@@ -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,
@@ -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,
@@ -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,
});
@@ -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,
});
@@ -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,
});
diff --git a/js/packages/core/src/transports/native.test.ts b/js/packages/core/src/transports/native.test.ts
index 0fa1e8b6..7c88a9e4 100644
--- a/js/packages/core/src/transports/native.test.ts
+++ b/js/packages/core/src/transports/native.test.ts
@@ -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"),
diff --git a/js/packages/core/src/transports/native.ts b/js/packages/core/src/transports/native.ts
index 4d56b003..b0b031d3 100644
--- a/js/packages/core/src/transports/native.ts
+++ b/js/packages/core/src/transports/native.ts
@@ -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;
}
@@ -167,6 +168,16 @@ 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(
@@ -174,6 +185,7 @@ class NativeIDKitRequest implements IDKitRequest {
config,
signalHashes,
legacySignalHash,
+ userPresenceCompleted,
),
});
};
@@ -349,6 +361,7 @@ function nativeResultToIDKitResult(
config: BuilderConfig,
signalHashes: Record
,
legacySignalHash: string,
+ userPresenceCompleted: boolean,
): IDKitResult {
const p = payload as Record;
const rpNonce = config.rp_context?.nonce ?? "";
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
+ return (
+ p?.user_presence_completed === true ||
+ (p?.proof_response as Record | undefined)
+ ?.user_presence_completed === true
+ );
+}
diff --git a/js/packages/core/src/types/config.ts b/js/packages/core/src/types/config.ts
index 0fa77403..994ae987 100644
--- a/js/packages/core/src/types/config.ts
+++ b/js/packages/core/src/types/config.ts
@@ -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;
@@ -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;
diff --git a/js/packages/core/src/types/index.ts b/js/packages/core/src/types/index.ts
index 5a543a1e..287f5d08 100644
--- a/js/packages/core/src/types/index.ts
+++ b/js/packages/core/src/types/index.ts
@@ -1,3 +1,2 @@
-export * from "./bridge";
export * from "./config";
export * from "./result";
diff --git a/js/packages/core/src/types/result.ts b/js/packages/core/src/types/result.ts
index a38c8336..fecf4ea6 100644
--- a/js/packages/core/src/types/result.ts
+++ b/js/packages/core/src/types/result.ts
@@ -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",
diff --git a/js/packages/react/src/__tests__/hooks.test.tsx b/js/packages/react/src/__tests__/hooks.test.tsx
index 3135c84b..2af1acce 100644
--- a/js/packages/react/src/__tests__/hooks.test.tsx
+++ b/js/packages/react/src/__tests__/hooks.test.tsx
@@ -141,6 +141,7 @@ describe("request/session hooks", () => {
bridge_url: undefined,
return_to: undefined,
allow_legacy_proofs: false,
+ require_user_presence: false,
override_connect_base_url: undefined,
environment: undefined,
});
@@ -163,7 +164,7 @@ describe("request/session hooks", () => {
useIDKitSession({
app_id: "app_test",
rp_context: baseRpContext,
- constraints: { type: "All", children: [] },
+ constraints: { all: [] },
}),
);
@@ -181,6 +182,7 @@ describe("request/session hooks", () => {
rp_context: baseRpContext,
action_description: undefined,
bridge_url: undefined,
+ require_user_presence: false,
override_connect_base_url: undefined,
return_to: undefined,
environment: undefined,
@@ -205,7 +207,7 @@ describe("request/session hooks", () => {
app_id: "app_test",
rp_context: baseRpContext,
existing_session_id: SESSION_ID_2,
- preset: { type: "OrbLegacy" },
+ constraints: { all: [] },
}),
);
@@ -222,6 +224,7 @@ describe("request/session hooks", () => {
rp_context: baseRpContext,
action_description: undefined,
bridge_url: undefined,
+ require_user_presence: false,
override_connect_base_url: undefined,
return_to: undefined,
environment: undefined,
@@ -266,11 +269,46 @@ describe("request/session hooks", () => {
bridge_url: undefined,
return_to: "idkit://callback?step=proof",
allow_legacy_proofs: false,
+ require_user_presence: false,
override_connect_base_url: undefined,
environment: undefined,
});
});
+ it("request hook forwards require_user_presence to core", async () => {
+ requestMock.mockReturnValue({
+ preset: vi.fn(async () =>
+ makeRequest(async () => ({
+ type: "confirmed",
+ result: { proof: "ok" },
+ })),
+ ),
+ });
+
+ const { result } = renderHook(() =>
+ useIDKitRequest({
+ app_id: "app_test",
+ action: "test-action",
+ rp_context: baseRpContext,
+ allow_legacy_proofs: false,
+ require_user_presence: true,
+ preset: { type: "OrbLegacy" },
+ }),
+ );
+
+ act(() => {
+ result.current.open();
+ });
+
+ await waitFor(() => {
+ expect(result.current.isSuccess).toBe(true);
+ });
+
+ expect(requestMock).toHaveBeenCalledWith(
+ expect.objectContaining({ require_user_presence: true }),
+ );
+ });
+
it("session hook forwards return_to to createSession", async () => {
createSessionMock.mockReturnValue({
constraints: vi.fn(async () => ({
@@ -287,7 +325,8 @@ describe("request/session hooks", () => {
app_id: "app_test",
rp_context: baseRpContext,
return_to: "idkit://callback?step=create",
- constraints: { type: "All", children: [] },
+ require_user_presence: true,
+ constraints: { all: [] },
}),
);
@@ -304,6 +343,7 @@ describe("request/session hooks", () => {
rp_context: baseRpContext,
action_description: undefined,
bridge_url: undefined,
+ require_user_presence: true,
override_connect_base_url: undefined,
return_to: "idkit://callback?step=create",
environment: undefined,
@@ -328,7 +368,7 @@ describe("request/session hooks", () => {
rp_context: baseRpContext,
existing_session_id: validSessionId,
return_to: "idkit://callback?step=prove",
- constraints: { type: "All", children: [] },
+ constraints: { all: [] },
}),
);
@@ -345,6 +385,7 @@ describe("request/session hooks", () => {
rp_context: baseRpContext,
action_description: undefined,
bridge_url: undefined,
+ require_user_presence: false,
override_connect_base_url: undefined,
return_to: "idkit://callback?step=prove",
environment: undefined,
@@ -357,7 +398,7 @@ describe("request/session hooks", () => {
app_id: "app_test",
rp_context: baseRpContext,
existing_session_id: " " as unknown as `session_${string}`,
- constraints: { type: "All", children: [] },
+ constraints: { all: [] },
}),
);
@@ -378,7 +419,7 @@ describe("request/session hooks", () => {
app_id: "app_test",
rp_context: baseRpContext,
existing_session_id: "session_2" as `session_${string}`,
- constraints: { type: "All", children: [] },
+ constraints: { all: [] },
}),
);
diff --git a/js/packages/react/src/__tests__/widgets.test.tsx b/js/packages/react/src/__tests__/widgets.test.tsx
index a9faae2d..cdd5986e 100644
--- a/js/packages/react/src/__tests__/widgets.test.tsx
+++ b/js/packages/react/src/__tests__/widgets.test.tsx
@@ -76,7 +76,7 @@ function createRequestProps(
allow_legacy_proofs: false,
preset: { type: "OrbLegacy" },
...overrides,
- };
+ } as IDKitRequestWidgetProps;
}
function createSessionProps(
@@ -88,7 +88,7 @@ function createSessionProps(
onSuccess: vi.fn(),
app_id: "app_test",
rp_context: baseRpContext,
- preset: { type: "OrbLegacy" },
+ constraints: { all: [] },
...overrides,
};
}
diff --git a/js/packages/react/src/hooks/useIDKitRequest.ts b/js/packages/react/src/hooks/useIDKitRequest.ts
index 22ee05e8..916439ec 100644
--- a/js/packages/react/src/hooks/useIDKitRequest.ts
+++ b/js/packages/react/src/hooks/useIDKitRequest.ts
@@ -17,6 +17,7 @@ export function useIDKitRequest(
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,
});
diff --git a/js/packages/react/src/hooks/useIDKitSession.ts b/js/packages/react/src/hooks/useIDKitSession.ts
index 9e826128..97bd300c 100644
--- a/js/packages/react/src/hooks/useIDKitSession.ts
+++ b/js/packages/react/src/hooks/useIDKitSession.ts
@@ -36,6 +36,7 @@ export function useIDKitSession(
rp_context: config.rp_context,
action_description: config.action_description,
bridge_url: config.bridge_url,
+ require_user_presence: config.require_user_presence ?? false,
override_connect_base_url: config.override_connect_base_url,
return_to: config.return_to,
environment: config.environment,
@@ -45,6 +46,7 @@ export function useIDKitSession(
rp_context: config.rp_context,
action_description: config.action_description,
bridge_url: config.bridge_url,
+ require_user_presence: config.require_user_presence ?? false,
override_connect_base_url: config.override_connect_base_url,
return_to: config.return_to,
environment: config.environment,
diff --git a/kotlin/Examples/IDKitSampleApp/app/src/main/java/com/worldcoin/idkit/sample/MainActivity.kt b/kotlin/Examples/IDKitSampleApp/app/src/main/java/com/worldcoin/idkit/sample/MainActivity.kt
index 57957a5d..dae800be 100644
--- a/kotlin/Examples/IDKitSampleApp/app/src/main/java/com/worldcoin/idkit/sample/MainActivity.kt
+++ b/kotlin/Examples/IDKitSampleApp/app/src/main/java/com/worldcoin/idkit/sample/MainActivity.kt
@@ -346,6 +346,7 @@ private class SampleModel {
actionDescription = "Local Android sample",
bridgeUrl = null,
allowLegacyProofs = false,
+ requireUserPresence = false,
overrideConnectBaseUrl = null,
returnTo = returnToURL,
environment = when (environment) {
diff --git a/kotlin/README.md b/kotlin/README.md
index 06b56991..4da56b4d 100644
--- a/kotlin/README.md
+++ b/kotlin/README.md
@@ -84,6 +84,7 @@ val config = IDKitRequestConfig(
actionDescription = "Log in",
bridgeUrl = null,
allowLegacyProofs = false,
+ requireUserPresence = false,
overrideConnectBaseUrl = null,
returnTo = null,
environment = Environment.STAGING,
diff --git a/kotlin/bindings/src/main/kotlin/com/worldcoin/idkit/IdKit.kt b/kotlin/bindings/src/main/kotlin/com/worldcoin/idkit/IdKit.kt
index a4927d2e..1eacde7e 100644
--- a/kotlin/bindings/src/main/kotlin/com/worldcoin/idkit/IdKit.kt
+++ b/kotlin/bindings/src/main/kotlin/com/worldcoin/idkit/IdKit.kt
@@ -13,11 +13,10 @@ import uniffi.idkit_core.AppError
// import uniffi.idkit_core.CredentialRequest
// import uniffi.idkit_core.CredentialType
import uniffi.idkit_core.IdKitBuilder
-import uniffi.idkit_core.IdKitRequestConfig
+import uniffi.idkit_core.IdKitRequestConfig as NativeIDKitRequestConfig
import uniffi.idkit_core.IdKitRequestWrapper
import uniffi.idkit_core.IdKitResult
-// TODO: Re-enable when World ID 4.0 is live
-// import uniffi.idkit_core.IdKitSessionConfig
+import uniffi.idkit_core.IdKitSessionConfig as NativeIDKitSessionConfig
import uniffi.idkit_core.Preset
import uniffi.idkit_core.Signal
import uniffi.idkit_core.StatusWrapper
@@ -31,12 +30,62 @@ import uniffi.idkit_core.idkitResultToJson as nativeIdkitResultToJson
// import uniffi.idkit_core.proveSession as nativeProveSession
import uniffi.idkit_core.request as nativeRequest
-typealias IDKitRequestConfig = IdKitRequestConfig
-// TODO: Re-enable when World ID 4.0 is live
-// typealias IDKitSessionConfig = IdKitSessionConfig
typealias IDKitResult = IdKitResult
typealias RpContext = uniffi.idkit_core.RpContext
typealias Environment = uniffi.idkit_core.Environment
+typealias ConnectUrlMode = uniffi.idkit_core.ConnectUrlMode
+
+data class IDKitRequestConfig(
+ val appId: String,
+ val action: String,
+ val rpContext: RpContext,
+ val actionDescription: String? = null,
+ val bridgeUrl: String? = null,
+ val allowLegacyProofs: Boolean = false,
+ val requireUserPresence: Boolean = false,
+ val overrideConnectBaseUrl: String? = null,
+ val returnTo: String? = null,
+ val environment: Environment? = null,
+ val connectUrlMode: ConnectUrlMode? = null,
+) {
+ internal fun toNative(): NativeIDKitRequestConfig =
+ NativeIDKitRequestConfig(
+ appId = appId,
+ action = action,
+ rpContext = rpContext,
+ actionDescription = actionDescription,
+ bridgeUrl = bridgeUrl,
+ allowLegacyProofs = allowLegacyProofs,
+ requireUserPresence = requireUserPresence,
+ overrideConnectBaseUrl = overrideConnectBaseUrl,
+ returnTo = returnTo,
+ environment = environment,
+ connectUrlMode = connectUrlMode,
+ )
+}
+
+data class IDKitSessionConfig(
+ val appId: String,
+ val rpContext: RpContext,
+ val actionDescription: String? = null,
+ val bridgeUrl: String? = null,
+ val requireUserPresence: Boolean = false,
+ val overrideConnectBaseUrl: String? = null,
+ val returnTo: String? = null,
+ val environment: Environment? = null,
+) {
+ internal fun toNative(): NativeIDKitSessionConfig =
+ NativeIDKitSessionConfig(
+ appId = appId,
+ rpContext = rpContext,
+ actionDescription = actionDescription,
+ bridgeUrl = bridgeUrl,
+ requireUserPresence = requireUserPresence,
+ overrideConnectBaseUrl = overrideConnectBaseUrl,
+ returnTo = returnTo,
+ environment = environment,
+ )
+}
class IDKitClientError(message: String) : IllegalArgumentException(message)
@@ -54,6 +103,7 @@ enum class IDKitErrorCode(val rawValue: String) {
CONNECTION_FAILED("connection_failed"),
MAX_VERIFICATIONS_REACHED("max_verifications_reached"),
FAILED_BY_HOST_APP("failed_by_host_app"),
+ USER_PRESENCE_FAILED("user_presence_failed"),
INVALID_RP_SIGNATURE("invalid_rp_signature"),
NULLIFIER_REPLAYED("nullifier_replayed"),
DUPLICATE_NONCE("duplicate_nonce"),
@@ -82,6 +132,7 @@ enum class IDKitErrorCode(val rawValue: String) {
AppError.CONNECTION_FAILED -> CONNECTION_FAILED
AppError.MAX_VERIFICATIONS_REACHED -> MAX_VERIFICATIONS_REACHED
AppError.FAILED_BY_HOST_APP -> FAILED_BY_HOST_APP
+ AppError.USER_PRESENCE_FAILED -> USER_PRESENCE_FAILED
AppError.INVALID_RP_SIGNATURE -> INVALID_RP_SIGNATURE
AppError.NULLIFIER_REPLAYED -> NULLIFIER_REPLAYED
AppError.DUPLICATE_NONCE -> DUPLICATE_NONCE
@@ -201,19 +252,19 @@ object IDKit {
fun request(config: IDKitRequestConfig): IDKitBuilder {
require(config.appId.isNotBlank()) { "app_id is required" }
require(config.action.isNotBlank()) { "action is required" }
- return IDKitBuilder(nativeRequest(config))
+ return IDKitBuilder(nativeRequest(config.toNative()))
}
// TODO: Re-enable when World ID 4.0 is live
// fun createSession(config: IDKitSessionConfig): IDKitBuilder {
// require(config.appId.isNotBlank()) { "app_id is required" }
- // return IDKitBuilder(nativeCreateSession(config))
+ // return IDKitBuilder(nativeCreateSession(config.toNative()))
// }
// fun proveSession(sessionId: String, config: IDKitSessionConfig): IDKitBuilder {
// require(sessionId.isNotBlank()) { "session_id is required" }
// require(config.appId.isNotBlank()) { "app_id is required" }
- // return IDKitBuilder(nativeProveSession(sessionId, config))
+ // return IDKitBuilder(nativeProveSession(sessionId, config.toNative()))
// }
fun hashSignal(signal: String): String = hashSignalFfi(Signal.fromString(signal))
diff --git a/kotlin/bindings/src/test/kotlin/com/worldcoin/idkit/IDKitTests.kt b/kotlin/bindings/src/test/kotlin/com/worldcoin/idkit/IDKitTests.kt
index da50115e..c24f5165 100644
--- a/kotlin/bindings/src/test/kotlin/com/worldcoin/idkit/IDKitTests.kt
+++ b/kotlin/bindings/src/test/kotlin/com/worldcoin/idkit/IDKitTests.kt
@@ -16,7 +16,10 @@ import uniffi.idkit_core.RpContext
import uniffi.idkit_core.StatusWrapper
class IDKitTests {
- private fun sampleResult(sessionId: String? = null): IDKitResult =
+ private fun sampleResult(
+ sessionId: String? = null,
+ userPresenceCompleted: Boolean = false,
+ ): IDKitResult =
IDKitResult(
protocolVersion = "4.0",
nonce = "0x1234",
@@ -24,6 +27,7 @@ class IDKitTests {
actionDescription = "Sample action",
sessionId = sessionId,
responses = emptyList(),
+ userPresenceCompleted = userPresenceCompleted,
environment = "production",
)
@@ -47,6 +51,7 @@ class IDKitTests {
actionDescription = null,
bridgeUrl = null,
allowLegacyProofs = false,
+ requireUserPresence = false,
overrideConnectBaseUrl = null,
returnTo = null,
environment = Environment.STAGING,
@@ -58,6 +63,7 @@ class IDKitTests {
// rpContext = sampleRpContext(),
// actionDescription = null,
// bridgeUrl = null,
+ // requireUserPresence = false,
// overrideConnectBaseUrl = null,
// returnTo = null,
// environment = Environment.STAGING,
@@ -89,6 +95,10 @@ class IDKitTests {
IDKitStatus.Failed(IDKitErrorCode.INVALID_NETWORK),
IDKitRequest.mapStatus(StatusWrapper.Failed(AppError.INVALID_NETWORK)),
)
+ assertEquals(
+ IDKitStatus.Failed(IDKitErrorCode.USER_PRESENCE_FAILED),
+ IDKitRequest.mapStatus(StatusWrapper.Failed(AppError.USER_PRESENCE_FAILED)),
+ )
assertEquals(
IDKitStatus.Failed(IDKitErrorCode.INVALID_RP_SIGNATURE),
IDKitRequest.mapStatus(StatusWrapper.Failed(AppError.INVALID_RP_SIGNATURE)),
diff --git a/rust/core/src/bridge.rs b/rust/core/src/bridge.rs
index 5d4363a7..843f1ced 100644
--- a/rust/core/src/bridge.rs
+++ b/rust/core/src/bridge.rs
@@ -13,7 +13,10 @@ use crate::{
};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
-use world_id_primitives::{FieldElement, ProofRequest, SessionId};
+use world_id_primitives::{
+ FieldElement, ProofRequest, ProofResponse, RequestVersion,
+ ResponseItem as ProtocolResponseItem, SessionId,
+};
#[cfg(feature = "native-crypto")]
use crate::crypto::CryptoKey;
@@ -122,6 +125,9 @@ struct BridgeRequestPayload {
/// - `false`: Only accept v4 proofs. Use after migration cutoff or for new apps.
allow_legacy_proofs: bool,
+ /// Whether World App should require a user-presence check before verification.
+ require_user_presence: bool,
+
/// Environment for the bridge request
environment: Environment,
}
@@ -225,21 +231,76 @@ enum BridgeResponse {
Error { error_code: AppError },
/// World ID 4.0 protocol response
- ResponseV2(world_id_primitives::ProofResponse),
+ ResponseV2(BridgeResponseV2),
/// World ID 4.0 protocol response with extensions
ResponseV2_1 {
proof_response: world_id_primitives::ProofResponse,
identity_attested: Option,
+ #[serde(default)]
+ user_presence_completed: bool,
},
/// Multi-credential legacy: bridge sends v3 proofs via `legacy_responses`
MultiLegacyResponse {
legacy_responses: Vec,
+ #[serde(default)]
+ user_presence_completed: bool,
},
/// V1 legacy (old World App 3.0, single credential)
- ResponseV1(BridgeResponseV1),
+ ResponseV1(BridgeResponseV1WithMetadata),
+}
+
+#[derive(Debug, Deserialize)]
+struct BridgeResponseV2 {
+ /// The response id references request id
+ id: String,
+ /// Version corresponding to request version
+ version: RequestVersion,
+ /// Optional session identifier for session proofs
+ #[serde(default)]
+ session_id: Option,
+ /// Protocol-level proof response error
+ #[serde(default)]
+ error: Option,
+ /// Per-credential proof responses
+ responses: Vec,
+ /// Whether World App completed the requested user-presence check.
+ #[serde(default)]
+ user_presence_completed: bool,
+}
+
+impl BridgeResponseV2 {
+ fn into_proof_response(self) -> ProofResponse {
+ ProofResponse {
+ id: self.id,
+ version: self.version,
+ session_id: self.session_id,
+ error: self.error,
+ responses: self.responses,
+ }
+ }
+}
+
+#[derive(Debug, Deserialize)]
+struct BridgeResponseV1WithMetadata {
+ #[serde(flatten)]
+ response: BridgeResponseV1,
+ /// Whether World App completed the requested user-presence check.
+ #[serde(default)]
+ user_presence_completed: bool,
+}
+
+fn user_presence_failure_status(
+ require_user_presence: bool,
+ user_presence_completed: bool,
+) -> Option {
+ if require_user_presence && !user_presence_completed {
+ Some(Status::Failed(AppError::UserPresenceFailed))
+ } else {
+ None
+ }
}
/// Status of a verification request
@@ -271,6 +332,7 @@ pub struct BridgeConnectionParams {
pub legacy_signal: String,
pub bridge_url: Option,
pub allow_legacy_proofs: bool,
+ pub require_user_presence: bool,
/// Optional override for the connect base URL (e.g., for staging environments)
pub override_connect_base_url: Option,
/// Optional deep-link callback URL appended as `return_to` on the connector URL
@@ -360,6 +422,8 @@ pub struct BridgeConnection {
return_to: Option,
/// Resolved environment for this connection
environment: Environment,
+ /// Whether a successful response must prove user presence was completed.
+ require_user_presence: bool,
}
/// Builds a `BridgeRequestPayload` from params without connecting to the bridge.
@@ -459,6 +523,7 @@ pub fn build_request_payload(
signal: legacy_signal_hash,
timestamp,
allow_legacy_proofs: params.allow_legacy_proofs,
+ require_user_presence: params.require_user_presence,
environment: params.environment.unwrap_or_default(),
};
@@ -603,6 +668,7 @@ impl BridgeConnection {
override_connect_base_url: params.override_connect_base_url,
return_to: params.return_to,
environment: params.environment.unwrap_or_default(),
+ require_user_presence: params.require_user_presence,
})
}
@@ -681,14 +747,33 @@ impl BridgeConnection {
match bridge_response {
BridgeResponse::Error { error_code } => Ok(Status::Failed(error_code)),
- BridgeResponse::ResponseV2(proof_response) => {
- self.handle_bridge_v2_response(proof_response, None)
+ BridgeResponse::ResponseV2(response) => {
+ let user_presence_completed = response.user_presence_completed;
+ self.handle_bridge_v2_response(
+ response.into_proof_response(),
+ None,
+ user_presence_completed,
+ )
}
BridgeResponse::ResponseV2_1 {
proof_response,
identity_attested,
- } => self.handle_bridge_v2_response(proof_response, identity_attested),
- BridgeResponse::MultiLegacyResponse { legacy_responses } => {
+ user_presence_completed,
+ } => self.handle_bridge_v2_response(
+ proof_response,
+ identity_attested,
+ user_presence_completed,
+ ),
+ BridgeResponse::MultiLegacyResponse {
+ legacy_responses,
+ user_presence_completed,
+ } => {
+ if let Some(status) = user_presence_failure_status(
+ self.require_user_presence,
+ user_presence_completed,
+ ) {
+ return Ok(status);
+ }
let responses: Vec = legacy_responses
.into_iter()
.map(|item| {
@@ -707,20 +792,30 @@ impl BridgeConnection {
self.action.clone(),
self.action_description.clone(),
responses,
+ user_presence_completed,
self.environment.as_ref(),
)))
}
BridgeResponse::ResponseV1(response) => {
+ if let Some(status) = user_presence_failure_status(
+ self.require_user_presence,
+ response.user_presence_completed,
+ ) {
+ return Ok(status);
+ }
+
// V1 responses are always protocol 3.0
// For V1 we don't have identifier, use verification_level as key
let signal_hash = self.cached_signal_hashes.legacy();
- let item = response.into_response_item(signal_hash);
+ let user_presence_completed = response.user_presence_completed;
+ let item = response.response.into_response_item(signal_hash);
Ok(Status::Confirmed(IDKitResult::new(
"3.0",
self.nonce.clone(),
self.action.clone(),
self.action_description.clone(),
vec![item],
+ user_presence_completed,
self.environment.as_ref(),
)))
}
@@ -734,12 +829,19 @@ impl BridgeConnection {
&self,
proof_response: world_id_primitives::ProofResponse,
identity_attested: Option,
+ user_presence_completed: bool,
) -> Result {
- // Check for protocol-level error
+ // Protocol-level errors take precedence over user-presence enforcement.
if let Some(error_code) = proof_response.error.as_deref() {
return Ok(Status::Failed(AppError::from_code(error_code)));
}
+ if let Some(status) =
+ user_presence_failure_status(self.require_user_presence, user_presence_completed)
+ {
+ return Ok(status);
+ }
+
let responses: Vec = proof_response
.responses
.into_iter()
@@ -763,6 +865,7 @@ impl BridgeConnection {
.to_owned(),
self.action_description.clone(),
responses,
+ user_presence_completed,
self.environment.as_ref(),
)
} else {
@@ -772,6 +875,7 @@ impl BridgeConnection {
self.action.clone(),
self.action_description.clone(),
responses,
+ user_presence_completed,
self.environment.as_ref(),
);
result.identity_attested = identity_attested;
@@ -809,6 +913,8 @@ pub struct IDKitRequestConfig {
/// - `true`: Accept both v3 and v4 proofs. Use during migration.
/// - `false`: Only accept v4 proofs. Use after migration cutoff or for new apps.
pub allow_legacy_proofs: bool,
+ /// Optional user-presence requirement. Defaults to false when omitted.
+ pub require_user_presence: Option,
/// Optional override for the connect base URL (e.g., for staging environments)
pub override_connect_base_url: Option,
/// Optional deep-link callback URL appended as `return_to` on the connector URL
@@ -833,6 +939,8 @@ pub struct IDKitSessionConfig {
pub action_description: Option,
/// Optional bridge URL (defaults to production)
pub bridge_url: Option,
+ /// Optional user-presence requirement. Defaults to false when omitted.
+ pub require_user_presence: Option,
/// Optional override for the connect base URL (e.g., for staging environments)
pub override_connect_base_url: Option,
/// Optional deep-link callback URL appended as `return_to` on the connector URL
@@ -894,6 +1002,7 @@ impl IDKitConfig {
legacy_signal: String::new(),
bridge_url,
allow_legacy_proofs: config.allow_legacy_proofs,
+ require_user_presence: config.require_user_presence.unwrap_or(false),
override_connect_base_url: config.override_connect_base_url.clone(),
return_to: config.return_to.clone(),
environment: config.environment,
@@ -919,6 +1028,7 @@ impl IDKitConfig {
legacy_signal: String::new(),
bridge_url,
allow_legacy_proofs: false,
+ require_user_presence: config.require_user_presence.unwrap_or(false),
override_connect_base_url: config.override_connect_base_url.clone(),
return_to: config.return_to.clone(),
environment: config.environment,
@@ -946,6 +1056,7 @@ impl IDKitConfig {
legacy_signal: String::new(),
bridge_url,
allow_legacy_proofs: false,
+ require_user_presence: config.require_user_presence.unwrap_or(false),
override_connect_base_url: config.override_connect_base_url.clone(),
return_to: config.return_to.clone(),
environment: config.environment,
@@ -1006,6 +1117,7 @@ impl IDKitConfig {
legacy_signal: bridge_params.legacy_signal.unwrap_or_default(),
bridge_url,
allow_legacy_proofs,
+ require_user_presence: config.require_user_presence.unwrap_or(false),
override_connect_base_url: config.override_connect_base_url.clone(),
return_to: config.return_to.clone(),
environment: config.environment,
@@ -1030,6 +1142,7 @@ impl IDKitConfig {
legacy_signal: bridge_params.legacy_signal.unwrap_or_default(),
bridge_url,
allow_legacy_proofs: false,
+ require_user_presence: config.require_user_presence.unwrap_or(false),
override_connect_base_url: config.override_connect_base_url.clone(),
return_to: config.return_to.clone(),
environment: config.environment,
@@ -1056,6 +1169,7 @@ impl IDKitConfig {
legacy_signal: bridge_params.legacy_signal.unwrap_or_default(),
bridge_url,
allow_legacy_proofs: false,
+ require_user_presence: config.require_user_presence.unwrap_or(false),
override_connect_base_url: config.override_connect_base_url.clone(),
return_to: config.return_to.clone(),
environment: config.environment,
@@ -1369,6 +1483,7 @@ mod tests {
proof_request: Some(proof_request),
identity_attributes: None,
allow_legacy_proofs: false,
+ require_user_presence: false,
environment: Environment::Production,
};
@@ -1379,6 +1494,7 @@ mod tests {
assert!(json.contains("proof_request"));
assert!(json.contains("rp_1234567890abcdef"));
assert!(json.contains("allow_legacy_proofs"));
+ assert!(json.contains("require_user_presence"));
}
#[test]
@@ -1409,6 +1525,7 @@ mod tests {
legacy_signal: String::new(),
bridge_url: None,
allow_legacy_proofs: false,
+ require_user_presence: false,
override_connect_base_url: None,
return_to: None,
environment: Some(Environment::Production),
@@ -1541,10 +1658,10 @@ mod tests {
let response: BridgeResponse = serde_json::from_str(json).unwrap();
match response {
BridgeResponse::ResponseV1(v1) => {
- assert_eq!(v1.verification_level, VerificationLevel::Device);
- assert_eq!(v1.proof, "0xproof");
- assert_eq!(v1.merkle_root, "0xroot");
- assert_eq!(v1.nullifier_hash, "0xnull");
+ assert_eq!(v1.response.verification_level, VerificationLevel::Device);
+ assert_eq!(v1.response.proof, "0xproof");
+ assert_eq!(v1.response.merkle_root, "0xroot");
+ assert_eq!(v1.response.nullifier_hash, "0xnull");
}
other => panic!("Expected ResponseV1, got: {other:?}"),
}
@@ -1564,12 +1681,77 @@ mod tests {
let response: BridgeResponse = serde_json::from_str(json).unwrap();
match response {
BridgeResponse::ResponseV1(v1) => {
- assert_eq!(v1.verification_level, VerificationLevel::Orb);
+ assert_eq!(v1.response.verification_level, VerificationLevel::Orb);
}
other => panic!("Expected ResponseV1, got: {other:?}"),
}
}
+ #[test]
+ fn test_bridge_response_v2_user_presence_defaults_false() {
+ let json = r#"{
+ "id": "request-id",
+ "version": 1,
+ "responses": []
+ }"#;
+
+ let response: BridgeResponse = serde_json::from_str(json).unwrap();
+ match response {
+ BridgeResponse::ResponseV2(v2) => {
+ assert!(!v2.user_presence_completed);
+ }
+ other => panic!("Expected ResponseV2, got: {other:?}"),
+ }
+ }
+
+ #[test]
+ fn test_bridge_response_v2_user_presence_explicit_true() {
+ let json = r#"{
+ "id": "request-id",
+ "version": 1,
+ "responses": [],
+ "user_presence_completed": true
+ }"#;
+
+ let response: BridgeResponse = serde_json::from_str(json).unwrap();
+ match response {
+ BridgeResponse::ResponseV2(v2) => {
+ assert!(v2.user_presence_completed);
+ }
+ other => panic!("Expected ResponseV2, got: {other:?}"),
+ }
+ }
+
+ #[test]
+ fn test_required_user_presence_fails_when_not_completed() {
+ assert_eq!(
+ user_presence_failure_status(true, false),
+ Some(Status::Failed(AppError::UserPresenceFailed))
+ );
+ assert_eq!(user_presence_failure_status(true, true), None);
+ assert_eq!(user_presence_failure_status(false, false), None);
+ }
+
+ #[test]
+ fn test_protocol_error_takes_precedence_over_user_presence_failure() {
+ let mut connection = sample_connection(None);
+ connection.require_user_presence = true;
+
+ let proof_response = ProofResponse {
+ id: "req_failed".to_string(),
+ version: RequestVersion::V1,
+ session_id: None,
+ error: Some("invalid_rp_signature".to_string()),
+ responses: vec![],
+ };
+
+ let status = connection
+ .handle_bridge_v2_response(proof_response, None, false)
+ .unwrap();
+
+ assert_eq!(status, Status::Failed(AppError::InvalidRpSignature));
+ }
+
#[test]
fn test_bridge_response_error_deserialization() {
let json = r#"{"error_code": "user_rejected"}"#;
@@ -1696,8 +1878,12 @@ mod tests {
let response: BridgeResponse = serde_json::from_str(json).unwrap();
match response {
- BridgeResponse::MultiLegacyResponse { legacy_responses } => {
+ BridgeResponse::MultiLegacyResponse {
+ legacy_responses,
+ user_presence_completed,
+ } => {
assert_eq!(legacy_responses.len(), 1);
+ assert!(!user_presence_completed);
assert_eq!(
legacy_responses[0].verification_level,
VerificationLevel::Orb
@@ -1729,8 +1915,12 @@ mod tests {
let response: BridgeResponse = serde_json::from_str(json).unwrap();
match response {
- BridgeResponse::MultiLegacyResponse { legacy_responses } => {
+ BridgeResponse::MultiLegacyResponse {
+ legacy_responses,
+ user_presence_completed,
+ } => {
assert_eq!(legacy_responses.len(), 2);
+ assert!(!user_presence_completed);
assert_eq!(
legacy_responses[0].verification_level,
VerificationLevel::Orb
@@ -1789,6 +1979,39 @@ mod tests {
}
}
+ #[cfg(feature = "ffi")]
+ #[test]
+ fn test_request_config_defaults_user_presence_requirement_to_false() {
+ let signature = "0x".to_string() + &"00".repeat(64) + "1b";
+ let rp_context = RpContext::new(
+ "rp_1234567890abcdef",
+ "0x0000000000000000000000000000000000000000000000000000000000000001",
+ 1_700_000_000,
+ 1_700_003_600,
+ &signature,
+ )
+ .unwrap();
+ let config = IDKitConfig::Request(IDKitRequestConfig {
+ app_id: "app_test".to_string(),
+ action: "test-action".to_string(),
+ rp_context: std::sync::Arc::new(rp_context),
+ action_description: None,
+ bridge_url: None,
+ allow_legacy_proofs: false,
+ require_user_presence: None,
+ override_connect_base_url: None,
+ return_to: None,
+ environment: None,
+ connect_url_mode: None,
+ });
+
+ let params = config
+ .to_params(ConstraintNode::Any { any: Vec::new() })
+ .unwrap();
+
+ assert!(!params.require_user_presence);
+ }
+
#[test]
fn test_selfie_check_legacy_preset_serializes_face_verification_level() {
let preset = crate::preset::Preset::selfie_check_legacy(Some("face-signal".to_string()));
@@ -1819,6 +2042,7 @@ mod tests {
legacy_signal: bridge_params.legacy_signal.unwrap_or_default(),
bridge_url: None,
allow_legacy_proofs: false,
+ require_user_presence: false,
override_connect_base_url: None,
return_to: None,
@@ -1860,6 +2084,7 @@ mod tests {
legacy_signal: bridge_params.legacy_signal.unwrap_or_default(),
bridge_url: None,
allow_legacy_proofs: false,
+ require_user_presence: false,
override_connect_base_url: None,
return_to: None,
@@ -1923,6 +2148,7 @@ mod tests {
legacy_signal: "test-signal".to_string(),
bridge_url: None,
allow_legacy_proofs: false,
+ require_user_presence: false,
override_connect_base_url: None,
return_to: None,
@@ -1969,6 +2195,7 @@ mod tests {
legacy_signal: "test-signal".to_string(),
bridge_url: None,
allow_legacy_proofs: false,
+ require_user_presence: false,
override_connect_base_url: None,
return_to: None,
environment: None,
@@ -1978,10 +2205,66 @@ mod tests {
// native=true includes timestamp
let payload = build_request_payload(¶ms, true).unwrap();
assert_eq!(payload["timestamp"], "2023-11-14T22:13:20Z");
+ assert_eq!(payload["require_user_presence"], serde_json::json!(false));
// native=false omits timestamp
let payload_bridge = build_request_payload(¶ms, false).unwrap();
assert!(payload_bridge.get("timestamp").is_none());
+ assert_eq!(
+ payload_bridge["require_user_presence"],
+ serde_json::json!(false)
+ );
+ }
+
+ #[test]
+ fn test_build_request_payload_serializes_user_presence_requirement() {
+ let app_id = AppId::new("app_test").unwrap();
+ let sig = "0x".to_string() + &"00".repeat(64) + "1b";
+ let rp_context = RpContext::new(
+ "rp_1234567890abcdef",
+ "0x0000000000000000000000000000000000000000000000000000000000000001",
+ 1_700_000_000,
+ 1_700_003_600,
+ &sig,
+ )
+ .unwrap();
+
+ let item = CredentialRequest::new(
+ CredentialType::ProofOfHuman,
+ Some(Signal::from_string("test")),
+ );
+ let constraints = ConstraintNode::item(item);
+
+ let params = BridgeConnectionParams {
+ app_id,
+ kind: RequestKind::Uniqueness {
+ action: "my-action".to_string(),
+ },
+ constraints: Some(constraints),
+ rp_context,
+ action_description: None,
+ legacy_verification_level: VerificationLevel::Orb,
+ legacy_signal: "test-signal".to_string(),
+ bridge_url: None,
+ allow_legacy_proofs: false,
+ require_user_presence: true,
+ override_connect_base_url: None,
+ return_to: None,
+ environment: None,
+ identity_attributes: None,
+ };
+
+ let bridge_payload = build_request_payload(¶ms, false).unwrap();
+ assert_eq!(
+ bridge_payload["require_user_presence"],
+ serde_json::json!(true)
+ );
+
+ let native_payload = build_request_payload(¶ms, true).unwrap();
+ assert_eq!(
+ native_payload["require_user_presence"],
+ serde_json::json!(true)
+ );
}
#[test]
@@ -2015,6 +2298,7 @@ mod tests {
legacy_signal: address.to_string(),
bridge_url: None,
allow_legacy_proofs: false,
+ require_user_presence: false,
override_connect_base_url: None,
return_to: None,
@@ -2053,6 +2337,7 @@ mod tests {
override_connect_base_url: None,
return_to,
environment: Environment::Production,
+ require_user_presence: false,
}
}
diff --git a/rust/core/src/error.rs b/rust/core/src/error.rs
index b11ad3be..55c0c9d3 100644
--- a/rust/core/src/error.rs
+++ b/rust/core/src/error.rs
@@ -118,6 +118,10 @@ pub enum AppError {
#[error("Verification failed by host app")]
FailedByHostApp,
+ /// User presence check failed or was not completed
+ #[error("User presence check failed")]
+ UserPresenceFailed,
+
/// RP signature is invalid
#[error("Invalid RP signature")]
InvalidRpSignature,
@@ -178,6 +182,7 @@ impl AppError {
"connection_failed" => Self::ConnectionFailed,
"max_verifications_reached" => Self::MaxVerificationsReached,
"failed_by_host_app" => Self::FailedByHostApp,
+ "user_presence_failed" => Self::UserPresenceFailed,
"invalid_rp_signature" => Self::InvalidRpSignature,
"nullifier_replayed" => Self::NullifierReplayed,
"duplicate_nonce" => Self::DuplicateNonce,
diff --git a/rust/core/src/types.rs b/rust/core/src/types.rs
index faa9f589..24eea44d 100644
--- a/rust/core/src/types.rs
+++ b/rust/core/src/types.rs
@@ -711,6 +711,9 @@ pub struct IDKitResult {
/// Array of credential responses (always successful - errors at `BridgeResponse` level)
pub responses: Vec,
+ /// Whether World App completed the requested user-presence check.
+ pub user_presence_completed: bool,
+
/// The environment used for this request ("production" or "staging")
pub environment: String,
@@ -729,6 +732,7 @@ impl IDKitResult {
action: Option,
action_description: Option,
responses: Vec,
+ user_presence_completed: bool,
environment: impl Into,
) -> Self {
Self {
@@ -738,6 +742,7 @@ impl IDKitResult {
action_description,
session_id: None,
responses,
+ user_presence_completed,
environment: environment.into(),
identity_attested: None,
}
@@ -751,6 +756,7 @@ impl IDKitResult {
session_id: String,
action_description: Option,
responses: Vec,
+ user_presence_completed: bool,
environment: impl Into,
) -> Self {
Self {
@@ -760,6 +766,7 @@ impl IDKitResult {
action_description,
session_id: Some(session_id),
responses,
+ user_presence_completed,
environment: environment.into(),
identity_attested: None,
}
@@ -1353,6 +1360,7 @@ mod tests {
None,
None,
responses,
+ false,
"production",
);
assert_eq!(result.protocol_version, "3.0");
diff --git a/rust/core/src/wasm_bindings.rs b/rust/core/src/wasm_bindings.rs
index ac52f001..4fe8d6dc 100644
--- a/rust/core/src/wasm_bindings.rs
+++ b/rust/core/src/wasm_bindings.rs
@@ -478,6 +478,7 @@ enum IDKitConfigWasm {
action_description: Option,
bridge_url: Option,
allow_legacy_proofs: bool,
+ require_user_presence: bool,
override_connect_base_url: Option,
return_to: Option,
environment: Option,
@@ -487,6 +488,7 @@ enum IDKitConfigWasm {
rp_context: RpContext,
action_description: Option,
bridge_url: Option,
+ require_user_presence: bool,
override_connect_base_url: Option,
return_to: Option,
environment: Option,
@@ -497,6 +499,7 @@ enum IDKitConfigWasm {
rp_context: RpContext,
action_description: Option,
bridge_url: Option,
+ require_user_presence: bool,
override_connect_base_url: Option,
return_to: Option,
environment: Option,
@@ -517,6 +520,7 @@ impl IDKitConfigWasm {
action_description,
bridge_url,
allow_legacy_proofs,
+ require_user_presence,
override_connect_base_url,
return_to,
environment,
@@ -542,6 +546,7 @@ impl IDKitConfigWasm {
legacy_signal: String::new(),
bridge_url,
allow_legacy_proofs: *allow_legacy_proofs,
+ require_user_presence: *require_user_presence,
override_connect_base_url: override_connect_base_url.clone(),
return_to: return_to.clone(),
@@ -557,6 +562,7 @@ impl IDKitConfigWasm {
rp_context,
action_description,
bridge_url,
+ require_user_presence,
override_connect_base_url,
return_to,
environment,
@@ -580,6 +586,7 @@ impl IDKitConfigWasm {
legacy_signal: String::new(),
bridge_url,
allow_legacy_proofs: false,
+ require_user_presence: *require_user_presence,
override_connect_base_url: override_connect_base_url.clone(),
return_to: return_to.clone(),
@@ -596,6 +603,7 @@ impl IDKitConfigWasm {
rp_context,
action_description,
bridge_url,
+ require_user_presence,
override_connect_base_url,
return_to,
environment,
@@ -621,6 +629,7 @@ impl IDKitConfigWasm {
legacy_signal: String::new(),
bridge_url,
allow_legacy_proofs: false,
+ require_user_presence: *require_user_presence,
override_connect_base_url: override_connect_base_url.clone(),
return_to: return_to.clone(),
@@ -676,6 +685,7 @@ impl IDKitBuilderWasm {
action_description: Option,
bridge_url: Option,
allow_legacy_proofs: bool,
+ require_user_presence: bool,
override_connect_base_url: Option,
return_to: Option,
environment: Option,
@@ -688,6 +698,7 @@ impl IDKitBuilderWasm {
action_description,
bridge_url,
allow_legacy_proofs,
+ require_user_presence,
override_connect_base_url,
return_to,
environment,
@@ -703,6 +714,7 @@ impl IDKitBuilderWasm {
rp_context: RpContextWasm,
action_description: Option,
bridge_url: Option,
+ require_user_presence: bool,
override_connect_base_url: Option,
return_to: Option,
environment: Option,
@@ -713,6 +725,7 @@ impl IDKitBuilderWasm {
rp_context: rp_context.into_inner(),
action_description,
bridge_url,
+ require_user_presence,
override_connect_base_url,
return_to,
environment,
@@ -729,6 +742,7 @@ impl IDKitBuilderWasm {
rp_context: RpContextWasm,
action_description: Option,
bridge_url: Option,
+ require_user_presence: bool,
override_connect_base_url: Option,
return_to: Option,
environment: Option,
@@ -740,6 +754,7 @@ impl IDKitBuilderWasm {
rp_context: rp_context.into_inner(),
action_description,
bridge_url,
+ require_user_presence,
override_connect_base_url,
return_to,
environment,
@@ -952,6 +967,7 @@ pub fn request(
action_description: Option,
bridge_url: Option,
allow_legacy_proofs: bool,
+ require_user_presence: bool,
override_connect_base_url: Option,
return_to: Option,
environment: Option,
@@ -963,6 +979,7 @@ pub fn request(
action_description,
bridge_url,
allow_legacy_proofs,
+ require_user_presence,
override_connect_base_url,
return_to,
environment,
@@ -975,11 +992,13 @@ pub fn request(
/// `session_`.
#[must_use]
#[wasm_bindgen(js_name = createSession)]
+#[allow(clippy::too_many_arguments)]
pub fn create_session(
app_id: String,
rp_context: RpContextWasm,
action_description: Option,
bridge_url: Option,
+ require_user_presence: bool,
override_connect_base_url: Option,
return_to: Option,
environment: Option,
@@ -989,6 +1008,7 @@ pub fn create_session(
rp_context,
action_description,
bridge_url,
+ require_user_presence,
override_connect_base_url,
return_to,
environment,
@@ -1008,6 +1028,7 @@ pub fn prove_session(
rp_context: RpContextWasm,
action_description: Option,
bridge_url: Option,
+ require_user_presence: bool,
override_connect_base_url: Option,
return_to: Option,
environment: Option,
@@ -1018,6 +1039,7 @@ pub fn prove_session(
rp_context,
action_description,
bridge_url,
+ require_user_presence,
override_connect_base_url,
return_to,
environment,
@@ -1211,6 +1233,8 @@ export interface IDKitResultV3 {
action_description?: string;
/** Array of V3 credential responses */
responses: ResponseItemV3[];
+ /** Whether World App completed the requested user-presence check. */
+ user_presence_completed: boolean;
/** The environment used for this request ("production" or "staging") */
environment: string;
}
@@ -1227,6 +1251,8 @@ export interface IDKitResultV4 {
action_description?: string;
/** Array of V4 credential responses */
responses: ResponseItemV4[];
+ /** Whether World App completed the requested user-presence check. */
+ user_presence_completed: boolean;
/** The environment used for this request ("production" or "staging") */
environment: string;
}
@@ -1243,6 +1269,8 @@ export interface IDKitResultSession {
session_id: `session_${string}`;
/** Array of session credential responses */
responses: ResponseItemSession[];
+ /** Whether World App completed the requested user-presence check. */
+ user_presence_completed: boolean;
/** The environment used for this request ("production" or "staging") */
environment: string;
}
@@ -1267,6 +1295,8 @@ export interface 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;
}
/** RpContext for proof requests */
@@ -1298,6 +1328,7 @@ export type IDKitErrorCode =
| "connection_failed"
| "max_verifications_reached"
| "failed_by_host_app"
+ | "user_presence_failed"
| "invalid_rp_signature"
| "nullifier_replayed"
| "duplicate_nonce"
@@ -1409,6 +1440,7 @@ export function createSession(
rp_context: RpContextWasm,
action_description?: string,
bridge_url?: string,
+ require_user_presence?: boolean,
override_connect_base_url?: string,
return_to?: string,
environment?: string
@@ -1427,6 +1459,7 @@ export function proveSession(
rp_context: RpContextWasm,
action_description?: string,
bridge_url?: string,
+ require_user_presence?: boolean,
override_connect_base_url?: string,
return_to?: string,
environment?: string
@@ -1451,6 +1484,7 @@ mod tests {
action_description: None,
bridge_url: None,
allow_legacy_proofs: false,
+ require_user_presence: false,
override_connect_base_url: None,
return_to: Some("idkit://callback?step=request".to_string()),
environment: None,
@@ -1466,6 +1500,28 @@ mod tests {
);
}
+ #[test]
+ fn request_params_preserve_user_presence_requirement() {
+ let config = IDKitConfigWasm::Request {
+ app_id: "app_staging_test".to_string(),
+ action: "test-action".to_string(),
+ rp_context: sample_rp_context(),
+ action_description: None,
+ bridge_url: None,
+ allow_legacy_proofs: false,
+ require_user_presence: true,
+ override_connect_base_url: None,
+ return_to: None,
+ environment: None,
+ };
+
+ let params = config
+ .to_params(Some(ConstraintNode::Any { any: Vec::new() }))
+ .expect("request params");
+
+ assert!(params.require_user_presence);
+ }
+
#[test]
fn create_session_params_preserve_return_to() {
let config = IDKitConfigWasm::CreateSession {
@@ -1473,6 +1529,7 @@ mod tests {
rp_context: sample_rp_context(),
action_description: None,
bridge_url: None,
+ require_user_presence: false,
override_connect_base_url: None,
return_to: Some("idkit://callback?step=create".to_string()),
environment: None,
@@ -1496,6 +1553,7 @@ mod tests {
rp_context: sample_rp_context(),
action_description: None,
bridge_url: None,
+ require_user_presence: false,
override_connect_base_url: None,
return_to: Some("idkit://callback?step=prove".to_string()),
environment: None,
diff --git a/swift/Examples/BasicVerification.swift b/swift/Examples/BasicVerification.swift
index 478abfd6..22d774b7 100644
--- a/swift/Examples/BasicVerification.swift
+++ b/swift/Examples/BasicVerification.swift
@@ -27,6 +27,7 @@ func basicVerification() async throws {
actionDescription: "Example verification",
bridgeUrl: nil,
allowLegacyProofs: false,
+ requireUserPresence: false,
overrideConnectBaseUrl: nil,
returnTo: nil,
environment: .staging,
diff --git a/swift/Examples/IDKitSampleApp/IDKitSampleApp/ContentView.swift b/swift/Examples/IDKitSampleApp/IDKitSampleApp/ContentView.swift
index 38b94838..79fb84bb 100644
--- a/swift/Examples/IDKitSampleApp/IDKitSampleApp/ContentView.swift
+++ b/swift/Examples/IDKitSampleApp/IDKitSampleApp/ContentView.swift
@@ -175,6 +175,7 @@ final class SampleModel: ObservableObject {
actionDescription: "Local iOS sample",
bridgeUrl: nil,
allowLegacyProofs: false,
+ requireUserPresence: false,
overrideConnectBaseUrl: nil,
returnTo: returnToURL,
environment: {
diff --git a/swift/README.md b/swift/README.md
index 70171361..b2d2dc9a 100644
--- a/swift/README.md
+++ b/swift/README.md
@@ -38,6 +38,7 @@ let config = IDKitRequestConfig(
actionDescription: "Log in",
bridgeUrl: nil,
allowLegacyProofs: false,
+ requireUserPresence: false,
overrideConnectBaseUrl: nil,
returnTo: nil,
environment: .staging
diff --git a/swift/Sources/IDKit/IDKit.swift b/swift/Sources/IDKit/IDKit.swift
index 79d09a19..7aae410c 100644
--- a/swift/Sources/IDKit/IDKit.swift
+++ b/swift/Sources/IDKit/IDKit.swift
@@ -1,27 +1,127 @@
import Foundation
-public typealias IDKitRequestConfig = IdKitRequestConfig
-public typealias IDKitSessionConfig = IdKitSessionConfig
public typealias IDKitResult = IdKitResult
+/// Configuration for uniqueness proof requests.
+public struct IDKitRequestConfig {
+ public let appId: String
+ public let action: String
+ public let rpContext: RpContext
+ public let actionDescription: String?
+ public let bridgeUrl: String?
+ public let allowLegacyProofs: Bool
+ public let requireUserPresence: Bool
+ public let overrideConnectBaseUrl: String?
+ public let returnTo: String?
+ public let environment: Environment?
+ public let connectUrlMode: ConnectUrlMode?
+
+ public init(
+ appId: String,
+ action: String,
+ rpContext: RpContext,
+ actionDescription: String? = nil,
+ bridgeUrl: String? = nil,
+ allowLegacyProofs: Bool = false,
+ requireUserPresence: Bool = false,
+ overrideConnectBaseUrl: String? = nil,
+ returnTo: String? = nil,
+ environment: Environment? = nil,
+ connectUrlMode: ConnectUrlMode? = nil
+ ) {
+ self.appId = appId
+ self.action = action
+ self.rpContext = rpContext
+ self.actionDescription = actionDescription
+ self.bridgeUrl = bridgeUrl
+ self.allowLegacyProofs = allowLegacyProofs
+ self.requireUserPresence = requireUserPresence
+ self.overrideConnectBaseUrl = overrideConnectBaseUrl
+ self.returnTo = returnTo
+ self.environment = environment
+ self.connectUrlMode = connectUrlMode
+ }
+
+ fileprivate var native: IdKitRequestConfig {
+ IdKitRequestConfig(
+ appId: appId,
+ action: action,
+ rpContext: rpContext,
+ actionDescription: actionDescription,
+ bridgeUrl: bridgeUrl,
+ allowLegacyProofs: allowLegacyProofs,
+ requireUserPresence: requireUserPresence,
+ overrideConnectBaseUrl: overrideConnectBaseUrl,
+ returnTo: returnTo,
+ environment: environment,
+ connectUrlMode: connectUrlMode
+ )
+ }
+}
+
+/// Configuration for session requests.
+public struct IDKitSessionConfig {
+ public let appId: String
+ public let rpContext: RpContext
+ public let actionDescription: String?
+ public let bridgeUrl: String?
+ public let requireUserPresence: Bool
+ public let overrideConnectBaseUrl: String?
+ public let returnTo: String?
+ public let environment: Environment?
+
+ public init(
+ appId: String,
+ rpContext: RpContext,
+ actionDescription: String? = nil,
+ bridgeUrl: String? = nil,
+ requireUserPresence: Bool = false,
+ overrideConnectBaseUrl: String? = nil,
+ returnTo: String? = nil,
+ environment: Environment? = nil
+ ) {
+ self.appId = appId
+ self.rpContext = rpContext
+ self.actionDescription = actionDescription
+ self.bridgeUrl = bridgeUrl
+ self.requireUserPresence = requireUserPresence
+ self.overrideConnectBaseUrl = overrideConnectBaseUrl
+ self.returnTo = returnTo
+ self.environment = environment
+ }
+
+ fileprivate var native: IdKitSessionConfig {
+ IdKitSessionConfig(
+ appId: appId,
+ rpContext: rpContext,
+ actionDescription: actionDescription,
+ bridgeUrl: bridgeUrl,
+ requireUserPresence: requireUserPresence,
+ overrideConnectBaseUrl: overrideConnectBaseUrl,
+ returnTo: returnTo,
+ environment: environment
+ )
+ }
+}
+
/// Main entry point for IDKit Swift SDK.
public enum IDKit {
public static let version = "4.0.8"
/// Creates a builder for uniqueness proof requests.
public static func request(config: IDKitRequestConfig) -> IDKitBuilder {
- IDKitBuilder(inner: IdKitBuilder.fromRequest(config: config))
+ IDKitBuilder(inner: IdKitBuilder.fromRequest(config: config.native))
}
// TODO: Re-enable when World ID 4.0 is live
// /// Creates a builder for creating a new session.
// public static func createSession(config: IDKitSessionConfig) -> IDKitBuilder {
- // IDKitBuilder(inner: IdKitBuilder.fromCreateSession(config: config))
+ // IDKitBuilder(inner: IdKitBuilder.fromCreateSession(config: config.native))
// }
// /// Creates a builder for proving an existing session.
// public static func proveSession(sessionId: String, config: IDKitSessionConfig) -> IDKitBuilder {
- // IDKitBuilder(inner: IdKitBuilder.fromProveSession(sessionId: sessionId, config: config))
+ // IDKitBuilder(inner: IdKitBuilder.fromProveSession(sessionId: sessionId, config: config.native))
// }
/// Hashes a string signal to the canonical 0x-prefixed field element string.
@@ -99,6 +199,7 @@ public enum IDKitErrorCode: String, Equatable {
case connectionFailed = "connection_failed"
case maxVerificationsReached = "max_verifications_reached"
case failedByHostApp = "failed_by_host_app"
+ case userPresenceFailed = "user_presence_failed"
case invalidRpSignature = "invalid_rp_signature"
case nullifierReplayed = "nullifier_replayed"
case duplicateNonce = "duplicate_nonce"
@@ -140,6 +241,8 @@ public enum IDKitErrorCode: String, Equatable {
.maxVerificationsReached
case .failedByHostApp:
.failedByHostApp
+ case .userPresenceFailed:
+ .userPresenceFailed
case .invalidRpSignature:
.invalidRpSignature
case .nullifierReplayed:
diff --git a/swift/Tests/IDKitTests/IDKitTests.swift b/swift/Tests/IDKitTests/IDKitTests.swift
index e1628e14..ba96a0ba 100644
--- a/swift/Tests/IDKitTests/IDKitTests.swift
+++ b/swift/Tests/IDKitTests/IDKitTests.swift
@@ -17,7 +17,7 @@ private actor StatusPoller {
}
}
-private func sampleResult(sessionId: String? = nil) -> IDKitResult {
+private func sampleResult(sessionId: String? = nil, userPresenceCompleted: Bool = false) -> IDKitResult {
IDKitResult(
protocolVersion: "4.0",
nonce: "0x1234",
@@ -25,6 +25,7 @@ private func sampleResult(sessionId: String? = nil) -> IDKitResult {
actionDescription: "Sample action",
sessionId: sessionId,
responses: [],
+ userPresenceCompleted: userPresenceCompleted,
environment: "production",
identityAttested: nil
)
@@ -50,6 +51,7 @@ func idkitEntrypoints() throws {
actionDescription: nil,
bridgeUrl: nil,
allowLegacyProofs: false,
+ requireUserPresence: false,
overrideConnectBaseUrl: nil,
returnTo: nil,
environment: nil,
@@ -62,6 +64,7 @@ func idkitEntrypoints() throws {
// rpContext: try sampleRpContext(),
// actionDescription: nil,
// bridgeUrl: nil,
+ // requireUserPresence: false,
// overrideConnectBaseUrl: nil,
// returnTo: nil,
// environment: nil
@@ -83,6 +86,7 @@ func statusMapping() {
#expect(IDKitRequest.mapStatus(.awaitingConfirmation) == .awaitingConfirmation)
#expect(IDKitRequest.mapStatus(.confirmed(result: result)) == .confirmed(result))
#expect(IDKitRequest.mapStatus(.failed(error: .invalidNetwork)) == .failed(.invalidNetwork))
+ #expect(IDKitRequest.mapStatus(.failed(error: .userPresenceFailed)) == .failed(.userPresenceFailed))
#expect(IDKitRequest.mapStatus(.failed(error: .invalidRpSignature)) == .failed(.invalidRpSignature))
#expect(IDKitRequest.mapStatus(.failed(error: .nullifierReplayed)) == .failed(.nullifierReplayed))
#expect(IDKitRequest.mapStatus(.failed(error: .duplicateNonce)) == .failed(.duplicateNonce))