@aztec/wallet-sdk 0.0.1-commit.fffb133c → 0.0.1-dev

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.
Files changed (71) hide show
  1. package/dest/base-wallet/base_wallet.d.ts +83 -35
  2. package/dest/base-wallet/base_wallet.d.ts.map +1 -1
  3. package/dest/base-wallet/base_wallet.js +231 -70
  4. package/dest/base-wallet/index.d.ts +3 -2
  5. package/dest/base-wallet/index.d.ts.map +1 -1
  6. package/dest/base-wallet/index.js +1 -0
  7. package/dest/base-wallet/utils.d.ts +50 -0
  8. package/dest/base-wallet/utils.d.ts.map +1 -0
  9. package/dest/base-wallet/utils.js +133 -0
  10. package/dest/crypto.d.ts +39 -1
  11. package/dest/crypto.d.ts.map +1 -1
  12. package/dest/crypto.js +88 -0
  13. package/dest/extension/handlers/background_connection_handler.d.ts +12 -2
  14. package/dest/extension/handlers/background_connection_handler.d.ts.map +1 -1
  15. package/dest/extension/handlers/background_connection_handler.js +44 -8
  16. package/dest/extension/handlers/content_script_connection_handler.d.ts +2 -1
  17. package/dest/extension/handlers/content_script_connection_handler.d.ts.map +1 -1
  18. package/dest/extension/handlers/content_script_connection_handler.js +19 -0
  19. package/dest/extension/handlers/internal_message_types.d.ts +3 -1
  20. package/dest/extension/handlers/internal_message_types.d.ts.map +1 -1
  21. package/dest/extension/handlers/internal_message_types.js +3 -1
  22. package/dest/extension/provider/extension_wallet.d.ts +27 -6
  23. package/dest/extension/provider/extension_wallet.d.ts.map +1 -1
  24. package/dest/extension/provider/extension_wallet.js +87 -9
  25. package/dest/extension/provider/index.d.ts +2 -2
  26. package/dest/extension/provider/index.d.ts.map +1 -1
  27. package/dest/iframe/handlers/iframe_connection_handler.d.ts +122 -0
  28. package/dest/iframe/handlers/iframe_connection_handler.d.ts.map +1 -0
  29. package/dest/iframe/handlers/iframe_connection_handler.js +240 -0
  30. package/dest/iframe/handlers/index.d.ts +2 -0
  31. package/dest/iframe/handlers/index.d.ts.map +1 -0
  32. package/dest/iframe/handlers/index.js +1 -0
  33. package/dest/iframe/provider/iframe_discovery.d.ts +25 -0
  34. package/dest/iframe/provider/iframe_discovery.d.ts.map +1 -0
  35. package/dest/iframe/provider/iframe_discovery.js +167 -0
  36. package/dest/iframe/provider/iframe_provider.d.ts +65 -0
  37. package/dest/iframe/provider/iframe_provider.d.ts.map +1 -0
  38. package/dest/iframe/provider/iframe_provider.js +257 -0
  39. package/dest/iframe/provider/iframe_wallet.d.ts +85 -0
  40. package/dest/iframe/provider/iframe_wallet.d.ts.map +1 -0
  41. package/dest/iframe/provider/iframe_wallet.js +269 -0
  42. package/dest/iframe/provider/index.d.ts +4 -0
  43. package/dest/iframe/provider/index.d.ts.map +1 -0
  44. package/dest/iframe/provider/index.js +3 -0
  45. package/dest/manager/types.d.ts +6 -5
  46. package/dest/manager/types.d.ts.map +1 -1
  47. package/dest/manager/wallet_manager.d.ts +1 -1
  48. package/dest/manager/wallet_manager.d.ts.map +1 -1
  49. package/dest/manager/wallet_manager.js +48 -18
  50. package/dest/types.d.ts +64 -2
  51. package/dest/types.d.ts.map +1 -1
  52. package/dest/types.js +29 -0
  53. package/package.json +19 -9
  54. package/src/base-wallet/base_wallet.ts +318 -124
  55. package/src/base-wallet/index.ts +7 -1
  56. package/src/base-wallet/utils.ts +240 -0
  57. package/src/crypto.ts +104 -0
  58. package/src/extension/handlers/background_connection_handler.ts +42 -9
  59. package/src/extension/handlers/content_script_connection_handler.ts +18 -0
  60. package/src/extension/handlers/internal_message_types.ts +2 -0
  61. package/src/extension/provider/extension_wallet.ts +104 -15
  62. package/src/extension/provider/index.ts +1 -1
  63. package/src/iframe/handlers/iframe_connection_handler.ts +342 -0
  64. package/src/iframe/handlers/index.ts +7 -0
  65. package/src/iframe/provider/iframe_discovery.ts +185 -0
  66. package/src/iframe/provider/iframe_provider.ts +331 -0
  67. package/src/iframe/provider/iframe_wallet.ts +323 -0
  68. package/src/iframe/provider/index.ts +3 -0
  69. package/src/manager/types.ts +5 -4
  70. package/src/manager/wallet_manager.ts +55 -23
  71. package/src/types.ts +72 -0
@@ -6,7 +6,17 @@ import { schemaHasMethod } from '@aztec/foundation/schemas';
6
6
  import type { FunctionsOf } from '@aztec/foundation/types';
7
7
 
8
8
  import { type EncryptedPayload, decrypt, encrypt } from '../../crypto.js';
9
- import { type WalletMessage, WalletMessageType, type WalletResponse } from '../../types.js';
9
+ import {
10
+ DEFAULT_HEARTBEAT_DEAD_AFTER_MS,
11
+ DEFAULT_HEARTBEAT_INTERVAL_MS,
12
+ type DisconnectCallback,
13
+ type HeartbeatOptions,
14
+ NOOP_LOGGER,
15
+ type WalletMessage,
16
+ WalletMessageType,
17
+ type WalletResponse,
18
+ type WalletSdkLogger,
19
+ } from '../../types.js';
10
20
 
11
21
  /**
12
22
  * Internal type representing a wallet method call before encryption.
@@ -19,11 +29,6 @@ type WalletMethodCall = {
19
29
  args: unknown[];
20
30
  };
21
31
 
22
- /**
23
- * Callback type for wallet disconnect events.
24
- */
25
- export type DisconnectCallback = () => void;
26
-
27
32
  /**
28
33
  * A wallet implementation that communicates with browser extension wallets
29
34
  * using an encrypted MessageChannel.
@@ -60,6 +65,11 @@ export class ExtensionWallet {
60
65
  private inFlight = new Map<string, PromiseWithResolvers<unknown>>();
61
66
  private disconnected = false;
62
67
  private disconnectCallbacks: DisconnectCallback[] = [];
68
+ private heartbeatTimer: ReturnType<typeof setInterval> | null = null;
69
+ private lastInboundAt = 0;
70
+ private log: WalletSdkLogger;
71
+ private heartbeatIntervalMs: number;
72
+ private heartbeatDeadAfterMs: number;
63
73
 
64
74
  /**
65
75
  * Private constructor - use {@link ExtensionWallet.create} to instantiate.
@@ -68,6 +78,8 @@ export class ExtensionWallet {
68
78
  * @param extensionId - The unique identifier of the target wallet extension
69
79
  * @param port - The MessagePort for private communication with the wallet
70
80
  * @param sharedKey - The derived AES-256-GCM shared key for encryption
81
+ * @param logger - Optional logger; defaults to a no-op logger
82
+ * @param heartbeatOptions - Optional heartbeat tuning (mostly useful for tests)
71
83
  */
72
84
  private constructor(
73
85
  private chainInfo: ChainInfo,
@@ -75,7 +87,13 @@ export class ExtensionWallet {
75
87
  private extensionId: string,
76
88
  private port: MessagePort,
77
89
  private sharedKey: CryptoKey,
78
- ) {}
90
+ logger?: WalletSdkLogger,
91
+ heartbeatOptions?: HeartbeatOptions,
92
+ ) {
93
+ this.log = logger ?? NOOP_LOGGER;
94
+ this.heartbeatIntervalMs = heartbeatOptions?.intervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS;
95
+ this.heartbeatDeadAfterMs = heartbeatOptions?.deadAfterMs ?? DEFAULT_HEARTBEAT_DEAD_AFTER_MS;
96
+ }
79
97
 
80
98
  /**
81
99
  * Creates a Wallet that communicates with a browser extension
@@ -86,6 +104,8 @@ export class ExtensionWallet {
86
104
  * @param sharedKey - The derived AES-256-GCM shared key for encryption
87
105
  * @param chainInfo - The chain information (chainId and version) for request context
88
106
  * @param appId - Application identifier used to identify the requesting dApp to the wallet
107
+ * @param logger - Optional logger; defaults to a no-op logger to keep extension/page bundles small
108
+ * @param heartbeatOptions - Optional override for heartbeat tuning (mostly useful for tests)
89
109
  * @returns A Wallet interface where all method calls are encrypted
90
110
  *
91
111
  * @example
@@ -109,13 +129,17 @@ export class ExtensionWallet {
109
129
  sharedKey: CryptoKey,
110
130
  chainInfo: ChainInfo,
111
131
  appId: string,
112
- ): Wallet {
113
- const wallet = new ExtensionWallet(chainInfo, appId, extensionId, port, sharedKey);
132
+ logger?: WalletSdkLogger,
133
+ heartbeatOptions?: HeartbeatOptions,
134
+ ): ExtensionWallet {
135
+ const wallet = new ExtensionWallet(chainInfo, appId, extensionId, port, sharedKey, logger, heartbeatOptions);
114
136
 
115
137
  // Set up message handler for encrypted responses and unencrypted control messages
116
138
  wallet.port.onmessage = (event: MessageEvent) => {
117
139
  const data = event.data;
118
- // Check for unencrypted disconnect notification
140
+ // Any inbound traffic counts as proof of liveness.
141
+ wallet.lastInboundAt = Date.now();
142
+
119
143
  if (data && typeof data === 'object' && 'type' in data && data.type === WalletMessageType.DISCONNECT) {
120
144
  wallet.handleDisconnect();
121
145
  return;
@@ -127,8 +151,10 @@ export class ExtensionWallet {
127
151
  wallet.port.start();
128
152
 
129
153
  return new Proxy(wallet, {
130
- get: (target, prop) => {
131
- if (schemaHasMethod(WalletSchema, prop.toString())) {
154
+ get: (target, prop, receiver) => {
155
+ if (prop === 'asWallet') {
156
+ return () => receiver as unknown as Wallet;
157
+ } else if (schemaHasMethod(WalletSchema, prop.toString())) {
132
158
  return async (...args: unknown[]) => {
133
159
  const result = await target.postMessage({
134
160
  type: prop.toString() as keyof FunctionsOf<Wallet>,
@@ -140,7 +166,13 @@ export class ExtensionWallet {
140
166
  return target[prop as keyof ExtensionWallet];
141
167
  }
142
168
  },
143
- }) as unknown as Wallet;
169
+ });
170
+ }
171
+
172
+ asWallet(): Wallet {
173
+ // Overridden by the proxy in create() to return the proxy itself.
174
+ // This body is never reached when accessed through create().
175
+ return this as unknown as Wallet;
144
176
  }
145
177
 
146
178
  /**
@@ -181,8 +213,10 @@ export class ExtensionWallet {
181
213
  resolve(result);
182
214
  }
183
215
  this.inFlight.delete(messageId);
184
- // eslint-disable-next-line no-empty
185
- } catch {}
216
+ this.maybeStopHeartbeat();
217
+ } catch (err) {
218
+ this.log.warn('Failed to decrypt wallet response', { err });
219
+ }
186
220
  }
187
221
 
188
222
  /**
@@ -220,9 +254,59 @@ export class ExtensionWallet {
220
254
 
221
255
  const { promise, resolve, reject } = promiseWithResolvers<unknown>();
222
256
  this.inFlight.set(messageId, { promise, resolve, reject });
257
+ this.startHeartbeat();
223
258
  return promise;
224
259
  }
225
260
 
261
+ /**
262
+ * Start the heartbeat probe loop while at least one request is in flight.
263
+ * Idempotent — calling while already running is a no-op.
264
+ *
265
+ * Heartbeat is opt-in via wire protocol: PINGs are unencrypted control messages
266
+ * (like DISCONNECT). Older wallets that do not understand PING simply drop it,
267
+ * which is safe — we only declare disconnect when **no** inbound traffic of any
268
+ * kind (PONG, encrypted response, DISCONNECT) arrives within the dead window.
269
+ * A wallet that is processing a slow request will reset the timer when it
270
+ * eventually responds, so this never causes false disconnects on legacy peers.
271
+ */
272
+ private startHeartbeat(): void {
273
+ if (this.heartbeatTimer !== null || this.disconnected) {
274
+ return;
275
+ }
276
+ this.lastInboundAt = Date.now();
277
+ this.heartbeatTimer = setInterval(() => this.heartbeatTick(), this.heartbeatIntervalMs);
278
+ }
279
+
280
+ private maybeStopHeartbeat(): void {
281
+ if (this.inFlight.size === 0 && this.heartbeatTimer !== null) {
282
+ clearInterval(this.heartbeatTimer);
283
+ this.heartbeatTimer = null;
284
+ }
285
+ }
286
+
287
+ private heartbeatTick(): void {
288
+ if (this.disconnected || this.inFlight.size === 0) {
289
+ this.maybeStopHeartbeat();
290
+ return;
291
+ }
292
+
293
+ const idleMs = Date.now() - this.lastInboundAt;
294
+ if (idleMs >= this.heartbeatDeadAfterMs) {
295
+ this.log.warn('Wallet channel unresponsive — declaring disconnect', {
296
+ idleMs,
297
+ inFlight: this.inFlight.size,
298
+ });
299
+ this.handleDisconnect();
300
+ return;
301
+ }
302
+
303
+ try {
304
+ this.port.postMessage({ type: WalletMessageType.PING });
305
+ } catch (err) {
306
+ this.log.warn('Failed to send heartbeat PING', { err });
307
+ }
308
+ }
309
+
226
310
  /**
227
311
  * Handles wallet disconnection.
228
312
  * Rejects all pending requests and notifies registered callbacks.
@@ -234,6 +318,11 @@ export class ExtensionWallet {
234
318
  }
235
319
  this.disconnected = true;
236
320
 
321
+ if (this.heartbeatTimer !== null) {
322
+ clearInterval(this.heartbeatTimer);
323
+ this.heartbeatTimer = null;
324
+ }
325
+
237
326
  if (this.port) {
238
327
  this.port.onmessage = null;
239
328
  this.port.close();
@@ -1,4 +1,4 @@
1
- export { ExtensionWallet, type DisconnectCallback } from './extension_wallet.js';
1
+ export { ExtensionWallet } from './extension_wallet.js';
2
2
  export {
3
3
  ExtensionProvider,
4
4
  type DiscoveredWallet,
@@ -0,0 +1,342 @@
1
+ /**
2
+ * IframeConnectionHandler — wallet-side of the cross-origin iframe protocol.
3
+ *
4
+ * This mirrors {@link BackgroundConnectionHandler} from `@aztec/wallet-sdk/extension/handlers`
5
+ * but uses `window.postMessage` instead of browser.runtime messaging.
6
+ *
7
+ * Message flow (wallet receives):
8
+ * parent → DISCOVERY → show approval UI → send DISCOVERY_RESPONSE
9
+ * parent → KEY_EXCHANGE_REQUEST → ECDH → send KEY_EXCHANGE_RESPONSE
10
+ * parent → SECURE_MESSAGE → decrypt → Wallet → encrypt → SECURE_RESPONSE
11
+ * parent → DISCONNECT → terminate session
12
+ *
13
+ * The wallet announces itself by posting WALLET_READY as soon as the handler starts,
14
+ * so the dApp knows it can send a discovery request.
15
+ */
16
+ import type { ChainInfo } from '@aztec/aztec.js/account';
17
+ import type { Wallet } from '@aztec/aztec.js/wallet';
18
+ import { WalletSchema } from '@aztec/aztec.js/wallet';
19
+ import { jsonStringify } from '@aztec/foundation/json-rpc';
20
+ import { parseWithOptionals, schemaHasMethod } from '@aztec/foundation/schemas';
21
+
22
+ import {
23
+ type EncryptedPayload,
24
+ decrypt,
25
+ deriveSessionKeys,
26
+ encrypt,
27
+ exportPublicKey,
28
+ generateKeyPair,
29
+ importPublicKey,
30
+ } from '../../crypto.js';
31
+ import { type WalletMessage, WalletMessageType, type WalletResponse, type WalletSdkLogger } from '../../types.js';
32
+
33
+ /**
34
+ * A pending discovery request from a dApp (before user approval).
35
+ */
36
+ export interface PendingSession {
37
+ /** Unique request identifier */
38
+ requestId: string;
39
+ /** Application identifier */
40
+ appId: string;
41
+ /** Origin URL of the requesting page */
42
+ origin: string;
43
+ /** Approval status */
44
+ status: 'pending' | 'approved';
45
+ }
46
+
47
+ /**
48
+ * An active session (after key exchange).
49
+ */
50
+ export interface ActiveSession {
51
+ /** Session identifier (same as the discovery requestId) */
52
+ sessionId: string;
53
+ /** AES-256-GCM shared key for this session */
54
+ sharedKey: CryptoKey;
55
+ /** Verification hash for emoji display */
56
+ verificationHash: string;
57
+ /** Origin URL of the connected dApp */
58
+ origin: string;
59
+ /** Application identifier */
60
+ appId: string;
61
+ }
62
+
63
+ /**
64
+ * Configuration for the iframe connection handler.
65
+ */
66
+ export interface IframeConnectionConfig {
67
+ /** Unique wallet identifier */
68
+ walletId: string;
69
+ /** Display name for the wallet */
70
+ walletName: string;
71
+ /** Wallet version string */
72
+ walletVersion: string;
73
+ /** Optional wallet icon URL */
74
+ walletIcon?: string;
75
+ /** Origins allowed to connect. If empty or undefined, all origins are allowed (dev mode). */
76
+ allowedOrigins?: string[];
77
+ /** Logger used for diagnostics. */
78
+ logger: WalletSdkLogger;
79
+ }
80
+
81
+ /**
82
+ * Event callbacks for the iframe connection handler.
83
+ */
84
+ export interface IframeConnectionCallbacks {
85
+ /** Called when a new discovery request arrives — wallet can show approval UI */
86
+ onPendingDiscovery?: (session: PendingSession) => void;
87
+ /** Called when a session is established (key exchange complete) */
88
+ onSessionEstablished?: (session: ActiveSession) => void;
89
+ /** Called when a session is terminated */
90
+ onSessionTerminated?: (sessionId: string) => void;
91
+ /** Called when a key exchange completes — show verificationHash as emojis to the user */
92
+ onVerificationHash?: (verificationHash: string) => void;
93
+ /**
94
+ * Resolves the Wallet instance to use for a given dApp and chain.
95
+ * Called when an encrypted message arrives and needs to be dispatched.
96
+ */
97
+ getWallet: (appId: string, chainInfo: ChainInfo) => Promise<Wallet>;
98
+ }
99
+
100
+ /**
101
+ * Handles the wallet side of the cross-origin iframe protocol.
102
+ *
103
+ * Manages the full lifecycle: discovery, ECDH key exchange, encrypted message
104
+ * dispatch to a {@link Wallet} instance, and session termination.
105
+ *
106
+ * @example
107
+ * ```typescript
108
+ * const handler = new IframeConnectionHandler(
109
+ * { walletId: 'my-wallet', walletName: 'My Wallet', walletVersion: '1.0.0', logger: console },
110
+ * {
111
+ * onPendingDiscovery: (session) => showApprovalUI(session),
112
+ * getWallet: (appId, chainInfo) => createWalletForApp(appId, chainInfo),
113
+ * },
114
+ * );
115
+ * handler.start();
116
+ * ```
117
+ */
118
+ export class IframeConnectionHandler {
119
+ private pendingSessions = new Map<string, PendingSession>();
120
+ private activeSessions = new Map<string, ActiveSession>();
121
+ private log: WalletSdkLogger;
122
+
123
+ constructor(
124
+ private config: IframeConnectionConfig,
125
+ private callbacks: IframeConnectionCallbacks,
126
+ ) {
127
+ this.log = config.logger;
128
+ }
129
+
130
+ start(): void {
131
+ window.addEventListener('message', this.handleMessage);
132
+ this.postToParent({ type: WalletMessageType.WALLET_READY });
133
+ this.log.info('IframeConnectionHandler started, posted WALLET_READY');
134
+ }
135
+
136
+ stop(): void {
137
+ window.removeEventListener('message', this.handleMessage);
138
+ }
139
+
140
+ approveDiscovery(requestId: string): void {
141
+ const pending = this.pendingSessions.get(requestId);
142
+ if (!pending || pending.status !== 'pending') {
143
+ return;
144
+ }
145
+
146
+ pending.status = 'approved';
147
+ this.postToOrigin(pending.origin, {
148
+ type: WalletMessageType.DISCOVERY_RESPONSE,
149
+ requestId,
150
+ walletInfo: {
151
+ id: this.config.walletId,
152
+ name: this.config.walletName,
153
+ version: this.config.walletVersion,
154
+ icon: this.config.walletIcon,
155
+ },
156
+ });
157
+ this.log.info(`Discovery approved for requestId=${requestId}`);
158
+ }
159
+
160
+ rejectDiscovery(requestId: string): void {
161
+ this.pendingSessions.delete(requestId);
162
+ }
163
+
164
+ terminateSession(sessionId: string): void {
165
+ const session = this.activeSessions.get(sessionId);
166
+ if (session) {
167
+ this.postToOrigin(session.origin, {
168
+ type: WalletMessageType.SESSION_DISCONNECTED,
169
+ sessionId,
170
+ });
171
+ this.activeSessions.delete(sessionId);
172
+ this.callbacks.onSessionTerminated?.(sessionId);
173
+ }
174
+ }
175
+
176
+ getPendingSessions(): PendingSession[] {
177
+ return Array.from(this.pendingSessions.values()).filter(s => s.status === 'pending');
178
+ }
179
+
180
+ private handleMessage = (event: MessageEvent): void => {
181
+ void this.handleMessageAsync(event);
182
+ };
183
+
184
+ private async handleMessageAsync(event: MessageEvent): Promise<void> {
185
+ if (this.config.allowedOrigins && this.config.allowedOrigins.length > 0) {
186
+ if (!this.config.allowedOrigins.includes(event.origin)) {
187
+ return;
188
+ }
189
+ }
190
+
191
+ const msg = event.data;
192
+ if (!msg || typeof msg !== 'object' || !msg.type) {
193
+ return;
194
+ }
195
+
196
+ switch (msg.type) {
197
+ case WalletMessageType.DISCOVERY:
198
+ this.handleDiscoveryRequest(msg, event.origin);
199
+ break;
200
+ case WalletMessageType.KEY_EXCHANGE_REQUEST:
201
+ await this.handleKeyExchangeRequest(msg, event.origin);
202
+ break;
203
+ case WalletMessageType.SECURE_MESSAGE:
204
+ await this.handleSecureMessage(msg);
205
+ break;
206
+ case WalletMessageType.DISCONNECT:
207
+ this.terminateSession(msg.sessionId);
208
+ break;
209
+ case WalletMessageType.PING:
210
+ this.handlePing(msg.sessionId);
211
+ break;
212
+ }
213
+ }
214
+
215
+ private handlePing(sessionId: string): void {
216
+ const session = this.activeSessions.get(sessionId);
217
+ if (!session) {
218
+ return;
219
+ }
220
+ this.postToOrigin(session.origin, { type: WalletMessageType.PONG, sessionId });
221
+ }
222
+
223
+ private handleDiscoveryRequest(msg: Record<string, unknown>, origin: string): void {
224
+ // eslint-disable-next-line jsdoc/require-jsdoc
225
+ const { requestId, appId } = msg as { requestId: string; appId: string };
226
+ const pending: PendingSession = { requestId, appId, origin, status: 'pending' };
227
+ this.pendingSessions.set(requestId, pending);
228
+ this.log.info(`Discovery request from appId=${appId} origin=${origin}`);
229
+ this.callbacks.onPendingDiscovery?.(pending);
230
+ }
231
+
232
+ private async handleKeyExchangeRequest(msg: Record<string, unknown>, origin: string): Promise<void> {
233
+ const { requestId, publicKey: appPublicKeyRaw } = msg as {
234
+ // eslint-disable-next-line jsdoc/require-jsdoc
235
+ requestId: string;
236
+ // eslint-disable-next-line jsdoc/require-jsdoc
237
+ publicKey: { kty: string; crv: string; x: string; y: string };
238
+ };
239
+ const pending = this.pendingSessions.get(requestId);
240
+ if (!pending || pending.status !== 'approved') {
241
+ this.log.warn(`Key exchange for unknown/unapproved requestId=${requestId}`);
242
+ return;
243
+ }
244
+
245
+ try {
246
+ const keyPair = await generateKeyPair();
247
+ const walletPublicKey = await exportPublicKey(keyPair.publicKey);
248
+ const appPublicKey = await importPublicKey(appPublicKeyRaw);
249
+ const sessionKeys = await deriveSessionKeys(keyPair, appPublicKey, false);
250
+
251
+ const session: ActiveSession = {
252
+ sessionId: requestId,
253
+ sharedKey: sessionKeys.encryptionKey,
254
+ verificationHash: sessionKeys.verificationHash,
255
+ origin: pending.origin,
256
+ appId: pending.appId,
257
+ };
258
+
259
+ this.activeSessions.set(requestId, session);
260
+ this.pendingSessions.delete(requestId);
261
+
262
+ this.postToOrigin(origin, {
263
+ type: WalletMessageType.KEY_EXCHANGE_RESPONSE,
264
+ requestId,
265
+ publicKey: walletPublicKey,
266
+ verificationHash: sessionKeys.verificationHash,
267
+ });
268
+
269
+ this.callbacks.onVerificationHash?.(sessionKeys.verificationHash);
270
+ this.callbacks.onSessionEstablished?.(session);
271
+ this.log.info(`Key exchange complete, sessionId=${requestId}`);
272
+ } catch (err) {
273
+ this.log.error(`Key exchange failed: ${err}`);
274
+ }
275
+ }
276
+
277
+ private async handleSecureMessage(msg: Record<string, unknown>): Promise<void> {
278
+ // eslint-disable-next-line jsdoc/require-jsdoc
279
+ const { sessionId, encrypted } = msg as { sessionId: string; encrypted: EncryptedPayload };
280
+ const session = this.activeSessions.get(sessionId);
281
+ if (!session) {
282
+ return;
283
+ }
284
+
285
+ let walletMessage: WalletMessage;
286
+ try {
287
+ walletMessage = await decrypt<WalletMessage>(session.sharedKey, encrypted);
288
+ } catch {
289
+ this.log.warn(`Decryption failed for sessionId=${sessionId}`);
290
+ return;
291
+ }
292
+
293
+ const { messageId, type, args, chainInfo, appId } = walletMessage;
294
+
295
+ let result: unknown;
296
+ let error: string | undefined;
297
+
298
+ try {
299
+ const wallet = await this.callbacks.getWallet(appId, chainInfo);
300
+
301
+ if (!schemaHasMethod(WalletSchema, type)) {
302
+ throw new Error(`Unknown wallet method: ${type}`);
303
+ }
304
+ // Zod's AnyZodTuple rejects optional tuple items typed as `T | undefined`
305
+ const sanitizedArgs = await parseWithOptionals(args, WalletSchema[type].parameters() as any);
306
+ result = await (wallet as Record<string, (...a: unknown[]) => Promise<unknown>>)[type](...sanitizedArgs);
307
+ } catch (err: unknown) {
308
+ error = err instanceof Error ? err.message : String(err);
309
+ this.log.error(`Error handling ${type}: ${error}`);
310
+ }
311
+
312
+ const response: WalletResponse = {
313
+ messageId,
314
+ walletId: this.config.walletId,
315
+ result,
316
+ error,
317
+ };
318
+
319
+ try {
320
+ const encryptedResponse = await encrypt(session.sharedKey, jsonStringify(response));
321
+ this.postToOrigin(session.origin, {
322
+ type: WalletMessageType.SECURE_RESPONSE,
323
+ sessionId,
324
+ encrypted: encryptedResponse,
325
+ });
326
+ } catch (err) {
327
+ this.log.error(`Encryption of response failed: ${err}`);
328
+ }
329
+ }
330
+
331
+ private postToParent(msg: object): void {
332
+ if (window.parent !== window) {
333
+ window.parent.postMessage(msg, '*');
334
+ }
335
+ }
336
+
337
+ private postToOrigin(origin: string, msg: object): void {
338
+ if (window.parent !== window) {
339
+ window.parent.postMessage(msg, origin);
340
+ }
341
+ }
342
+ }
@@ -0,0 +1,7 @@
1
+ export {
2
+ IframeConnectionHandler,
3
+ type IframeConnectionConfig,
4
+ type IframeConnectionCallbacks,
5
+ type PendingSession,
6
+ type ActiveSession,
7
+ } from './iframe_connection_handler.js';