@aztec/wallet-sdk 0.0.1-commit.f295ac2 → 0.0.1-commit.fc805bf
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/README.md +217 -294
- package/dest/base-wallet/base_wallet.d.ts +20 -7
- package/dest/base-wallet/base_wallet.d.ts.map +1 -1
- package/dest/base-wallet/base_wallet.js +29 -11
- package/dest/crypto.d.ts +59 -50
- package/dest/crypto.d.ts.map +1 -1
- package/dest/crypto.js +202 -108
- package/dest/emoji_alphabet.d.ts +35 -0
- package/dest/emoji_alphabet.d.ts.map +1 -0
- package/dest/emoji_alphabet.js +299 -0
- package/dest/extension/handlers/background_connection_handler.d.ts +158 -0
- package/dest/extension/handlers/background_connection_handler.d.ts.map +1 -0
- package/dest/extension/handlers/background_connection_handler.js +258 -0
- package/dest/extension/handlers/content_script_connection_handler.d.ts +56 -0
- package/dest/extension/handlers/content_script_connection_handler.d.ts.map +1 -0
- package/dest/extension/handlers/content_script_connection_handler.js +174 -0
- package/dest/extension/handlers/index.d.ts +12 -0
- package/dest/extension/handlers/index.d.ts.map +1 -0
- package/dest/extension/handlers/index.js +10 -0
- package/dest/extension/handlers/internal_message_types.d.ts +63 -0
- package/dest/extension/handlers/internal_message_types.d.ts.map +1 -0
- package/dest/extension/handlers/internal_message_types.js +22 -0
- package/dest/extension/provider/extension_provider.d.ts +107 -0
- package/dest/extension/provider/extension_provider.d.ts.map +1 -0
- package/dest/extension/provider/extension_provider.js +160 -0
- package/dest/extension/provider/extension_wallet.d.ts +131 -0
- package/dest/extension/provider/extension_wallet.d.ts.map +1 -0
- package/dest/{providers/extension → extension/provider}/extension_wallet.js +48 -95
- package/dest/extension/provider/index.d.ts +3 -0
- package/dest/extension/provider/index.d.ts.map +1 -0
- package/dest/{providers/extension → extension/provider}/index.js +0 -2
- package/dest/manager/index.d.ts +2 -8
- package/dest/manager/index.d.ts.map +1 -1
- package/dest/manager/index.js +0 -6
- package/dest/manager/types.d.ts +88 -6
- package/dest/manager/types.d.ts.map +1 -1
- package/dest/manager/types.js +17 -1
- package/dest/manager/wallet_manager.d.ts +50 -7
- package/dest/manager/wallet_manager.d.ts.map +1 -1
- package/dest/manager/wallet_manager.js +174 -44
- package/dest/types.d.ts +43 -12
- package/dest/types.d.ts.map +1 -1
- package/dest/types.js +3 -2
- package/package.json +10 -9
- package/src/base-wallet/base_wallet.ts +45 -20
- package/src/crypto.ts +237 -113
- package/src/emoji_alphabet.ts +317 -0
- package/src/extension/handlers/background_connection_handler.ts +423 -0
- package/src/extension/handlers/content_script_connection_handler.ts +246 -0
- package/src/extension/handlers/index.ts +25 -0
- package/src/extension/handlers/internal_message_types.ts +69 -0
- package/src/extension/provider/extension_provider.ts +233 -0
- package/src/{providers/extension → extension/provider}/extension_wallet.ts +52 -110
- package/src/extension/provider/index.ts +7 -0
- package/src/manager/index.ts +2 -10
- package/src/manager/types.ts +91 -5
- package/src/manager/wallet_manager.ts +192 -46
- package/src/types.ts +44 -10
- package/dest/providers/extension/extension_provider.d.ts +0 -63
- package/dest/providers/extension/extension_provider.d.ts.map +0 -1
- package/dest/providers/extension/extension_provider.js +0 -124
- package/dest/providers/extension/extension_wallet.d.ts +0 -155
- package/dest/providers/extension/extension_wallet.d.ts.map +0 -1
- package/dest/providers/extension/index.d.ts +0 -6
- package/dest/providers/extension/index.d.ts.map +0 -1
- package/src/providers/extension/extension_provider.ts +0 -167
- package/src/providers/extension/index.ts +0 -5
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
import type { ChainInfo } from '@aztec/aztec.js/account';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type EncryptedPayload,
|
|
5
|
+
decrypt,
|
|
6
|
+
deriveSessionKeys,
|
|
7
|
+
encrypt,
|
|
8
|
+
exportPublicKey,
|
|
9
|
+
generateKeyPair,
|
|
10
|
+
importPublicKey,
|
|
11
|
+
} from '../../crypto.js';
|
|
12
|
+
import {
|
|
13
|
+
type DiscoveryRequest,
|
|
14
|
+
type KeyExchangeRequest,
|
|
15
|
+
type KeyExchangeResponse,
|
|
16
|
+
type WalletInfo,
|
|
17
|
+
type WalletMessage,
|
|
18
|
+
WalletMessageType,
|
|
19
|
+
type WalletResponse,
|
|
20
|
+
} from '../../types.js';
|
|
21
|
+
import {
|
|
22
|
+
type BackgroundMessage,
|
|
23
|
+
type ContentScriptMessage,
|
|
24
|
+
InternalMessageType,
|
|
25
|
+
MessageOrigin,
|
|
26
|
+
type MessageSender,
|
|
27
|
+
} from './internal_message_types.js';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Status of a pending discovery request.
|
|
31
|
+
*/
|
|
32
|
+
export type DiscoveryStatus = 'pending' | 'approved' | 'rejected';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A pending discovery request from a dApp.
|
|
36
|
+
*/
|
|
37
|
+
export interface PendingDiscovery {
|
|
38
|
+
/** Unique request identifier. */
|
|
39
|
+
requestId: string;
|
|
40
|
+
/** Application identifier. */
|
|
41
|
+
appId: string;
|
|
42
|
+
/** Optional application name. */
|
|
43
|
+
appName?: string;
|
|
44
|
+
/** Origin URL of the requesting page. */
|
|
45
|
+
origin: string;
|
|
46
|
+
/** Network information. */
|
|
47
|
+
chainInfo: ChainInfo;
|
|
48
|
+
/** Browser tab ID. */
|
|
49
|
+
tabId: number;
|
|
50
|
+
/** Request timestamp. */
|
|
51
|
+
timestamp: number;
|
|
52
|
+
/** Current status. */
|
|
53
|
+
status: DiscoveryStatus;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* An active session with a connected dApp.
|
|
58
|
+
* Created after key exchange completes.
|
|
59
|
+
*/
|
|
60
|
+
export interface ActiveSession {
|
|
61
|
+
/** Unique session identifier. */
|
|
62
|
+
sessionId: string;
|
|
63
|
+
/** Derived AES-GCM encryption key. */
|
|
64
|
+
sharedKey: CryptoKey;
|
|
65
|
+
/** Hex-encoded verification hash for visual comparison. */
|
|
66
|
+
verificationHash: string;
|
|
67
|
+
/** Browser tab ID. */
|
|
68
|
+
tabId: number;
|
|
69
|
+
/** Origin URL of the connected app. */
|
|
70
|
+
origin: string;
|
|
71
|
+
/** Application identifier. */
|
|
72
|
+
appId: string;
|
|
73
|
+
/** Connection timestamp. */
|
|
74
|
+
connectedAt: number;
|
|
75
|
+
/** Network information. */
|
|
76
|
+
chainInfo: ChainInfo;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Transport interface for background script communication.
|
|
81
|
+
*/
|
|
82
|
+
export interface BackgroundTransport {
|
|
83
|
+
/**
|
|
84
|
+
* Send a message to a specific tab.
|
|
85
|
+
* Typically `(tabId, message) => browser.tabs.sendMessage(tabId, message)`.
|
|
86
|
+
*/
|
|
87
|
+
sendToTab: (tabId: number, message: BackgroundMessage) => void;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Register a listener for messages from content scripts.
|
|
91
|
+
* Typically `browser.runtime.onMessage.addListener`.
|
|
92
|
+
*/
|
|
93
|
+
addContentListener: (handler: (message: unknown, sender: MessageSender) => void) => void;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Event callbacks for the background connection handler.
|
|
98
|
+
* All callbacks are optional.
|
|
99
|
+
*/
|
|
100
|
+
export interface BackgroundConnectionCallbacks {
|
|
101
|
+
/**
|
|
102
|
+
* Called when a new discovery request is received and stored as pending.
|
|
103
|
+
*/
|
|
104
|
+
onPendingDiscovery?: (discovery: PendingDiscovery) => void;
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Called when a session is established (key exchange complete).
|
|
108
|
+
*/
|
|
109
|
+
onSessionEstablished?: (session: ActiveSession) => void;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Called when a session is terminated.
|
|
113
|
+
*/
|
|
114
|
+
onSessionTerminated?: (sessionId: string) => void;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Called when a decrypted wallet message is received.
|
|
118
|
+
*/
|
|
119
|
+
onWalletMessage?: (session: ActiveSession, message: WalletMessage) => void;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Configuration for the background connection handler.
|
|
124
|
+
*/
|
|
125
|
+
export interface BackgroundConnectionConfig {
|
|
126
|
+
/** Unique wallet identifier. */
|
|
127
|
+
walletId: string;
|
|
128
|
+
/** Display name for the wallet. */
|
|
129
|
+
walletName: string;
|
|
130
|
+
/** Wallet version string. */
|
|
131
|
+
walletVersion: string;
|
|
132
|
+
/** Optional wallet icon URL. */
|
|
133
|
+
walletIcon?: string;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Handles wallet session flow in the extension background script.
|
|
138
|
+
*
|
|
139
|
+
* This class manages:
|
|
140
|
+
* - Pending discovery requests (before user approval)
|
|
141
|
+
* - Active sessions (after key exchange)
|
|
142
|
+
* - Per-session ECDH key exchange
|
|
143
|
+
* - Message encryption/decryption
|
|
144
|
+
*
|
|
145
|
+
* @example
|
|
146
|
+
* ```typescript
|
|
147
|
+
* const handler = new BackgroundConnectionHandler(
|
|
148
|
+
* {
|
|
149
|
+
* walletId: 'my-wallet',
|
|
150
|
+
* walletName: 'My Wallet',
|
|
151
|
+
* walletVersion: '1.0.0',
|
|
152
|
+
* },
|
|
153
|
+
* {
|
|
154
|
+
* sendToTab: (tabId, message) => browser.tabs.sendMessage(tabId, message),
|
|
155
|
+
* addContentListener: (handler) => browser.runtime.onMessage.addListener(handler),
|
|
156
|
+
* },
|
|
157
|
+
* {
|
|
158
|
+
* onPendingDiscovery: (discovery) => updateBadge(),
|
|
159
|
+
* onSessionEstablished: (session) => console.log('Connected:', session.sessionId),
|
|
160
|
+
* onWalletMessage: (session, message) => nativePort.postMessage(message),
|
|
161
|
+
* }
|
|
162
|
+
* );
|
|
163
|
+
*
|
|
164
|
+
* handler.initialize();
|
|
165
|
+
* ```
|
|
166
|
+
*/
|
|
167
|
+
export class BackgroundConnectionHandler {
|
|
168
|
+
private pendingDiscoveries = new Map<string, PendingDiscovery>();
|
|
169
|
+
private activeSessions = new Map<string, ActiveSession>();
|
|
170
|
+
|
|
171
|
+
constructor(
|
|
172
|
+
private config: BackgroundConnectionConfig,
|
|
173
|
+
private transport: BackgroundTransport,
|
|
174
|
+
private callbacks: BackgroundConnectionCallbacks = {},
|
|
175
|
+
) {}
|
|
176
|
+
|
|
177
|
+
initialize(): void {
|
|
178
|
+
this.transport.addContentListener(this.handleMessage);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private handleMessage = (message: unknown, sender: MessageSender): void => {
|
|
182
|
+
const msg = message as ContentScriptMessage;
|
|
183
|
+
if (msg.origin !== MessageOrigin.CONTENT_SCRIPT) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const tabId = sender.tab?.id;
|
|
188
|
+
const tabOrigin = sender.tab?.url ? new URL(sender.tab.url).origin : 'unknown';
|
|
189
|
+
if (!tabId) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const { type, sessionId, content } = msg;
|
|
194
|
+
|
|
195
|
+
switch (type) {
|
|
196
|
+
case InternalMessageType.DISCOVERY_REQUEST:
|
|
197
|
+
this.handleDiscoveryRequest(content as DiscoveryRequest, tabId, tabOrigin);
|
|
198
|
+
break;
|
|
199
|
+
case InternalMessageType.KEY_EXCHANGE_REQUEST:
|
|
200
|
+
if (sessionId) {
|
|
201
|
+
this.handleKeyExchangeRequest(sessionId, content as KeyExchangeRequest).catch(() => {
|
|
202
|
+
// Key exchange failed - session won't be established
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
break;
|
|
206
|
+
case InternalMessageType.DISCONNECT_REQUEST:
|
|
207
|
+
if (sessionId) {
|
|
208
|
+
this.terminateSession(sessionId);
|
|
209
|
+
}
|
|
210
|
+
break;
|
|
211
|
+
case InternalMessageType.SECURE_MESSAGE:
|
|
212
|
+
if (sessionId) {
|
|
213
|
+
void this.handleEncryptedMessage(sessionId, content as EncryptedPayload);
|
|
214
|
+
}
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
getWalletInfo(): WalletInfo {
|
|
220
|
+
return {
|
|
221
|
+
id: this.config.walletId,
|
|
222
|
+
name: this.config.walletName,
|
|
223
|
+
version: this.config.walletVersion,
|
|
224
|
+
icon: this.config.walletIcon,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
handleDiscoveryRequest(request: DiscoveryRequest, tabId: number, origin: string): void {
|
|
229
|
+
const discovery: PendingDiscovery = {
|
|
230
|
+
requestId: request.requestId,
|
|
231
|
+
appId: request.appId,
|
|
232
|
+
origin,
|
|
233
|
+
chainInfo: request.chainInfo,
|
|
234
|
+
tabId,
|
|
235
|
+
timestamp: Date.now(),
|
|
236
|
+
status: 'pending',
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
this.pendingDiscoveries.set(request.requestId, discovery);
|
|
240
|
+
this.callbacks.onPendingDiscovery?.(discovery);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
approveDiscovery(requestId: string): boolean {
|
|
244
|
+
const discovery = this.pendingDiscoveries.get(requestId);
|
|
245
|
+
if (!discovery || discovery.status !== 'pending') {
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// The discovery requestId becomes our sessionId
|
|
250
|
+
// This is what will be used internally to correlate
|
|
251
|
+
// content<->background messages
|
|
252
|
+
const sessionId = requestId;
|
|
253
|
+
|
|
254
|
+
discovery.status = 'approved';
|
|
255
|
+
this.transport.sendToTab(discovery.tabId, {
|
|
256
|
+
origin: MessageOrigin.BACKGROUND,
|
|
257
|
+
type: InternalMessageType.DISCOVERY_APPROVED,
|
|
258
|
+
sessionId,
|
|
259
|
+
content: this.getWalletInfo(),
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
rejectDiscovery(requestId: string): boolean {
|
|
266
|
+
const discovery = this.pendingDiscoveries.get(requestId);
|
|
267
|
+
if (!discovery || discovery.status !== 'pending') {
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
discovery.status = 'rejected';
|
|
272
|
+
this.pendingDiscoveries.delete(requestId);
|
|
273
|
+
|
|
274
|
+
return true;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async handleKeyExchangeRequest(sessionId: string, request: KeyExchangeRequest): Promise<void> {
|
|
278
|
+
const discovery = this.pendingDiscoveries.get(sessionId);
|
|
279
|
+
if (!discovery || discovery.status !== 'approved') {
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
try {
|
|
284
|
+
const keyPair = await generateKeyPair();
|
|
285
|
+
const publicKey = await exportPublicKey(keyPair.publicKey);
|
|
286
|
+
|
|
287
|
+
const appPublicKey = await importPublicKey(request.publicKey);
|
|
288
|
+
const sessionKeys = await deriveSessionKeys(keyPair, appPublicKey, false);
|
|
289
|
+
|
|
290
|
+
const session: ActiveSession = {
|
|
291
|
+
sessionId,
|
|
292
|
+
sharedKey: sessionKeys.encryptionKey,
|
|
293
|
+
verificationHash: sessionKeys.verificationHash,
|
|
294
|
+
tabId: discovery.tabId,
|
|
295
|
+
origin: discovery.origin,
|
|
296
|
+
appId: discovery.appId,
|
|
297
|
+
connectedAt: Date.now(),
|
|
298
|
+
chainInfo: discovery.chainInfo,
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
this.activeSessions.set(sessionId, session);
|
|
302
|
+
this.pendingDiscoveries.delete(sessionId);
|
|
303
|
+
|
|
304
|
+
const response: KeyExchangeResponse = {
|
|
305
|
+
type: WalletMessageType.KEY_EXCHANGE_RESPONSE,
|
|
306
|
+
requestId: sessionId, // Public protocol uses requestId
|
|
307
|
+
publicKey,
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
this.transport.sendToTab(discovery.tabId, {
|
|
311
|
+
origin: MessageOrigin.BACKGROUND,
|
|
312
|
+
type: InternalMessageType.KEY_EXCHANGE_RESPONSE,
|
|
313
|
+
sessionId,
|
|
314
|
+
content: response,
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
this.callbacks.onSessionEstablished?.(session);
|
|
318
|
+
} catch {
|
|
319
|
+
// Key exchange failed silently - session won't be established
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
async handleEncryptedMessage(sessionId: string, encrypted: EncryptedPayload): Promise<void> {
|
|
324
|
+
const session = this.activeSessions.get(sessionId);
|
|
325
|
+
if (!session) {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
try {
|
|
330
|
+
const message = await decrypt<WalletMessage>(session.sharedKey, encrypted);
|
|
331
|
+
this.callbacks.onWalletMessage?.(session, message);
|
|
332
|
+
} catch {
|
|
333
|
+
// Decryption failed - ignore malformed message
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async sendResponse(sessionId: string, response: WalletResponse): Promise<void> {
|
|
338
|
+
const session = this.activeSessions.get(sessionId);
|
|
339
|
+
if (!session) {
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
try {
|
|
344
|
+
const encrypted = await encrypt(session.sharedKey, JSON.stringify(response));
|
|
345
|
+
this.transport.sendToTab(session.tabId, {
|
|
346
|
+
origin: MessageOrigin.BACKGROUND,
|
|
347
|
+
type: InternalMessageType.SECURE_RESPONSE,
|
|
348
|
+
sessionId,
|
|
349
|
+
content: encrypted,
|
|
350
|
+
});
|
|
351
|
+
} catch {
|
|
352
|
+
// Encryption failed - response won't be sent
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
terminateSession(sessionId: string): void {
|
|
357
|
+
const session = this.activeSessions.get(sessionId);
|
|
358
|
+
if (session) {
|
|
359
|
+
// Notify the content script (and ultimately the dApp) that the session is disconnected
|
|
360
|
+
this.transport.sendToTab(session.tabId, {
|
|
361
|
+
origin: MessageOrigin.BACKGROUND,
|
|
362
|
+
type: InternalMessageType.SESSION_DISCONNECTED,
|
|
363
|
+
sessionId,
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
this.activeSessions.delete(sessionId);
|
|
367
|
+
this.callbacks.onSessionTerminated?.(sessionId);
|
|
368
|
+
|
|
369
|
+
// Restore discovery to approved state so user can retry key exchange
|
|
370
|
+
const discovery: PendingDiscovery = {
|
|
371
|
+
requestId: sessionId,
|
|
372
|
+
appId: session.appId,
|
|
373
|
+
origin: session.origin,
|
|
374
|
+
chainInfo: session.chainInfo,
|
|
375
|
+
tabId: session.tabId,
|
|
376
|
+
timestamp: Date.now(),
|
|
377
|
+
status: 'approved',
|
|
378
|
+
};
|
|
379
|
+
this.pendingDiscoveries.set(sessionId, discovery);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
terminateForTab(tabId: number): void {
|
|
384
|
+
for (const [sessionId, session] of this.activeSessions) {
|
|
385
|
+
if (session.tabId === tabId) {
|
|
386
|
+
this.terminateSession(sessionId);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
for (const [requestId, discovery] of this.pendingDiscoveries) {
|
|
390
|
+
if (discovery.tabId === tabId) {
|
|
391
|
+
this.pendingDiscoveries.delete(requestId);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
clearAll(): void {
|
|
397
|
+
for (const sessionId of this.activeSessions.keys()) {
|
|
398
|
+
this.callbacks.onSessionTerminated?.(sessionId);
|
|
399
|
+
}
|
|
400
|
+
this.activeSessions.clear();
|
|
401
|
+
this.pendingDiscoveries.clear();
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
getPendingDiscoveries(): PendingDiscovery[] {
|
|
405
|
+
return Array.from(this.pendingDiscoveries.values()).filter(d => d.status === 'pending');
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
getPendingDiscoveryCount(): number {
|
|
409
|
+
return this.getPendingDiscoveries().length;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
getActiveSessions(): ActiveSession[] {
|
|
413
|
+
return Array.from(this.activeSessions.values());
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
getSession(sessionId: string): ActiveSession | undefined {
|
|
417
|
+
return this.activeSessions.get(sessionId);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
getPendingDiscovery(requestId: string): PendingDiscovery | undefined {
|
|
421
|
+
return this.pendingDiscoveries.get(requestId);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import type { EncryptedPayload, ExportedPublicKey } from '../../crypto.js';
|
|
2
|
+
import { type DiscoveryRequest, type DiscoveryResponse, type WalletInfo, WalletMessageType } from '../../types.js';
|
|
3
|
+
import {
|
|
4
|
+
type BackgroundMessage,
|
|
5
|
+
type ContentScriptMessage,
|
|
6
|
+
InternalMessageType,
|
|
7
|
+
MessageOrigin,
|
|
8
|
+
} from './internal_message_types.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Key exchange request sent over MessageChannel.
|
|
12
|
+
*/
|
|
13
|
+
interface KeyExchangeRequest {
|
|
14
|
+
/** Message type identifier. */
|
|
15
|
+
type: typeof WalletMessageType.KEY_EXCHANGE_REQUEST;
|
|
16
|
+
/** Request identifier for correlation. */
|
|
17
|
+
requestId: string;
|
|
18
|
+
/** ECDH public key in JWK format. */
|
|
19
|
+
publicKey: ExportedPublicKey;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Key exchange response sent over MessageChannel.
|
|
24
|
+
*/
|
|
25
|
+
interface KeyExchangeResponse {
|
|
26
|
+
/** Message type identifier. */
|
|
27
|
+
type: typeof WalletMessageType.KEY_EXCHANGE_RESPONSE;
|
|
28
|
+
/** Request identifier for correlation. */
|
|
29
|
+
requestId: string;
|
|
30
|
+
/** ECDH public key in JWK format. */
|
|
31
|
+
publicKey: ExportedPublicKey;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Port connection stored by the content script.
|
|
36
|
+
*/
|
|
37
|
+
interface PortConnection {
|
|
38
|
+
/** MessagePort for communication with page. */
|
|
39
|
+
port: MessagePort;
|
|
40
|
+
/** Session identifier. */
|
|
41
|
+
sessionId: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Transport interface for content script communication.
|
|
46
|
+
*/
|
|
47
|
+
export interface ContentScriptTransport {
|
|
48
|
+
/**
|
|
49
|
+
* Send a message to the background script.
|
|
50
|
+
* Typically `browser.runtime.sendMessage`.
|
|
51
|
+
*/
|
|
52
|
+
sendToBackground: (message: ContentScriptMessage) => void;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Register a listener for messages from the background script.
|
|
56
|
+
* Typically `browser.runtime.onMessage.addListener`.
|
|
57
|
+
*/
|
|
58
|
+
addBackgroundListener: (handler: (message: BackgroundMessage) => void) => void;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Handles wallet connection flow in the extension content script.
|
|
63
|
+
*
|
|
64
|
+
* This class manages:
|
|
65
|
+
* - Listening for discovery requests from the page
|
|
66
|
+
* - Creating MessageChannels after discovery approval
|
|
67
|
+
* - Relaying key exchange messages between page and background
|
|
68
|
+
* - Relaying encrypted messages between page and background
|
|
69
|
+
*
|
|
70
|
+
* The content script acts as a pure relay - it never has access to
|
|
71
|
+
* private keys or shared secrets.
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* ```typescript
|
|
75
|
+
* const handler = new ContentScriptConnectionHandler({
|
|
76
|
+
* sendToBackground: (message) => browser.runtime.sendMessage(message),
|
|
77
|
+
* addBackgroundListener: (handler) => browser.runtime.onMessage.addListener(handler),
|
|
78
|
+
* });
|
|
79
|
+
*
|
|
80
|
+
* handler.start();
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
export class ContentScriptConnectionHandler {
|
|
84
|
+
private ports = new Map<string, PortConnection>();
|
|
85
|
+
private listening = false;
|
|
86
|
+
private pageMessageHandler: ((event: MessageEvent) => void) | null = null;
|
|
87
|
+
|
|
88
|
+
constructor(private transport: ContentScriptTransport) {}
|
|
89
|
+
|
|
90
|
+
start(): void {
|
|
91
|
+
if (this.listening) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
this.transport.addBackgroundListener(this.handleBackgroundMessage);
|
|
96
|
+
|
|
97
|
+
this.pageMessageHandler = (event: MessageEvent) => {
|
|
98
|
+
if (event.source !== window) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let data: DiscoveryRequest;
|
|
103
|
+
try {
|
|
104
|
+
data = JSON.parse(event.data);
|
|
105
|
+
} catch {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!data?.type) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (data.type === WalletMessageType.DISCOVERY) {
|
|
114
|
+
this.handleDiscoveryRequest(data);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
window.addEventListener('message', this.pageMessageHandler);
|
|
119
|
+
this.listening = true;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private handleBackgroundMessage = (message: BackgroundMessage): void => {
|
|
123
|
+
if (message.origin !== MessageOrigin.BACKGROUND) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const { type, sessionId, content } = message;
|
|
128
|
+
|
|
129
|
+
switch (type) {
|
|
130
|
+
case InternalMessageType.DISCOVERY_APPROVED:
|
|
131
|
+
this.handleDiscoveryApproved(sessionId, content as WalletInfo);
|
|
132
|
+
break;
|
|
133
|
+
case InternalMessageType.KEY_EXCHANGE_RESPONSE:
|
|
134
|
+
this.handleKeyExchangeResponse(sessionId, content as KeyExchangeResponse);
|
|
135
|
+
break;
|
|
136
|
+
case InternalMessageType.SECURE_RESPONSE:
|
|
137
|
+
this.handleSecureResponse(sessionId, content as EncryptedPayload);
|
|
138
|
+
break;
|
|
139
|
+
case InternalMessageType.SESSION_DISCONNECTED:
|
|
140
|
+
this.handleSessionDisconnected(sessionId);
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
private handleDiscoveryRequest(request: DiscoveryRequest): void {
|
|
146
|
+
this.transport.sendToBackground({
|
|
147
|
+
origin: MessageOrigin.CONTENT_SCRIPT,
|
|
148
|
+
type: InternalMessageType.DISCOVERY_REQUEST,
|
|
149
|
+
content: request,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private handleDiscoveryApproved(sessionId: string, walletInfo: WalletInfo): void {
|
|
154
|
+
const channel = new MessageChannel();
|
|
155
|
+
|
|
156
|
+
this.ports.set(sessionId, {
|
|
157
|
+
port: channel.port1,
|
|
158
|
+
sessionId,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
channel.port1.onmessage = (event: MessageEvent) => {
|
|
162
|
+
const data = event.data;
|
|
163
|
+
|
|
164
|
+
switch (data?.type) {
|
|
165
|
+
case WalletMessageType.KEY_EXCHANGE_REQUEST:
|
|
166
|
+
this.transport.sendToBackground({
|
|
167
|
+
origin: MessageOrigin.CONTENT_SCRIPT,
|
|
168
|
+
type: InternalMessageType.KEY_EXCHANGE_REQUEST,
|
|
169
|
+
sessionId,
|
|
170
|
+
content: data as KeyExchangeRequest,
|
|
171
|
+
});
|
|
172
|
+
break;
|
|
173
|
+
case WalletMessageType.DISCONNECT:
|
|
174
|
+
this.transport.sendToBackground({
|
|
175
|
+
origin: MessageOrigin.CONTENT_SCRIPT,
|
|
176
|
+
type: InternalMessageType.DISCONNECT_REQUEST,
|
|
177
|
+
sessionId,
|
|
178
|
+
content: data,
|
|
179
|
+
});
|
|
180
|
+
break;
|
|
181
|
+
default:
|
|
182
|
+
this.transport.sendToBackground({
|
|
183
|
+
origin: MessageOrigin.CONTENT_SCRIPT,
|
|
184
|
+
type: InternalMessageType.SECURE_MESSAGE,
|
|
185
|
+
sessionId,
|
|
186
|
+
content: data as EncryptedPayload,
|
|
187
|
+
});
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
channel.port1.start();
|
|
193
|
+
|
|
194
|
+
const response: DiscoveryResponse = {
|
|
195
|
+
type: WalletMessageType.DISCOVERY_RESPONSE,
|
|
196
|
+
requestId: sessionId,
|
|
197
|
+
walletInfo,
|
|
198
|
+
};
|
|
199
|
+
window.postMessage(JSON.stringify(response), '*', [channel.port2]);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private handleKeyExchangeResponse(sessionId: string, response: KeyExchangeResponse): void {
|
|
203
|
+
const connection = this.ports.get(sessionId);
|
|
204
|
+
if (!connection) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
connection.port.postMessage(response);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private handleSecureResponse(sessionId: string, content: EncryptedPayload): void {
|
|
211
|
+
const connection = this.ports.get(sessionId);
|
|
212
|
+
if (!connection) {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
connection.port.postMessage(content);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private handleSessionDisconnected(sessionId: string): void {
|
|
219
|
+
const connection = this.ports.get(sessionId);
|
|
220
|
+
if (!connection) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
connection.port.postMessage({ type: WalletMessageType.DISCONNECT });
|
|
224
|
+
connection.port.close();
|
|
225
|
+
this.ports.delete(sessionId);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
closeConnection(sessionId: string): void {
|
|
229
|
+
const connection = this.ports.get(sessionId);
|
|
230
|
+
if (connection) {
|
|
231
|
+
connection.port.close();
|
|
232
|
+
this.ports.delete(sessionId);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
closeAllConnections(): void {
|
|
237
|
+
for (const [sessionId, connection] of this.ports) {
|
|
238
|
+
connection.port.close();
|
|
239
|
+
this.ports.delete(sessionId);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
getConnectionCount(): number {
|
|
244
|
+
return this.ports.size;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-safe exports for extension background and content scripts.
|
|
3
|
+
*
|
|
4
|
+
* This module contains ONLY handlers that work in service worker/content script
|
|
5
|
+
* environments without Node.js polyfills.
|
|
6
|
+
*
|
|
7
|
+
* @packageDocumentation
|
|
8
|
+
*/
|
|
9
|
+
export {
|
|
10
|
+
BackgroundConnectionHandler,
|
|
11
|
+
type PendingDiscovery,
|
|
12
|
+
type ActiveSession,
|
|
13
|
+
type DiscoveryStatus,
|
|
14
|
+
type BackgroundConnectionCallbacks,
|
|
15
|
+
type BackgroundConnectionConfig,
|
|
16
|
+
type BackgroundTransport,
|
|
17
|
+
} from './background_connection_handler.js';
|
|
18
|
+
export { ContentScriptConnectionHandler, type ContentScriptTransport } from './content_script_connection_handler.js';
|
|
19
|
+
export {
|
|
20
|
+
type ContentScriptMessage,
|
|
21
|
+
type BackgroundMessage,
|
|
22
|
+
type MessageSender,
|
|
23
|
+
type MessageOriginType,
|
|
24
|
+
MessageOrigin,
|
|
25
|
+
} from './internal_message_types.js';
|