diff --git a/packages/core/src/bridge.ts b/packages/core/src/bridge.ts index 742043e2..043f4403 100644 --- a/packages/core/src/bridge.ts +++ b/packages/core/src/bridge.ts @@ -2,10 +2,10 @@ import { create } from 'zustand' import { type IDKitConfig } from '@/types/config' import { VerificationState } from '@/types/bridge' import type { ISuccessResult } from '@/types/result' -import type { CredentialType } from '@/types/config' import { validate_bridge_url } from './lib/validation' import { encodeAction, generateSignal } from '@/lib/hashing' import { AppErrorCodes, ResponseStatus } from '@/types/bridge' +import type { BridgeConfig, CredentialType } from '@/types/config' import { decryptResponse, encryptRequest, exportKey, generateKey } from '@/lib/crypto' import { DEFAULT_VERIFICATION_LEVEL, @@ -26,148 +26,151 @@ type BridgeResponse = response: { iv: string; payload: string } } -type BridgeResult = - | ISuccessResult - | (Omit & { credential_type: CredentialType }) - | { error_code: AppErrorCodes } +type BridgeResult = { error_code: AppErrorCodes } | (Success & { error_code: never }) -export type WorldBridgeStore = { +export type IBridgeStore = { bridge_url: string iv: Uint8Array | null key: CryptoKey | null + result: Response | null requestId: string | null connectorURI: string | null - result: ISuccessResult | null errorCode: AppErrorCodes | null verificationState: VerificationState - createClient: (config: IDKitConfig) => Promise + createClient: (config: BridgeConfig & Request) => Promise pollForUpdates: () => Promise reset: () => void } -export const useWorldBridgeStore = create((set, get) => ({ - iv: null, - key: null, - result: null, - errorCode: null, - requestId: null, - connectorURI: null, - bridge_url: DEFAULT_BRIDGE_URL, - verificationState: VerificationState.PreparingClient, - - createClient: async ({ bridge_url, app_id, verification_level, action_description, action, signal }) => { - const { key, iv } = await generateKey() - - if (bridge_url) { - const validation = validate_bridge_url(bridge_url, app_id.includes('staging')) - if (!validation.valid) { - console.error(validation.errors.join('\n')) - set({ verificationState: VerificationState.Failed }) - throw new Error('Invalid bridge_url. Please check the console for more details.') +export const buildBridgeStore = , Response>( + preprocessRequest?: (request: Omit) => Record, + preprocessResponse?: (response: Response) => Response +) => + create>((set, get) => ({ + iv: null, + key: null, + result: null, + errorCode: null, + requestId: null, + connectorURI: null, + bridge_url: DEFAULT_BRIDGE_URL, + verificationState: VerificationState.PreparingClient, + + createClient: async ({ bridge_url, ...request }: BridgeConfig & Request) => { + const { key, iv } = await generateKey() + + if (bridge_url) { + const validation = validate_bridge_url(bridge_url, request.app_id.includes('staging')) + if (!validation.valid) { + console.error(validation.errors.join('\n')) + set({ verificationState: VerificationState.Failed }) + throw new Error('Invalid bridge_url. Please check the console for more details.') + } } - } - const res = await fetch(new URL('/request', bridge_url ?? DEFAULT_BRIDGE_URL), { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify( - await encryptRequest( - key, - iv, - JSON.stringify({ - app_id, - action_description, - action: encodeAction(action), - signal: generateSignal(signal).digest, - credential_types: verification_level_to_credential_types( - verification_level ?? DEFAULT_VERIFICATION_LEVEL - ), - verification_level: verification_level ?? DEFAULT_VERIFICATION_LEVEL, - }) - ) - ), - }) - - if (!res.ok) { - set({ verificationState: VerificationState.Failed }) - throw new Error('Failed to create client') - } - - const { request_id } = (await res.json()) as { request_id: string } - - set({ - iv, - key, - requestId: request_id, - bridge_url: bridge_url ?? DEFAULT_BRIDGE_URL, - verificationState: VerificationState.WaitingForConnection, - connectorURI: `https://worldcoin.org/verify?t=wld&i=${request_id}&k=${encodeURIComponent( - await exportKey(key) - )}${bridge_url && bridge_url !== DEFAULT_BRIDGE_URL ? `&b=${encodeURIComponent(bridge_url)}` : ''}`, - }) - }, - - pollForUpdates: async () => { - const key = get().key - if (!key) throw new Error('No keypair found. Please call `createClient` first.') - - const res = await fetch(new URL(`/response/${get().requestId}`, get().bridge_url)) - - if (!res.ok) { - return set({ - errorCode: AppErrorCodes.ConnectionFailed, - verificationState: VerificationState.Failed, + const res = await fetch(new URL('/request', bridge_url ?? DEFAULT_BRIDGE_URL), { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify( + await encryptRequest(key, iv, JSON.stringify(preprocessRequest?.(request) ?? request)) + ), }) - } - const { response, status } = (await res.json()) as BridgeResponse + if (!res.ok) { + set({ verificationState: VerificationState.Failed }) + throw new Error('Failed to create client') + } - if (status != ResponseStatus.Completed) { - return set({ - verificationState: - status == ResponseStatus.Retrieved - ? VerificationState.WaitingForApp - : VerificationState.WaitingForConnection, + const { request_id } = (await res.json()) as { request_id: string } + + set({ + iv, + key, + requestId: request_id, + bridge_url: bridge_url ?? DEFAULT_BRIDGE_URL, + verificationState: VerificationState.WaitingForConnection, + connectorURI: `https://worldcoin.org/verify?t=wld&i=${request_id}&k=${encodeURIComponent( + await exportKey(key) + )}${bridge_url && bridge_url !== DEFAULT_BRIDGE_URL ? `&b=${encodeURIComponent(bridge_url)}` : ''}`, }) - } + }, - let result = JSON.parse( - await decryptResponse(key, buffer_decode(response.iv), response.payload) - ) as BridgeResult + pollForUpdates: async () => { + const key = get().key + if (!key) throw new Error('No keypair found. Please call `createClient` first.') - if ('error_code' in result) { - return set({ - errorCode: result.error_code, - verificationState: VerificationState.Failed, - }) - } + const res = await fetch(new URL(`/response/${get().requestId}`, get().bridge_url)) + if (!res.ok) { + return set({ + errorCode: AppErrorCodes.ConnectionFailed, + verificationState: VerificationState.Failed, + }) + } + + const { response, status } = (await res.json()) as BridgeResponse + + if (status != ResponseStatus.Completed) { + return set({ + verificationState: + status == ResponseStatus.Retrieved + ? VerificationState.WaitingForApp + : VerificationState.WaitingForConnection, + }) + } + + const result = JSON.parse( + await decryptResponse(key, buffer_decode(response.iv), response.payload) + ) as BridgeResult + + if ('error_code' in result) { + return set({ + errorCode: result.error_code, + verificationState: VerificationState.Failed, + }) + } + + set({ + key: null, + requestId: null, + connectorURI: null, + result: preprocessResponse?.(result) ?? result, + verificationState: VerificationState.Confirmed, + }) + }, + + reset: () => { + set({ + iv: null, + key: null, + result: null, + errorCode: null, + requestId: null, + connectorURI: null, + verificationState: VerificationState.PreparingClient, + }) + }, + })) + +export type WalletBridgeStore = IBridgeStore +export const useWalletBridgeStore = buildBridgeStore( + ({ app_id, action_description, action, signal, verification_level }) => ({ + app_id, + action_description, + action: encodeAction(action), + signal: generateSignal(signal).digest, + credential_types: verification_level_to_credential_types(verification_level ?? DEFAULT_VERIFICATION_LEVEL), + verification_level: verification_level ?? DEFAULT_VERIFICATION_LEVEL, + }), + result => { if ('credential_type' in result) { result = { - verification_level: credential_type_to_verification_level(result.credential_type), ...result, + verification_level: credential_type_to_verification_level(result.credential_type as CredentialType), } satisfies ISuccessResult } - set({ - result, - key: null, - requestId: null, - connectorURI: null, - verificationState: VerificationState.Confirmed, - }) - }, - - reset: () => { - set({ - iv: null, - key: null, - result: null, - errorCode: null, - requestId: null, - connectorURI: null, - verificationState: VerificationState.PreparingClient, - }) - }, -})) + return result + } +) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 51f08899..1d0fee05 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -7,8 +7,9 @@ export { CredentialType, VerificationLevel, IDKitConfig, + BridgeConfig, } from '@/types' -export { useWorldBridgeStore, WorldBridgeStore } from '@/bridge' +export { useWalletBridgeStore, buildBridgeStore, WalletBridgeStore, IBridgeStore } from '@/bridge' export { DEFAULT_VERIFICATION_LEVEL, verification_level_to_credential_types } from '@/lib/utils' diff --git a/packages/core/src/types/config.ts b/packages/core/src/types/config.ts index af36cef0..0da079cc 100644 --- a/packages/core/src/types/config.ts +++ b/packages/core/src/types/config.ts @@ -16,17 +16,20 @@ export enum VerificationLevel { Device = 'device', } -export type IDKitConfig = { +export type BridgeConfig = { + /** URL to a third-party bridge to use when connecting to the World App. Optional. */ + bridge_url?: string /** Unique identifier for the app verifying the action. This should be the app ID obtained from the Developer Portal. */ app_id: `app_${string}` +} + +export type IDKitConfig = BridgeConfig & { /** Identifier for the action the user is performing. Should be left blank for [Sign in with Worldcoin](https://docs.worldcoin.org/id/sign-in). */ action: AbiEncodedValue | string /** The description of the specific action (shown to users in World App). Only recommended for actions created on-the-fly. */ action_description?: string /** Encodes data into a proof that must match when validating. Read more on the [On-chain section](https://docs.worldcoin.org/advanced/on-chain). */ signal?: AbiEncodedValue | string - /** URL to a third-party bridge to use when connecting to the World App. Optional. */ - bridge_url?: string /** The minimum required level of verification. Defaults to "orb". */ verification_level?: VerificationLevel } diff --git a/packages/core/src/types/index.ts b/packages/core/src/types/index.ts index 9f49c011..f50f24cd 100644 --- a/packages/core/src/types/index.ts +++ b/packages/core/src/types/index.ts @@ -1,3 +1,3 @@ export { ISuccessResult, IErrorState } from '@/types/result' export { AppErrorCodes, VerificationState } from '@/types/bridge' -export { AbiEncodedValue, VerificationLevel, CredentialType, IDKitConfig } from '@/types/config' +export { AbiEncodedValue, VerificationLevel, CredentialType, IDKitConfig, BridgeConfig } from '@/types/config' diff --git a/packages/react/src/services/wld-bridge.ts b/packages/react/src/services/wld-bridge.ts index 38db0c30..902bab9e 100644 --- a/packages/react/src/services/wld-bridge.ts +++ b/packages/react/src/services/wld-bridge.ts @@ -1,5 +1,5 @@ import { useEffect, useRef } from 'react' -import { useWorldBridgeStore } from '@worldcoin/idkit-core' +import { useWalletBridgeStore } from '@worldcoin/idkit-core' import type { ISuccessResult, AppErrorCodes, VerificationState, IDKitConfig } from '@worldcoin/idkit-core' type UseAppBridgeResponse = { @@ -20,7 +20,7 @@ export const useWorldBridge = ( ): UseAppBridgeResponse => { const ref_verification_level = useRef(verification_level) const { reset, result, connectorURI, createClient, pollForUpdates, verificationState, errorCode } = - useWorldBridgeStore() + useWalletBridgeStore() useEffect(() => { if (!connectorURI) {