Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support non-worldid bridge usage on core #289

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
239 changes: 121 additions & 118 deletions packages/core/src/bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -26,148 +26,151 @@ type BridgeResponse =
response: { iv: string; payload: string }
}

type BridgeResult =
| ISuccessResult
| (Omit<ISuccessResult, 'verification_level'> & { credential_type: CredentialType })
| { error_code: AppErrorCodes }
type BridgeResult<Success> = { error_code: AppErrorCodes } | (Success & { error_code: never })

export type WorldBridgeStore = {
export type IBridgeStore<Request, Response> = {
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<void>
createClient: (config: BridgeConfig & Request) => Promise<void>
pollForUpdates: () => Promise<void>
reset: () => void
}

export const useWorldBridgeStore = create<WorldBridgeStore>((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 = <Request extends Record<string, unknown>, Response>(
preprocessRequest?: (request: Omit<BridgeConfig & Request, 'bridge_url'>) => Record<string, unknown>,
preprocessResponse?: (response: Response) => Response
) =>
create<IBridgeStore<Request, Response>>((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<Response>

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<IDKitConfig, ISuccessResult>
export const useWalletBridgeStore = buildBridgeStore<IDKitConfig, ISuccessResult>(
({ 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
}
)
3 changes: 2 additions & 1 deletion packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
9 changes: 6 additions & 3 deletions packages/core/src/types/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need to include app_id inside bridge config. If this is for allowing generic payloads in the bridge?

}

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
}
2 changes: 1 addition & 1 deletion packages/core/src/types/index.ts
Original file line number Diff line number Diff line change
@@ -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'
4 changes: 2 additions & 2 deletions packages/react/src/services/wld-bridge.ts
Original file line number Diff line number Diff line change
@@ -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 = {
Expand All @@ -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) {
Expand Down
Loading