@zkpassport/sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts ADDED
@@ -0,0 +1,326 @@
1
+ import { randomBytes } from 'crypto'
2
+ import { Alpha3Code, getAlpha3Code, registerLocale } from 'i18n-iso-countries'
3
+ import {
4
+ DisclosableIDCredential,
5
+ IDCredential,
6
+ IDCredentialConfig,
7
+ IDCredentialValue,
8
+ NumericalIDCredential,
9
+ } from '@/types/credentials'
10
+ import { ProofResult } from '@/types/query-result'
11
+ import { CountryName } from '@/types/countries'
12
+ //import { UltraHonkBackend, ProofData, CompiledCircuit } from '@noir-lang/backend_barretenberg'
13
+ import { bytesToHex } from '@noble/ciphers/utils'
14
+ import { getWebSocketClient, WebSocketClient } from '@/websocket'
15
+ import { createEncryptedJsonRpcRequest } from '@/json-rpc'
16
+ import { decrypt, generateECDHKeyPair, getSharedSecret } from '@/encryption'
17
+ import { JsonRpcRequest } from '@/types/json-rpc'
18
+ import logger from '@/logger'
19
+
20
+ registerLocale(require('i18n-iso-countries/langs/en.json'))
21
+
22
+ function normalizeCountry(country: CountryName | Alpha3Code) {
23
+ let normalizedCountry: Alpha3Code | undefined
24
+ const alpha3 = getAlpha3Code(country, 'en') as Alpha3Code | undefined
25
+ normalizedCountry = alpha3 || (country as Alpha3Code)
26
+ return normalizedCountry
27
+ }
28
+
29
+ function numericalCompare(
30
+ fnName: 'gte' | 'gt' | 'lte' | 'lt',
31
+ key: NumericalIDCredential,
32
+ value: number | Date,
33
+ requestId: string,
34
+ requestIdToConfig: Record<string, Record<string, IDCredentialConfig>>,
35
+ ) {
36
+ requestIdToConfig[requestId][key] = {
37
+ ...requestIdToConfig[requestId][key],
38
+ [fnName]: value,
39
+ }
40
+ }
41
+
42
+ function rangeCompare(
43
+ key: NumericalIDCredential,
44
+ value: [number | Date, number | Date],
45
+ requestId: string,
46
+ requestIdToConfig: Record<string, Record<string, IDCredentialConfig>>,
47
+ ) {
48
+ requestIdToConfig[requestId][key] = {
49
+ ...requestIdToConfig[requestId][key],
50
+ range: value,
51
+ }
52
+ }
53
+
54
+ function generalCompare(
55
+ fnName: 'in' | 'out' | 'eq',
56
+ key: IDCredential,
57
+ value: any,
58
+ requestId: string,
59
+ requestIdToConfig: Record<string, Record<string, IDCredentialConfig>>,
60
+ ) {
61
+ requestIdToConfig[requestId][key] = {
62
+ ...requestIdToConfig[requestId][key],
63
+ [fnName]: value,
64
+ }
65
+ }
66
+
67
+ export * from '@/constants'
68
+ export * from '@/types'
69
+
70
+ export class ZkPassport {
71
+ private domain: string
72
+ private topicToConfig: Record<string, Record<string, IDCredentialConfig>> = {}
73
+ private topicToKeyPair: Record<string, { privateKey: Uint8Array; publicKey: Uint8Array }> = {}
74
+ private topicToWebSocketClient: Record<string, WebSocketClient> = {}
75
+ private topicToSharedSecret: Record<string, Uint8Array> = {}
76
+ private topicToQRCodeScanned: Record<string, boolean> = {}
77
+
78
+ private onQRCodeScannedCallbacks: Record<string, Array<() => void>> = {}
79
+ private onGeneratingProofCallbacks: Record<string, Array<(topic: string) => void>> = {}
80
+ private onBridgeConnectCallbacks: Record<string, Array<() => void>> = {}
81
+ private onProofGeneratedCallbacks: Record<string, Array<(result: ProofResult) => void>> = {}
82
+ private onRejectCallbacks: Record<string, Array<() => void>> = {}
83
+ private onErrorCallbacks: Record<string, Array<(topic: string) => void>> = {}
84
+ private topicToService: Record<string, { name: string; logo: string; purpose: string }> = {}
85
+
86
+ constructor(_domain?: string) {
87
+ if (!_domain && typeof window === 'undefined') {
88
+ throw new Error('Domain argument is required in Node.js environment')
89
+ }
90
+ this.domain = _domain || window.location.hostname
91
+ }
92
+
93
+ /**
94
+ * @notice Handle an encrypted message.
95
+ * @param request The request.
96
+ * @param outerRequest The outer request.
97
+ */
98
+ private async handleEncryptedMessage(topic: string, request: JsonRpcRequest, outerRequest: JsonRpcRequest) {
99
+ logger.debug('Received encrypted message:', request)
100
+ if (request.method === 'accept') {
101
+ logger.debug(`User accepted the request and is generating a proof`)
102
+ await Promise.all(this.onGeneratingProofCallbacks[topic].map((callback) => callback(topic)))
103
+ } else if (request.method === 'reject') {
104
+ logger.debug(`User rejected the request`)
105
+ await Promise.all(this.onRejectCallbacks[topic].map((callback) => callback()))
106
+ } else if (request.method === 'done') {
107
+ logger.debug(`User generated proof`)
108
+ await Promise.all(this.onProofGeneratedCallbacks[topic].map((callback) => callback(request.params.result)))
109
+ } else if (request.method === 'error') {
110
+ await Promise.all(this.onErrorCallbacks[topic].map((callback) => callback(request.params.error)))
111
+ }
112
+ }
113
+
114
+ private getZkPassportRequest(topic: string) {
115
+ return {
116
+ eq: <T extends IDCredential>(key: T, value: IDCredentialValue<T>) => {
117
+ if (key === 'issuing_country' || key === 'nationality') {
118
+ value = normalizeCountry(value as CountryName) as IDCredentialValue<T>
119
+ }
120
+ generalCompare('eq', key, value, topic, this.topicToConfig)
121
+ return this.getZkPassportRequest(topic)
122
+ },
123
+ gte: <T extends NumericalIDCredential>(key: T, value: IDCredentialValue<T>) => {
124
+ numericalCompare('gte', key, value, topic, this.topicToConfig)
125
+ return this.getZkPassportRequest(topic)
126
+ },
127
+ gt: <T extends NumericalIDCredential>(key: T, value: IDCredentialValue<T>) => {
128
+ numericalCompare('gt', key, value, topic, this.topicToConfig)
129
+ return this.getZkPassportRequest(topic)
130
+ },
131
+ lte: <T extends NumericalIDCredential>(key: T, value: IDCredentialValue<T>) => {
132
+ numericalCompare('lte', key, value, topic, this.topicToConfig)
133
+ return this.getZkPassportRequest(topic)
134
+ },
135
+ lt: <T extends NumericalIDCredential>(key: T, value: IDCredentialValue<T>) => {
136
+ numericalCompare('lt', key, value, topic, this.topicToConfig)
137
+ return this.getZkPassportRequest(topic)
138
+ },
139
+ range: <T extends NumericalIDCredential>(key: T, start: IDCredentialValue<T>, end: IDCredentialValue<T>) => {
140
+ rangeCompare(key, [start, end], topic, this.topicToConfig)
141
+ return this.getZkPassportRequest(topic)
142
+ },
143
+ in: <T extends IDCredential>(key: T, value: IDCredentialValue<T>[]) => {
144
+ if (key === 'issuing_country' || key === 'nationality') {
145
+ value = value.map((v) => normalizeCountry(v as CountryName)) as IDCredentialValue<T>[]
146
+ }
147
+ generalCompare('in', key, value, topic, this.topicToConfig)
148
+ return this.getZkPassportRequest(topic)
149
+ },
150
+ out: <T extends IDCredential>(key: T, value: IDCredentialValue<T>[]) => {
151
+ if (key === 'issuing_country' || key === 'nationality') {
152
+ value = value.map((v) => normalizeCountry(v as CountryName)) as IDCredentialValue<T>[]
153
+ }
154
+ generalCompare('out', key, value, topic, this.topicToConfig)
155
+ return this.getZkPassportRequest(topic)
156
+ },
157
+ disclose: (key: DisclosableIDCredential) => {
158
+ this.topicToConfig[topic][key] = {
159
+ ...this.topicToConfig[topic][key],
160
+ disclose: true,
161
+ }
162
+ return this.getZkPassportRequest(topic)
163
+ },
164
+ /*checkAML: (country?: CountryName | Alpha2Code | Alpha3Code) => {
165
+ return this.getZkPassportRequest(topic)
166
+ },*/
167
+ done: () => {
168
+ const base64Config = Buffer.from(JSON.stringify(this.topicToConfig[topic])).toString('base64')
169
+ const base64Service = Buffer.from(JSON.stringify(this.topicToService[topic])).toString('base64')
170
+ const pubkey = bytesToHex(this.topicToKeyPair[topic].publicKey)
171
+ return {
172
+ url: `https://zkpassport.id/r?d=${this.domain}&t=${topic}&c=${base64Config}&s=${base64Service}&p=${pubkey}`,
173
+ requestId: topic,
174
+ onQRCodeScanned: (callback: () => void) => this.onQRCodeScannedCallbacks[topic].push(callback),
175
+ onGeneratingProof: (callback: () => void) => this.onGeneratingProofCallbacks[topic].push(callback),
176
+ onBridgeConnect: (callback: () => void) => this.onBridgeConnectCallbacks[topic].push(callback),
177
+ onProofGenerated: (callback: (result: ProofResult) => void) =>
178
+ this.onProofGeneratedCallbacks[topic].push(callback),
179
+ onReject: (callback: () => void) => this.onRejectCallbacks[topic].push(callback),
180
+ onError: (callback: (error: string) => void) => this.onErrorCallbacks[topic].push(callback),
181
+ isBridgeConnected: () => this.topicToWebSocketClient[topic].readyState === WebSocket.OPEN,
182
+ isQRCodeScanned: () => this.topicToQRCodeScanned[topic] === true,
183
+ }
184
+ },
185
+ }
186
+ }
187
+
188
+ /**
189
+ * @notice Create a new request.
190
+ * @returns The query builder object.
191
+ */
192
+ public async request({
193
+ name,
194
+ logo,
195
+ purpose,
196
+ topicOverride,
197
+ keyPairOverride,
198
+ }: {
199
+ name: string
200
+ logo: string
201
+ purpose: string
202
+ topicOverride?: string
203
+ keyPairOverride?: { privateKey: Uint8Array; publicKey: Uint8Array }
204
+ }) {
205
+ const topic = topicOverride || randomBytes(16).toString('hex')
206
+
207
+ const keyPair = keyPairOverride || (await generateECDHKeyPair())
208
+ this.topicToKeyPair[topic] = {
209
+ privateKey: keyPair.privateKey,
210
+ publicKey: keyPair.publicKey,
211
+ }
212
+
213
+ this.topicToConfig[topic] = {}
214
+ this.topicToService[topic] = { name, logo, purpose }
215
+
216
+ this.onQRCodeScannedCallbacks[topic] = []
217
+ this.onGeneratingProofCallbacks[topic] = []
218
+ this.onBridgeConnectCallbacks[topic] = []
219
+ this.onProofGeneratedCallbacks[topic] = []
220
+ this.onRejectCallbacks[topic] = []
221
+ this.onErrorCallbacks[topic] = []
222
+
223
+ const wsClient = getWebSocketClient(`wss://bridge.zkpassport.id?topic=${topic}`, this.domain)
224
+ this.topicToWebSocketClient[topic] = wsClient
225
+ wsClient.onopen = async () => {
226
+ logger.info('[frontend] WebSocket connection established')
227
+ await Promise.all(this.onBridgeConnectCallbacks[topic].map((callback) => callback()))
228
+ }
229
+ wsClient.addEventListener('message', async (event: any) => {
230
+ logger.debug('[frontend] Received message:', event.data)
231
+ try {
232
+ const data: JsonRpcRequest = JSON.parse(event.data)
233
+ // Handshake happens when the mobile app scans the QR code and connects to the bridge
234
+ if (data.method === 'handshake') {
235
+ logger.debug('[frontend] Received handshake:', event.data)
236
+
237
+ this.topicToQRCodeScanned[topic] = true
238
+ this.topicToSharedSecret[topic] = await getSharedSecret(bytesToHex(keyPair.privateKey), data.params.pubkey)
239
+ logger.debug('[frontend] Shared secret:', Buffer.from(this.topicToSharedSecret[topic]).toString('hex'))
240
+
241
+ const encryptedMessage = await createEncryptedJsonRpcRequest(
242
+ 'hello',
243
+ null,
244
+ this.topicToSharedSecret[topic],
245
+ topic,
246
+ )
247
+ logger.debug('[frontend] Sending encrypted message:', encryptedMessage)
248
+ wsClient.send(JSON.stringify(encryptedMessage))
249
+
250
+ await Promise.all(this.onQRCodeScannedCallbacks[topic].map((callback) => callback()))
251
+ return
252
+ }
253
+
254
+ // Handle encrypted messages
255
+ if (data.method === 'encryptedMessage') {
256
+ // Decode the payload from base64 to Uint8Array
257
+ const payload = new Uint8Array(
258
+ atob(data.params.payload)
259
+ .split('')
260
+ .map((c) => c.charCodeAt(0)),
261
+ )
262
+ try {
263
+ // Decrypt the payload using the shared secret
264
+ const decrypted = await decrypt(payload, this.topicToSharedSecret[topic], topic)
265
+ const decryptedJson: JsonRpcRequest = JSON.parse(decrypted)
266
+ this.handleEncryptedMessage(topic, decryptedJson, data)
267
+ } catch (error) {
268
+ logger.error('[frontend] Error decrypting message:', error)
269
+ }
270
+ return
271
+ }
272
+ } catch (error) {
273
+ logger.error('[frontend] Error:', error)
274
+ }
275
+ })
276
+ wsClient.onerror = (error: Event) => {
277
+ logger.error('[frontend] WebSocket error:', error)
278
+ }
279
+ return this.getZkPassportRequest(topic)
280
+ }
281
+
282
+ /**
283
+ * @notice Verifies a proof.
284
+ * @param proof The proof to verify.
285
+ * @returns True if the proof is valid, false otherwise.
286
+ */
287
+ /*public verify(result: ProofResult) {
288
+ const backend = new UltraHonkBackend(proofOfAgeCircuit as CompiledCircuit)
289
+ const proofData: ProofData = {
290
+ proof: Buffer.from(result.proof as string, 'hex'),
291
+ // TODO: extract the public inputs from the proof
292
+ publicInputs: [],
293
+ }
294
+ return backend.verifyProof(proofData)
295
+ }*/
296
+
297
+ /**
298
+ * @notice Returns the URL of the request.
299
+ * @param requestId The request ID.
300
+ * @returns The URL of the request.
301
+ */
302
+ public getUrl(requestId: string) {
303
+ const pubkey = bytesToHex(this.topicToKeyPair[requestId].publicKey)
304
+ const base64Config = Buffer.from(JSON.stringify(this.topicToConfig[requestId])).toString('base64')
305
+ const base64Service = Buffer.from(JSON.stringify(this.topicToService[requestId])).toString('base64')
306
+ return `https://zkpassport.id/r?d=${this.domain}&t=${requestId}&c=${base64Config}&s=${base64Service}&p=${pubkey}`
307
+ }
308
+
309
+ /**
310
+ * @notice Cancels a request by closing the WebSocket connection and deleting the associated data.
311
+ * @param requestId The request ID.
312
+ */
313
+ public cancelRequest(requestId: string) {
314
+ this.topicToWebSocketClient[requestId].close()
315
+ delete this.topicToWebSocketClient[requestId]
316
+ delete this.topicToKeyPair[requestId]
317
+ delete this.topicToConfig[requestId]
318
+ delete this.topicToSharedSecret[requestId]
319
+ this.onQRCodeScannedCallbacks[requestId] = []
320
+ this.onGeneratingProofCallbacks[requestId] = []
321
+ this.onBridgeConnectCallbacks[requestId] = []
322
+ this.onProofGeneratedCallbacks[requestId] = []
323
+ this.onRejectCallbacks[requestId] = []
324
+ this.onErrorCallbacks[requestId] = []
325
+ }
326
+ }
@@ -0,0 +1,57 @@
1
+ import { randomBytes } from 'crypto'
2
+ import { JsonRpcRequest, JsonRpcResponse } from '@/types/json-rpc'
3
+ import { encrypt } from '@/encryption'
4
+ import { WebSocketClient } from '@/websocket'
5
+ import logger from '@/logger'
6
+
7
+ export function createJsonRpcRequest(method: string, params: any): JsonRpcRequest {
8
+ return {
9
+ jsonrpc: '2.0',
10
+ id: randomBytes(16).toString('hex'),
11
+ method,
12
+ params,
13
+ }
14
+ }
15
+
16
+ export async function createEncryptedJsonRpcRequest(
17
+ method: string,
18
+ params: any,
19
+ sharedSecret: Uint8Array,
20
+ topic: string,
21
+ ): Promise<JsonRpcRequest> {
22
+ const encryptedMessage = await encrypt(JSON.stringify({ method, params: params || {} }), sharedSecret, topic)
23
+ return createJsonRpcRequest('encryptedMessage', {
24
+ payload: Buffer.from(encryptedMessage).toString('base64'),
25
+ })
26
+ }
27
+
28
+ export async function sendEncryptedJsonRpcRequest(
29
+ method: string,
30
+ params: any,
31
+ sharedSecret: Uint8Array,
32
+ topic: string,
33
+ wsClient: WebSocketClient,
34
+ ): Promise<boolean> {
35
+ try {
36
+ const message = { method, params: params || {} }
37
+ const encryptedMessage = await encrypt(JSON.stringify(message), sharedSecret, topic)
38
+ const request = createJsonRpcRequest('encryptedMessage', {
39
+ payload: Buffer.from(encryptedMessage).toString('base64'),
40
+ })
41
+ logger.debug('Sending encrypted message (original):', message)
42
+ logger.debug('Sending encrypted message (encrypted):', request)
43
+ wsClient.send(JSON.stringify(request))
44
+ return true
45
+ } catch (error) {
46
+ logger.error('Error sending encrypted message:', error)
47
+ return false
48
+ }
49
+ }
50
+
51
+ export function createJsonRpcResponse(id: string, result: any): JsonRpcResponse {
52
+ return {
53
+ jsonrpc: '2.0',
54
+ id,
55
+ result,
56
+ }
57
+ }
package/src/logger.ts ADDED
@@ -0,0 +1,44 @@
1
+ // import { createLogger, transports, format } from 'winston'
2
+ // import colors from 'colors/safe'
3
+ // import util from 'util'
4
+
5
+ // const logger = createLogger({
6
+ // level: 'debug',
7
+ // format: format.combine(
8
+ // format.timestamp({ format: 'HH:mm' }),
9
+ // format.printf(({ timestamp, level, message, additionalInfo }) => {
10
+ // const colorMap = {
11
+ // debug: colors.cyan,
12
+ // info: colors.green,
13
+ // warn: colors.yellow,
14
+ // error: colors.red,
15
+ // } as const
16
+ // const coloredLevel = (colorMap[level as keyof typeof colorMap] || colors.white)(level.toUpperCase())
17
+
18
+ // let logMessage = `${timestamp} [${coloredLevel}] ${message}`
19
+
20
+ // if (additionalInfo && additionalInfo.length > 0) {
21
+ // logMessage += ' ' + util.inspect(additionalInfo, { depth: null, colors: true })
22
+ // }
23
+
24
+ // return logMessage
25
+ // }),
26
+ // ),
27
+ // transports: [new transports.Console()],
28
+ // })
29
+
30
+ // const customLogger = {
31
+ // debug: (message: string, ...args: any[]) => logger.debug(message, { additionalInfo: args }),
32
+ // info: (message: string, ...args: any[]) => logger.info(message, { additionalInfo: args }),
33
+ // warn: (message: string, ...args: any[]) => logger.warn(message, { additionalInfo: args }),
34
+ // error: (message: string, ...args: any[]) => logger.error(message, { additionalInfo: args }),
35
+ // }
36
+
37
+ const customLogger = {
38
+ debug: (message: string, ...args: any[]) => console.debug(message, ...args),
39
+ info: (message: string, ...args: any[]) => console.info(message, ...args),
40
+ warn: (message: string, ...args: any[]) => console.warn(message, ...args),
41
+ error: (message: string, ...args: any[]) => console.error(message, ...args),
42
+ }
43
+
44
+ export default customLogger
package/src/mobile.ts ADDED
@@ -0,0 +1,177 @@
1
+ import { bytesToHex } from '@noble/ciphers/utils'
2
+ import { getWebSocketClient, WebSocketClient } from '@/websocket'
3
+ import { sendEncryptedJsonRpcRequest } from '@/json-rpc'
4
+ import { decrypt, generateECDHKeyPair, getSharedSecret } from '@/encryption'
5
+ import { JsonRpcRequest } from '@/types/json-rpc'
6
+ import logger from '@/logger'
7
+
8
+ export class ZkPassportProver {
9
+ private domain?: string
10
+ private topicToKeyPair: Record<string, { privateKey: Uint8Array; publicKey: Uint8Array }> = {}
11
+ private topicToWebSocketClient: Record<string, WebSocketClient> = {}
12
+ private topicToRemoteDomainVerified: Record<string, boolean> = {}
13
+ private topicToSharedSecret: Record<string, Uint8Array> = {}
14
+ private topicToRemotePublicKey: Record<string, Uint8Array> = {}
15
+
16
+ private onDomainVerifiedCallbacks: Record<string, Array<() => void>> = {}
17
+ private onBridgeConnectCallbacks: Record<string, Array<() => void>> = {}
18
+ private onWebsiteDomainVerifyFailureCallbacks: Record<string, Array<() => void>> = {}
19
+
20
+ constructor() {}
21
+
22
+ /**
23
+ * @notice Handle an encrypted message.
24
+ * @param request The request.
25
+ * @param outerRequest The outer request.
26
+ */
27
+ private async handleEncryptedMessage(topic: string, request: JsonRpcRequest, outerRequest: JsonRpcRequest) {
28
+ logger.debug('Received encrypted message:', request)
29
+ if (request.method === 'hello') {
30
+ logger.info(`Successfully verified origin domain name: ${outerRequest.origin}`)
31
+ this.topicToRemoteDomainVerified[topic] = true
32
+ await Promise.all(this.onDomainVerifiedCallbacks[topic].map((callback) => callback()))
33
+ } else if (request.method === 'closed_page') {
34
+ // TODO: Implement
35
+ }
36
+ }
37
+
38
+ /**
39
+ * @notice Scan a credentirequest QR code.
40
+ * @returns
41
+ */
42
+ public async scan(
43
+ url: string,
44
+ {
45
+ keyPairOverride,
46
+ }: {
47
+ keyPairOverride?: { privateKey: Uint8Array; publicKey: Uint8Array }
48
+ } = {},
49
+ ) {
50
+ const parsedUrl = new URL(url)
51
+ const domain = parsedUrl.searchParams.get('d')
52
+ const topic = parsedUrl.searchParams.get('t')
53
+ const pubkeyHex = parsedUrl.searchParams.get('p')
54
+
55
+ if (!domain || !topic || !pubkeyHex) {
56
+ throw new Error('Invalid URL: missing required parameters')
57
+ }
58
+
59
+ const pubkey = new Uint8Array(Buffer.from(pubkeyHex, 'hex'))
60
+
61
+ this.domain = domain
62
+ const keyPair = keyPairOverride || (await generateECDHKeyPair())
63
+
64
+ this.topicToKeyPair[topic] = {
65
+ privateKey: keyPair.privateKey,
66
+ publicKey: keyPair.publicKey,
67
+ }
68
+ this.topicToRemotePublicKey[topic] = pubkey
69
+ this.topicToSharedSecret[topic] = await getSharedSecret(bytesToHex(keyPair.privateKey), bytesToHex(pubkey))
70
+ this.topicToRemoteDomainVerified[topic] = false
71
+ this.onDomainVerifiedCallbacks[topic] = []
72
+ this.onBridgeConnectCallbacks[topic] = []
73
+
74
+ // Set up WebSocket connection
75
+ const wsClient = getWebSocketClient(
76
+ `wss://bridge.zkpassport.id?topic=${topic}&pubkey=${bytesToHex(keyPair.publicKey)}`,
77
+ )
78
+ this.topicToWebSocketClient[topic] = wsClient
79
+
80
+ wsClient.onopen = async () => {
81
+ logger.info('[mobile] WebSocket connection established')
82
+ await Promise.all(this.onBridgeConnectCallbacks[topic].map((callback) => callback()))
83
+ // Server sends handshake automatically (when it sees a pubkey in websocket URI)
84
+ // wsClient.send(
85
+ // JSON.stringify(
86
+ // createJsonRpcRequest('handshake', {
87
+ // pubkey: bytesToHex(keyPair.publicKey),
88
+ // }),
89
+ // ),
90
+ // )
91
+ }
92
+
93
+ wsClient.addEventListener('message', async (event: any) => {
94
+ logger.info('[mobile] Received message:', event.data)
95
+
96
+ try {
97
+ const data: JsonRpcRequest = JSON.parse(event.data)
98
+ const originDomain = data.origin ? new URL(data.origin).hostname : undefined
99
+ // Origin domain must match domain in QR code
100
+ if (originDomain !== this.domain) {
101
+ logger.warn(
102
+ `[mobile] Origin does not match domain in QR code. Expected ${this.domain} but got ${originDomain}`,
103
+ )
104
+ return
105
+ }
106
+
107
+ if (data.method === 'encryptedMessage') {
108
+ // Decode the payload from base64 to Uint8Array
109
+ const payload = new Uint8Array(
110
+ atob(data.params.payload)
111
+ .split('')
112
+ .map((c) => c.charCodeAt(0)),
113
+ )
114
+ try {
115
+ // Decrypt the payload using the shared secret
116
+ const decrypted = await decrypt(payload, this.topicToSharedSecret[topic], topic)
117
+ const decryptedJson: JsonRpcRequest = JSON.parse(decrypted)
118
+ await this.handleEncryptedMessage(topic, decryptedJson, data)
119
+ } catch (error) {
120
+ logger.error('[mobile] Error decrypting message:', error)
121
+ }
122
+ }
123
+ } catch (error) {
124
+ logger.error('[mobile] Error:', error)
125
+ }
126
+ })
127
+
128
+ wsClient.onerror = (error: Event) => {
129
+ logger.error('[mobile] WebSocket error:', error)
130
+ }
131
+
132
+ return {
133
+ domain: this.domain,
134
+ requestId: topic,
135
+ isBridgeConnected: () => this.topicToWebSocketClient[topic].readyState === WebSocket.OPEN,
136
+ isDomainVerified: () => this.topicToRemoteDomainVerified[topic] === true,
137
+ onDomainVerified: (callback: () => void) => this.onDomainVerifiedCallbacks[topic].push(callback),
138
+ onBridgeConnect: (callback: () => void) => this.onBridgeConnectCallbacks[topic].push(callback),
139
+ notifyReject: async () => {
140
+ await sendEncryptedJsonRpcRequest(
141
+ 'reject',
142
+ null,
143
+ this.topicToSharedSecret[topic],
144
+ topic,
145
+ this.topicToWebSocketClient[topic],
146
+ )
147
+ },
148
+ notifyAccept: async () => {
149
+ await sendEncryptedJsonRpcRequest(
150
+ 'accept',
151
+ null,
152
+ this.topicToSharedSecret[topic],
153
+ topic,
154
+ this.topicToWebSocketClient[topic],
155
+ )
156
+ },
157
+ notifyDone: async (proof: any) => {
158
+ await sendEncryptedJsonRpcRequest(
159
+ 'done',
160
+ { proof },
161
+ this.topicToSharedSecret[topic],
162
+ topic,
163
+ this.topicToWebSocketClient[topic],
164
+ )
165
+ },
166
+ notifyError: async (error: string) => {
167
+ await sendEncryptedJsonRpcRequest(
168
+ 'error',
169
+ { error },
170
+ this.topicToSharedSecret[topic],
171
+ topic,
172
+ this.topicToWebSocketClient[topic],
173
+ )
174
+ },
175
+ }
176
+ }
177
+ }