From 0ee2ddc10289d465a36ee0a77994ee24710c07d5 Mon Sep 17 00:00:00 2001 From: Takis Kakalis <80459599+Takaros999@users.noreply.github.com> Date: Wed, 21 Oct 2026 20:42:44 +0000 Subject: [PATCH] fix(sdk): add explicit session creation flags --- js/examples/nextjs/app/ui.tsx | 4 +- js/packages/core/src/request.ts | 17 +++++- .../core/src/transports/bridge.test.ts | 58 +++++++++++++++++-- .../core/src/transports/native.test.ts | 20 ++++--- js/packages/core/src/types/config.ts | 4 +- .../react/src/__tests__/hooks.test.tsx | 6 +- .../src/hooks/useIDKitInviteCodeRequest.ts | 1 + .../react/src/hooks/useIDKitRequest.ts | 1 + .../main/kotlin/com/worldcoin/idkit/IdKit.kt | 25 +++++++- .../kotlin/com/worldcoin/idkit/IDKitTests.kt | 12 +++- swift/Sources/IDKit/IDKit.swift | 10 +++- swift/Tests/IDKitTests/IDKitTests.swift | 13 +++-- 12 files changed, 142 insertions(+), 29 deletions(-) diff --git a/js/examples/nextjs/app/ui.tsx b/js/examples/nextjs/app/ui.tsx index d2b8ac15..3b6f1a45 100644 --- a/js/examples/nextjs/app/ui.tsx +++ b/js/examples/nextjs/app/ui.tsx @@ -1957,7 +1957,7 @@ export function DemoClient(): ReactElement { app_id={APP_ID} // allow empty string action as input action={action ?? "test-action"} - session_id={shouldCreateSession ? "create" : undefined} + create_session={shouldCreateSession} rp_context={widgetRpContext} allow_legacy_proofs={shouldAllowLegacyProofs} require_user_presence={requireUserPresence} @@ -1982,7 +1982,7 @@ export function DemoClient(): ReactElement { app_id={APP_ID} // allow empty string action as input action={action ?? "test-action"} - session_id={shouldCreateSession ? "create" : undefined} + create_session={shouldCreateSession} rp_context={widgetRpContext} allow_legacy_proofs={shouldAllowLegacyProofs} require_user_presence={requireUserPresence} diff --git a/js/packages/core/src/request.ts b/js/packages/core/src/request.ts index dd242c6d..f85d5b6a 100644 --- a/js/packages/core/src/request.ts +++ b/js/packages/core/src/request.ts @@ -893,6 +893,19 @@ class IDKitInviteCodeBuilder { // Entry points // ───────────────────────────────────────────────────────────────────────────── +function sessionCreationOption( + config: IDKitRequestConfig, +): "create" | undefined { + if ( + config.create_session !== undefined && + typeof config.create_session !== "boolean" + ) { + throw new Error("create_session must be a boolean"); + } + if (config.session_id !== undefined) return config.session_id; + return config.create_session ? "create" : undefined; +} + /** * Creates an IDKit verification request builder * @@ -960,7 +973,7 @@ function createRequestForNamespace( package_name: options.package_name, package_version: options.package_version, action: String(config.action), - session_id: config.session_id, + session_id: sessionCreationOption(config), rp_context: config.rp_context, action_description: config.action_description, bridge_url: config.bridge_url, @@ -1026,7 +1039,7 @@ function createRequestWithInviteCodeForNamespace( package_name: options.package_name, package_version: options.package_version, action: String(config.action), - session_id: config.session_id, + session_id: sessionCreationOption(config), rp_context: config.rp_context, action_description: config.action_description, bridge_url: config.bridge_url, diff --git a/js/packages/core/src/transports/bridge.test.ts b/js/packages/core/src/transports/bridge.test.ts index 69d5c656..405b6241 100644 --- a/js/packages/core/src/transports/bridge.test.ts +++ b/js/packages/core/src/transports/bridge.test.ts @@ -161,9 +161,59 @@ describe("bridge raw JSON compatibility", () => { }); describe("encrypted bridge lifecycle", () => { - it.each([false, true])( - "creates and validates a bound uniqueness proof over the encrypted bridge (invite=%s)", - async (invite) => { + it.each([IDKit.request, IDKit.requestWithInviteCode])( + "%s preserves ordinary uniqueness and rejects invalid session options", + async (request) => { + configureIDKitRuntime({ + fetch: async (_url, init) => + json({ + request_id: + JSON.parse(init?.body as string).request_id ?? "request-id", + }), + }); + for (const create_session of [undefined, false]) { + const result = await request({ ...config, create_session }).preset( + proofOfHuman(), + ); + expect(result.getDebugReport().request_payload).toMatchObject({ + proof_request: { proof_type: "uniqueness", session_id: null }, + }); + } + for (const create_session of ["true", 1, null]) { + expect(() => + request({ + ...config, + create_session, + } as unknown as IDKitRequestConfig), + ).toThrow("create_session must be a boolean"); + } + for (const session_id of ["", SESSION, null]) { + await expect( + request({ + ...config, + create_session: true, + session_id, + } as unknown as IDKitRequestConfig).preset(proofOfHuman()), + ).rejects.toThrow('Uniqueness session_id must be "create"'); + } + await expect( + request({ + ...config, + create_session: true, + allow_legacy_proofs: true, + }).preset(proofOfHuman()), + ).rejects.toThrow("allow_legacy_proofs: false"); + }, + ); + + it.each([ + [false, { create_session: true }], + [true, { create_session: true }], + [false, { session_id: "create" as const }], + [true, { create_session: false, session_id: "create" as const }], + ] as const)( + "creates and validates a bound uniqueness proof over the encrypted bridge (invite=%s, options=%j)", + async (invite, sessionOptions) => { let pollBody: unknown = { status: "initialized" }; configureIDKitRuntime({ fetch: async (_url, init) => { @@ -176,7 +226,7 @@ describe("encrypted bridge lifecycle", () => { return json(pollBody); }, }); - const boundConfig = { ...config, session_id: "create" as const }; + const boundConfig = { ...config, ...sessionOptions }; const request = await (invite ? IDKit.requestWithInviteCode(boundConfig).preset(proofOfHuman()) : IDKit.request(boundConfig).constraints({ type: "proof_of_human" })); diff --git a/js/packages/core/src/transports/native.test.ts b/js/packages/core/src/transports/native.test.ts index bd2b3f27..1dfce2e7 100644 --- a/js/packages/core/src/transports/native.test.ts +++ b/js/packages/core/src/transports/native.test.ts @@ -5,7 +5,8 @@ import type { BuilderConfig } from "./native"; import { createNativeRequest, getWorldAppVerifyVersion } from "./native"; import { hashSignal } from "../lib/hashing"; import { setDebug } from "../lib/debug"; -import { FIELD, SESSION, legacy, response } from "../conformance/cases"; +import { FIELD, SESSION, config, legacy, response } from "../conformance/cases"; +import { IDKit, proofOfHuman } from "../index"; const baseConfig: BuilderConfig = { type: "request", @@ -90,12 +91,17 @@ describe("native transport request lifecycle", () => { it.each(["bound", "missing-session", "legacy"])( "validates bound uniqueness through the native event channel (%s)", async (variant) => { - const req = createNativeRequest( - {}, - { ...baseConfig, session_id: "create" }, - {}, - "", - ); + (globalThis as any).window.WorldApp = { + supported_commands: [{ name: "verify", supported_versions: [2] }], + }; + const req = await IDKit.request({ + ...config, + app_id: "app_staging_test", + create_session: true, + }).preset(proofOfHuman()); + expect(req.getDebugReport().request_payload).toMatchObject({ + proof_request: { proof_type: "uniqueness", session_id: "create" }, + }); activeRequest = req; const completion = req.pollUntilCompletion({ timeout: 1000 }); miniKitHandlers["miniapp-verify-action"]?.({ diff --git a/js/packages/core/src/types/config.ts b/js/packages/core/src/types/config.ts index 789960fd..710785b1 100644 --- a/js/packages/core/src/types/config.ts +++ b/js/packages/core/src/types/config.ts @@ -40,7 +40,9 @@ export type IDKitRequestConfig = { app_id: `app_${string}`; /** Identifier for the action the user is performing. Should be left blank for [Sign in with Worldcoin](https://docs.world.org/id/sign-in). */ action: AbiEncodedValue | string; - /** Create a session bound to this uniqueness proof. Requires allow_legacy_proofs: false. */ + /** Create a session bound to this uniqueness proof. Defaults to false; requires allow_legacy_proofs: false. */ + create_session?: boolean; + /** @deprecated Use create_session instead. When supplied, this alias still requests session creation. */ session_id?: "create"; /** RP context for protocol-level proof requests (required) */ rp_context: RpContext; diff --git a/js/packages/react/src/__tests__/hooks.test.tsx b/js/packages/react/src/__tests__/hooks.test.tsx index 04a82a64..a70cb166 100644 --- a/js/packages/react/src/__tests__/hooks.test.tsx +++ b/js/packages/react/src/__tests__/hooks.test.tsx @@ -175,6 +175,7 @@ describe("request/session hooks", () => { expect(requestMock).toHaveBeenCalledWith({ app_id: "app_test", action: "test-action", + create_session: undefined, session_id: undefined, rp_context: baseRpContext, action_description: undefined, @@ -205,7 +206,7 @@ describe("request/session hooks", () => { useRequest({ app_id: "app_test", action: "signup", - session_id: "create", + create_session: true, rp_context: baseRpContext, allow_legacy_proofs: false, preset: { type: "SelfieCheck" }, @@ -216,7 +217,7 @@ describe("request/session hooks", () => { await waitFor(() => expect(result.current.isSuccess).toBe(true)); expect(request).toHaveBeenCalledWith( - expect.objectContaining({ action: "signup", session_id: "create" }), + expect.objectContaining({ action: "signup", create_session: true }), ); expect(result.current.result).toEqual(response); }, @@ -337,6 +338,7 @@ describe("request/session hooks", () => { expect(requestMock).toHaveBeenCalledWith({ app_id: "app_test", action: "test-action", + create_session: undefined, session_id: undefined, rp_context: baseRpContext, action_description: undefined, diff --git a/js/packages/react/src/hooks/useIDKitInviteCodeRequest.ts b/js/packages/react/src/hooks/useIDKitInviteCodeRequest.ts index 351e66e1..3ec461c1 100644 --- a/js/packages/react/src/hooks/useIDKitInviteCodeRequest.ts +++ b/js/packages/react/src/hooks/useIDKitInviteCodeRequest.ts @@ -14,6 +14,7 @@ export function useIDKitInviteCodeRequest( const builder = IDKit.requestWithInviteCode({ app_id: config.app_id, action: config.action, + create_session: config.create_session, session_id: config.session_id, rp_context: config.rp_context, action_description: config.action_description, diff --git a/js/packages/react/src/hooks/useIDKitRequest.ts b/js/packages/react/src/hooks/useIDKitRequest.ts index 3fc0fba9..de16ef09 100644 --- a/js/packages/react/src/hooks/useIDKitRequest.ts +++ b/js/packages/react/src/hooks/useIDKitRequest.ts @@ -13,6 +13,7 @@ export function useIDKitRequest( const builder = IDKit.request({ app_id: config.app_id, action: config.action, + create_session: config.create_session, session_id: config.session_id, rp_context: config.rp_context, action_description: config.action_description, 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 9bed878c..b86fcd29 100644 --- a/kotlin/bindings/src/main/kotlin/com/worldcoin/idkit/IdKit.kt +++ b/kotlin/bindings/src/main/kotlin/com/worldcoin/idkit/IdKit.kt @@ -58,9 +58,32 @@ data class IDKitRequestConfig( val returnTo: String? = null, val environment: Environment? = null, val connectUrlMode: ConnectUrlMode? = null, - /** Set to "create" to bind a new session to the uniqueness proof. */ + /** Deprecated: use [createSession] instead. Only "create" is accepted. */ val sessionId: String? = null, ) { + /** Whether the uniqueness proof creates a session. */ + val createSession: Boolean get() = sessionId == "create" + + constructor( + appId: String, + action: String, + rpContext: RpContext, + actionDescription: String? = null, + bridgeUrl: String? = null, + allowLegacyProofs: Boolean = false, + requireUserPresence: Boolean = false, + overrideConnectBaseUrl: String? = null, + returnTo: String? = null, + environment: Environment? = null, + connectUrlMode: ConnectUrlMode? = null, + sessionId: String? = null, + createSession: Boolean, + ) : this( + appId, action, rpContext, actionDescription, bridgeUrl, allowLegacyProofs, + requireUserPresence, overrideConnectBaseUrl, returnTo, environment, connectUrlMode, + sessionId ?: if (createSession) "create" else null, + ) + internal fun toNative(): NativeIDKitRequestConfig = NativeIDKitRequestConfig( appId = appId, 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 f6429fcb..0c957634 100644 --- a/kotlin/bindings/src/test/kotlin/com/worldcoin/idkit/IDKitTests.kt +++ b/kotlin/bindings/src/test/kotlin/com/worldcoin/idkit/IDKitTests.kt @@ -136,6 +136,7 @@ class IDKitTests { val proofRequest = payload.proofRequest!! assertEquals(1u, proofRequest.version) assertEquals("uniqueness", proofRequest.proofType) + assertNull(proofRequest.sessionId) assertEquals("rp_1234567890abcdef", proofRequest.rpId) assertEquals(1_700_000_000u, proofRequest.createdAt) assertEquals(1_700_003_600u, proofRequest.expiresAt) @@ -197,13 +198,18 @@ class IDKitTests { @Test fun `uniqueness requests optionally create a session without legacy fallback`() { - for (sessionId in listOf(null, "create")) { + for ((sessionId, createSession, expectedSessionId) in listOf( + Triple(null, false, null), Triple(null, true, "create"), + Triple("create", false, "create"), Triple("create", true, "create"), + )) { val config = IDKitRequestConfig( appId = "app_staging_1234567890abcdef", action = "test-action", rpContext = sampleRpContext(), sessionId = sessionId, + createSession = createSession, ) + assertEquals(expectedSessionId != null, config.copy().createSession) val payload = IDKit.createBridgePayloadFromPresets( config, identityCheck(attributes = listOf(IdentityAttribute.MinimumAge(18u))), ) @@ -211,8 +217,8 @@ class IDKitTests { assertEquals("test-action", payload.action) assertEquals("uniqueness", proofRequest.proofType) assertEquals(IDKit.hashSignal("test-action"), proofRequest.action) - assertEquals(sessionId, proofRequest.sessionId) - assertEquals(sessionId == null, payload.allowLegacyProofs) + assertEquals(expectedSessionId, proofRequest.sessionId) + assertEquals(expectedSessionId == null, payload.allowLegacyProofs) } } diff --git a/swift/Sources/IDKit/IDKit.swift b/swift/Sources/IDKit/IDKit.swift index c5f49cb8..b989cd3f 100644 --- a/swift/Sources/IDKit/IDKit.swift +++ b/swift/Sources/IDKit/IDKit.swift @@ -24,8 +24,10 @@ public struct IDKitRequestConfig { public let returnTo: String? public let environment: Environment? public let connectUrlMode: ConnectUrlMode? - /// Set to "create" to bind a new session to the uniqueness proof. + /// Deprecated: use `createSession` instead. Only "create" is accepted. public let sessionId: String? + /// Bind a new session to the uniqueness proof. + public let createSession: Bool public init( appId: String, @@ -39,7 +41,8 @@ public struct IDKitRequestConfig { returnTo: String? = nil, environment: Environment? = nil, connectUrlMode: ConnectUrlMode? = nil, - sessionId: String? = nil + sessionId: String? = nil, + createSession: Bool = false ) { self.appId = appId self.action = action @@ -53,6 +56,7 @@ public struct IDKitRequestConfig { self.environment = environment self.connectUrlMode = connectUrlMode self.sessionId = sessionId + self.createSession = createSession } fileprivate var native: IdKitRequestConfig { @@ -70,7 +74,7 @@ public struct IDKitRequestConfig { returnTo: returnTo, environment: environment, connectUrlMode: connectUrlMode, - sessionId: sessionId + sessionId: sessionId ?? (createSession ? "create" : nil) ) } } diff --git a/swift/Tests/IDKitTests/IDKitTests.swift b/swift/Tests/IDKitTests/IDKitTests.swift index 33fe4bc5..8b3ce172 100644 --- a/swift/Tests/IDKitTests/IDKitTests.swift +++ b/swift/Tests/IDKitTests/IDKitTests.swift @@ -129,6 +129,7 @@ func bridgeRequestPayloadIdentityCheck() throws { let proofRequest = try #require(payload.proofRequest) #expect(proofRequest.version == 1) #expect(proofRequest.proofType == "uniqueness") + #expect(proofRequest.sessionId == nil) #expect(proofRequest.rpId == "rp_1234567890abcdef") #expect(proofRequest.createdAt == 1_700_000_000) #expect(proofRequest.expiresAt == 1_700_003_600) @@ -185,12 +186,16 @@ func bridgeRequestPayloadFromConstraints() throws { @Test("uniqueness requests optionally create a session without legacy fallback") func uniquenessSessionPayload() throws { - for sessionId in [nil, "create"] as [String?] { + for (sessionId, createSession, expectedSessionId) in [ + (nil, false, nil), (nil, true, "create"), + ("create", false, "create"), ("create", true, "create") + ] as [(String?, Bool, String?)] { let config = IDKitRequestConfig( appId: "app_staging_1234567890abcdef", action: "test-action", rpContext: try sampleRpContext(), - sessionId: sessionId + sessionId: sessionId, + createSession: createSession ) let payload = try IDKit.createBridgePayloadFromPresets( config: config, preset: identityCheck(attributes: [.minimumAge(18)]) @@ -199,8 +204,8 @@ func uniquenessSessionPayload() throws { #expect(payload.action == "test-action") #expect(proofRequest.proofType == "uniqueness") #expect(proofRequest.action == IDKit.hashSignal("test-action")) - #expect(proofRequest.sessionId == sessionId) - #expect(payload.allowLegacyProofs == (sessionId == nil)) + #expect(proofRequest.sessionId == expectedSessionId) + #expect(payload.allowLegacyProofs == (expectedSessionId == nil)) } }