walletpair-sdk 1.0.2 → 1.0.5

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 (74) hide show
  1. package/README.md +13 -0
  2. package/dist/ble/framing.d.ts.map +1 -1
  3. package/dist/ble/framing.js +2 -2
  4. package/dist/ble/framing.js.map +1 -1
  5. package/dist/ble/index.d.ts +2 -2
  6. package/dist/ble/index.d.ts.map +1 -1
  7. package/dist/ble/index.js +2 -2
  8. package/dist/ble/index.js.map +1 -1
  9. package/dist/ble/web-ble-transport.d.ts +1 -1
  10. package/dist/ble/web-ble-transport.d.ts.map +1 -1
  11. package/dist/ble/web-ble-transport.js +23 -12
  12. package/dist/ble/web-ble-transport.js.map +1 -1
  13. package/dist/crypto.d.ts.map +1 -1
  14. package/dist/crypto.js +29 -12
  15. package/dist/crypto.js.map +1 -1
  16. package/dist/dapp-session.d.ts.map +1 -1
  17. package/dist/dapp-session.js +15 -5
  18. package/dist/dapp-session.js.map +1 -1
  19. package/dist/emitter.d.ts +1 -3
  20. package/dist/emitter.d.ts.map +1 -1
  21. package/dist/emitter.js +4 -2
  22. package/dist/emitter.js.map +1 -1
  23. package/dist/evm/eip1193.d.ts +2 -2
  24. package/dist/evm/eip1193.d.ts.map +1 -1
  25. package/dist/evm/eip1193.js +32 -18
  26. package/dist/evm/eip1193.js.map +1 -1
  27. package/dist/evm/index.d.ts +2 -2
  28. package/dist/evm/index.d.ts.map +1 -1
  29. package/dist/evm/index.js.map +1 -1
  30. package/dist/wallet-session.d.ts.map +1 -1
  31. package/dist/wallet-session.js +4 -3
  32. package/dist/wallet-session.js.map +1 -1
  33. package/dist/ws-transport.d.ts +3 -2
  34. package/dist/ws-transport.d.ts.map +1 -1
  35. package/dist/ws-transport.js +13 -4
  36. package/dist/ws-transport.js.map +1 -1
  37. package/package.json +20 -1
  38. package/src/__tests__/adversarial/crypto-attacks.test.ts +240 -233
  39. package/src/__tests__/adversarial/malicious-dapp.test.ts +228 -194
  40. package/src/__tests__/adversarial/malicious-relay.test.ts +292 -220
  41. package/src/__tests__/adversarial/malicious-wallet.test.ts +246 -180
  42. package/src/__tests__/spec-compliance/canonical-json.test.ts +105 -105
  43. package/src/__tests__/spec-compliance/crypto-vectors.test.ts +149 -154
  44. package/src/__tests__/spec-compliance/message-format.test.ts +180 -151
  45. package/src/__tests__/spec-compliance/sequence-numbers.test.ts +142 -149
  46. package/src/__tests__/spec-compliance/state-machine.test.ts +203 -180
  47. package/src/ble/framing.test.ts +122 -114
  48. package/src/ble/framing.ts +48 -51
  49. package/src/ble/index.ts +7 -7
  50. package/src/ble/web-ble-transport.test.ts +93 -84
  51. package/src/ble/web-ble-transport.ts +70 -57
  52. package/src/ble/web-bluetooth.d.ts +19 -19
  53. package/src/canonical-json.test.ts +301 -285
  54. package/src/crypto-directional.test.ts +155 -129
  55. package/src/crypto-hardening.test.ts +292 -283
  56. package/src/crypto.test.ts +364 -346
  57. package/src/crypto.ts +185 -175
  58. package/src/dapp-session.test.ts +522 -385
  59. package/src/dapp-session.ts +17 -11
  60. package/src/emitter.test.ts +122 -122
  61. package/src/emitter.ts +20 -18
  62. package/src/evm/eip1193.test.ts +283 -205
  63. package/src/evm/eip1193.ts +162 -138
  64. package/src/evm/index.ts +5 -5
  65. package/src/evm/wagmi.test.ts +1 -1
  66. package/src/integration.test.ts +329 -201
  67. package/src/security.test.ts +331 -238
  68. package/src/sequence-validation.test.ts +6 -9
  69. package/src/test-helpers.ts +102 -78
  70. package/src/types.test.ts +45 -50
  71. package/src/wallet-session.test.ts +611 -383
  72. package/src/wallet-session.ts +7 -9
  73. package/src/ws-transport.test.ts +141 -139
  74. package/src/ws-transport.ts +52 -41
@@ -1,27 +1,24 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import { DAppSession } from './dapp-session.js';
3
- import { WalletSession } from './wallet-session.js';
4
- import { makeJoinBody, MockTransport, MockRelay } from './test-helpers.js';
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
2
+ import type { AadHeader } from './crypto.js'
5
3
  import {
6
- generateX25519KeyPair,
4
+ b64urlDecode,
5
+ computeSessionFingerprint,
7
6
  computeSharedSecret,
8
7
  deriveSessionKey,
9
- deriveDirectionalSessionKeys,
10
- computeSessionFingerprint,
11
- sealPayload,
12
- b64urlEncode,
13
- b64urlDecode,
8
+ generateX25519KeyPair,
14
9
  parsePairingUri,
15
- } from './crypto.js';
16
- import type { AadHeader, SessionCryptoContext } from './crypto.js';
17
- import type { ProtocolMessage } from './types.js';
10
+ sealPayload,
11
+ } from './crypto.js'
12
+ import { DAppSession } from './dapp-session.js'
13
+ import { MockTransport, makeJoinBody } from './test-helpers.js'
14
+ import type { ProtocolMessage } from './types.js'
18
15
 
19
16
  function flushMicrotasks(): Promise<void> {
20
- return new Promise((r) => setTimeout(r, 10));
17
+ return new Promise((r) => setTimeout(r, 10))
21
18
  }
22
19
 
23
20
  function dappPubKeyFromCreate(transport: MockTransport): string {
24
- return transport.sent.find(m => m.t === 'create')!.from!;
21
+ return transport.sent.find((m) => m.t === 'create')?.from!
25
22
  }
26
23
 
27
24
  function receiveFreshJoin(
@@ -30,10 +27,13 @@ function receiveFreshJoin(
30
27
  walletKp: ReturnType<typeof generateX25519KeyPair>,
31
28
  ): void {
32
29
  transport.receive({
33
- v: 1, t: 'join', ch: session.channelId,
34
- ts: Date.now(), from: walletKp.publicKeyB64,
30
+ v: 1,
31
+ t: 'join',
32
+ ch: session.channelId,
33
+ ts: Date.now(),
34
+ from: walletKp.publicKeyB64,
35
35
  body: makeJoinBody(session.channelId, dappPubKeyFromCreate(transport), walletKp),
36
- } as ProtocolMessage);
36
+ } as ProtocolMessage)
37
37
  }
38
38
 
39
39
  function receiveConnected(
@@ -42,606 +42,743 @@ function receiveConnected(
42
42
  walletPubKeyB64: string,
43
43
  ): void {
44
44
  transport.receive({
45
- v: 1, t: 'ready', ch: session.channelId,
46
- ts: Date.now(), from: '_adapter',
45
+ v: 1,
46
+ t: 'ready',
47
+ ch: session.channelId,
48
+ ts: Date.now(),
49
+ from: '_adapter',
47
50
  body: { state: 'connected', reconnect: false, remote: walletPubKeyB64 },
48
- } as ProtocolMessage);
51
+ } as ProtocolMessage)
49
52
  }
50
53
 
51
54
  describe('DAppSession', () => {
52
- let transport: MockTransport;
53
- let session: DAppSession;
55
+ let transport: MockTransport
56
+ let session: DAppSession
54
57
 
55
58
  beforeEach(() => {
56
- transport = new MockTransport();
57
- session = new DAppSession({ transport, meta: { name: 'Test dApp', description: 'Test', url: 'https://test.com', icon: 'https://test.com/icon.png' } });
58
- });
59
+ transport = new MockTransport()
60
+ session = new DAppSession({
61
+ transport,
62
+ meta: {
63
+ name: 'Test dApp',
64
+ description: 'Test',
65
+ url: 'https://test.com',
66
+ icon: 'https://test.com/icon.png',
67
+ },
68
+ })
69
+ })
59
70
 
60
71
  describe('createPairing', () => {
61
72
  it('starts in idle phase', () => {
62
- expect(session.phase).toBe('idle');
63
- });
73
+ expect(session.phase).toBe('idle')
74
+ })
64
75
 
65
76
  it('creates pairing and transitions to waiting', async () => {
66
- const phases: string[] = [];
67
- session.on('phase', (p) => phases.push(p));
68
-
69
- const uri = await session.createPairing();
70
- expect(uri).toContain('walletpair:?ch=');
71
- expect(uri).toContain('&pubkey=');
72
- expect(session.phase).toBe('waiting');
73
- expect(session.channelId).toHaveLength(64);
74
- expect(session.pairingUri).toBe(uri);
75
- expect(phases).toContain('waiting');
76
- });
77
+ const phases: string[] = []
78
+ session.on('phase', (p) => phases.push(p))
79
+
80
+ const uri = await session.createPairing()
81
+ expect(uri).toContain('walletpair:?ch=')
82
+ expect(uri).toContain('&pubkey=')
83
+ expect(session.phase).toBe('waiting')
84
+ expect(session.channelId).toHaveLength(64)
85
+ expect(session.pairingUri).toBe(uri)
86
+ expect(phases).toContain('waiting')
87
+ })
77
88
 
78
89
  it('emits pairingUri event', async () => {
79
- const handler = vi.fn();
80
- session.on('pairingUri', handler);
81
- await session.createPairing();
82
- expect(handler).toHaveBeenCalledWith(session.pairingUri);
83
- });
90
+ const handler = vi.fn()
91
+ session.on('pairingUri', handler)
92
+ await session.createPairing()
93
+ expect(handler).toHaveBeenCalledWith(session.pairingUri)
94
+ })
84
95
 
85
96
  it('sends create message to transport', async () => {
86
- await session.createPairing();
87
- expect(transport.sent).toHaveLength(1);
88
- expect(transport.sent[0]!.t).toBe('create');
89
- expect(transport.sent[0]!.from).toBeTruthy();
90
- });
97
+ await session.createPairing()
98
+ expect(transport.sent).toHaveLength(1)
99
+ expect(transport.sent[0]?.t).toBe('create')
100
+ expect(transport.sent[0]?.from).toBeTruthy()
101
+ })
91
102
 
92
103
  it('pairing URI is parseable', async () => {
93
- await session.createPairing();
94
- const parsed = parsePairingUri(session.pairingUri);
95
- expect(parsed.ch).toBe(session.channelId);
96
- });
97
- });
104
+ await session.createPairing()
105
+ const parsed = parsePairingUri(session.pairingUri)
106
+ expect(parsed.ch).toBe(session.channelId)
107
+ })
108
+ })
98
109
 
99
110
  describe('wallet join handling', () => {
100
- let walletKp: ReturnType<typeof generateX25519KeyPair>;
111
+ let walletKp: ReturnType<typeof generateX25519KeyPair>
101
112
 
102
113
  beforeEach(async () => {
103
- await session.createPairing();
104
- walletKp = generateX25519KeyPair();
105
- });
114
+ await session.createPairing()
115
+ walletKp = generateX25519KeyPair()
116
+ })
106
117
 
107
118
  it('auto-accepts and transitions to accepting on join', async () => {
108
- const phases: string[] = [];
109
- session.on('phase', (p) => phases.push(p));
119
+ const phases: string[] = []
120
+ session.on('phase', (p) => phases.push(p))
110
121
 
111
- receiveFreshJoin(transport, session, walletKp);
122
+ receiveFreshJoin(transport, session, walletKp)
112
123
 
113
124
  // With auto-accept, the session should not stay in pending_accept
114
125
  // It should proceed to accepting (waiting for ready.connected)
115
- expect(session.phase).not.toBe('idle');
116
- });
126
+ expect(session.phase).not.toBe('idle')
127
+ })
117
128
 
118
129
  it('computes and emits session fingerprint on createPairing', async () => {
119
- expect(session.sessionFingerprint).toMatch(/^\d{4}$/);
120
- });
130
+ expect(session.sessionFingerprint).toMatch(/^\d{4}$/)
131
+ })
121
132
 
122
133
  it('emits walletJoined with capabilities and meta from sealed_join', async () => {
123
- const handler = vi.fn();
124
- session.on('walletJoined', handler);
134
+ const handler = vi.fn()
135
+ session.on('walletJoined', handler)
125
136
 
126
- receiveFreshJoin(transport, session, walletKp);
137
+ receiveFreshJoin(transport, session, walletKp)
127
138
 
128
139
  expect(handler).toHaveBeenCalledWith({
129
140
  capabilities: expect.objectContaining({ methods: expect.any(Array) }),
130
141
  meta: expect.objectContaining({ name: 'Test Wallet' }),
131
- });
132
- });
133
- });
142
+ })
143
+ })
144
+ })
134
145
 
135
146
  describe('auto-accept on join', () => {
136
147
  it('auto-accepts and transitions to connected on ready', async () => {
137
- await session.createPairing();
138
- const walletKp = generateX25519KeyPair();
148
+ await session.createPairing()
149
+ const walletKp = generateX25519KeyPair()
139
150
 
140
- receiveFreshJoin(transport, session, walletKp);
151
+ receiveFreshJoin(transport, session, walletKp)
141
152
 
142
153
  // Should have auto-sent accept (no manual acceptWallet needed)
143
- const acceptMsg = transport.sent.find(m => m.t === 'accept');
144
- expect(acceptMsg).toBeTruthy();
145
- expect((acceptMsg as any).body.target).toBe(walletKp.publicKeyB64);
154
+ const acceptMsg = transport.sent.find((m) => m.t === 'accept')
155
+ expect(acceptMsg).toBeTruthy()
156
+ expect((acceptMsg as any).body.target).toBe(walletKp.publicKeyB64)
146
157
 
147
158
  // Simulate relay responding with ready.connected
148
- receiveConnected(transport, session, walletKp.publicKeyB64);
159
+ receiveConnected(transport, session, walletKp.publicKeyB64)
149
160
 
150
- expect(session.phase).toBe('connected');
151
- });
161
+ expect(session.phase).toBe('connected')
162
+ })
152
163
 
153
164
  it('rejects ready.connected with missing remote', async () => {
154
- await session.createPairing();
155
- const walletKp = generateX25519KeyPair();
156
- const errorHandler = vi.fn();
157
- session.on('error', errorHandler);
165
+ await session.createPairing()
166
+ const walletKp = generateX25519KeyPair()
167
+ const errorHandler = vi.fn()
168
+ session.on('error', errorHandler)
158
169
 
159
- receiveFreshJoin(transport, session, walletKp);
170
+ receiveFreshJoin(transport, session, walletKp)
160
171
 
161
172
  transport.receive({
162
- v: 1, t: 'ready', ch: session.channelId,
163
- ts: Date.now(), from: '_adapter',
173
+ v: 1,
174
+ t: 'ready',
175
+ ch: session.channelId,
176
+ ts: Date.now(),
177
+ from: '_adapter',
164
178
  body: { state: 'connected', reconnect: false, remote: null },
165
- } as ProtocolMessage);
179
+ } as ProtocolMessage)
166
180
 
167
- expect(errorHandler).toHaveBeenCalledWith(expect.objectContaining({
168
- message: expect.stringContaining('remote does not match'),
169
- }));
170
- expect(session.phase).toBe('closed');
171
- });
172
- });
181
+ expect(errorHandler).toHaveBeenCalledWith(
182
+ expect.objectContaining({
183
+ message: expect.stringContaining('remote does not match'),
184
+ }),
185
+ )
186
+ expect(session.phase).toBe('closed')
187
+ })
188
+ })
173
189
 
174
190
  describe('rejectWallet', () => {
175
191
  it('sends close with user_rejected and closes session (autoAccept disabled)', async () => {
176
192
  const manualSession = new DAppSession({
177
- transport, autoAccept: false,
178
- meta: { name: 'Test dApp', description: 'Test', url: 'https://test.com', icon: 'https://test.com/icon.png' },
179
- });
180
- await manualSession.createPairing();
181
- const walletKp = generateX25519KeyPair();
182
-
183
- receiveFreshJoin(transport, manualSession, walletKp);
184
-
185
- manualSession.rejectWallet();
186
-
187
- const closeMsg = transport.sent.find(m => m.t === 'close');
188
- expect(closeMsg).toBeTruthy();
189
- expect((closeMsg as any).body.reason).toBe('user_rejected');
190
- expect(manualSession.phase).toBe('closed');
191
- });
192
- });
193
+ transport,
194
+ autoAccept: false,
195
+ meta: {
196
+ name: 'Test dApp',
197
+ description: 'Test',
198
+ url: 'https://test.com',
199
+ icon: 'https://test.com/icon.png',
200
+ },
201
+ })
202
+ await manualSession.createPairing()
203
+ const walletKp = generateX25519KeyPair()
204
+
205
+ receiveFreshJoin(transport, manualSession, walletKp)
206
+
207
+ manualSession.rejectWallet()
208
+
209
+ const closeMsg = transport.sent.find((m) => m.t === 'close')
210
+ expect(closeMsg).toBeTruthy()
211
+ expect((closeMsg as any).body.reason).toBe('user_rejected')
212
+ expect(manualSession.phase).toBe('closed')
213
+ })
214
+ })
193
215
 
194
216
  describe('request/response', () => {
195
- let walletKp: ReturnType<typeof generateX25519KeyPair>;
196
- let sessionKey: Uint8Array;
197
- let walletToDappKey: Uint8Array;
217
+ let walletKp: ReturnType<typeof generateX25519KeyPair>
218
+ let sessionKey: Uint8Array
219
+ let walletToDappKey: Uint8Array
198
220
 
199
221
  beforeEach(async () => {
200
- await session.createPairing();
201
- walletKp = generateX25519KeyPair();
222
+ await session.createPairing()
223
+ walletKp = generateX25519KeyPair()
202
224
 
203
225
  // Simulate join
204
- receiveFreshJoin(transport, session, walletKp);
226
+ receiveFreshJoin(transport, session, walletKp)
205
227
 
206
228
  // Derive session key from wallet side
207
- const dappPubB64 = transport.sent[0]!.from!;
208
- const dappPub = b64urlDecode(dappPubB64);
209
- const shared = computeSharedSecret(walletKp.privateKey, dappPub);
210
- sessionKey = deriveSessionKey(shared, session.channelId);
229
+ const dappPubB64 = transport.sent[0]?.from!
230
+ const dappPub = b64urlDecode(dappPubB64)
231
+ const shared = computeSharedSecret(walletKp.privateKey, dappPub)
232
+ sessionKey = deriveSessionKey(shared, session.channelId)
211
233
 
212
234
  // Auto-accepted; simulate relay ready.connected
213
- receiveConnected(transport, session, walletKp.publicKeyB64);
214
- walletToDappKey = (session as any).recvKey;
215
- });
235
+ receiveConnected(transport, session, walletKp.publicKeyB64)
236
+ walletToDappKey = (session as any).recvKey
237
+ })
216
238
 
217
239
  it('sends encrypted request', async () => {
218
- const promise = session.request('wallet_getAccounts');
240
+ const promise = session.request('wallet_getAccounts')
219
241
 
220
- await flushMicrotasks();
242
+ await flushMicrotasks()
221
243
 
222
- const reqMsg = transport.sent.find(m => m.t === 'req');
223
- expect(reqMsg).toBeTruthy();
224
- const reqBody = (reqMsg as any).body;
225
- expect(reqBody.id).toMatch(/^req-/);
226
- expect(reqBody.sealed).toBeTruthy();
244
+ const reqMsg = transport.sent.find((m) => m.t === 'req')
245
+ expect(reqMsg).toBeTruthy()
246
+ const reqBody = (reqMsg as any).body
247
+ expect(reqBody.id).toMatch(/^req-/)
248
+ expect(reqBody.sealed).toBeTruthy()
227
249
 
228
250
  // Simulate wallet response
229
- const resData = { _ok: true, _result: ['0xabc123'] };
230
- const resHdr: AadHeader = { type: 'res', from: walletKp.publicKeyB64, id: reqBody.id };
251
+ const resData = { _ok: true, _result: ['0xabc123'] }
252
+ const resHdr: AadHeader = { type: 'res', from: walletKp.publicKeyB64, id: reqBody.id }
231
253
  transport.receive({
232
- v: 1, t: 'res', ch: session.channelId,
233
- ts: Date.now(), from: walletKp.publicKeyB64,
234
- body: { id: reqBody.id, sealed: sealPayload(walletToDappKey, session.channelId, 0, resData, resHdr) },
235
- } as ProtocolMessage);
236
-
237
- const result = await promise;
238
- expect(result).toEqual(['0xabc123']);
239
- });
254
+ v: 1,
255
+ t: 'res',
256
+ ch: session.channelId,
257
+ ts: Date.now(),
258
+ from: walletKp.publicKeyB64,
259
+ body: {
260
+ id: reqBody.id,
261
+ sealed: sealPayload(walletToDappKey, session.channelId, 0, resData, resHdr),
262
+ },
263
+ } as ProtocolMessage)
264
+
265
+ const result = await promise
266
+ expect(result).toEqual(['0xabc123'])
267
+ })
240
268
 
241
269
  it('sends request with encrypted params', async () => {
242
- const promise = session.request('wallet_signMessage', { message: 'Hello' });
243
- await flushMicrotasks();
270
+ const promise = session.request('wallet_signMessage', { message: 'Hello' })
271
+ await flushMicrotasks()
244
272
 
245
- const reqMsg = transport.sent.find(m => m.t === 'req') as any;
246
- expect(reqMsg.body.sealed).toBeTruthy(); // params were sealed
273
+ const reqMsg = transport.sent.find((m) => m.t === 'req') as any
274
+ expect(reqMsg.body.sealed).toBeTruthy() // params were sealed
247
275
 
248
276
  // Respond
249
- const reqId = reqMsg.body.id;
277
+ const reqId = reqMsg.body.id
250
278
  transport.receive({
251
- v: 1, t: 'res', ch: session.channelId,
252
- ts: Date.now(), from: walletKp.publicKeyB64,
253
- body: { id: reqId, sealed: sealPayload(walletToDappKey, session.channelId, 0, { _ok: true, _result: { signature: '0x...' } }, { type: 'res', from: walletKp.publicKeyB64, id: reqId }) },
254
- } as ProtocolMessage);
255
-
256
- const result = await promise;
257
- expect(result).toEqual({ signature: '0x...' });
258
- });
279
+ v: 1,
280
+ t: 'res',
281
+ ch: session.channelId,
282
+ ts: Date.now(),
283
+ from: walletKp.publicKeyB64,
284
+ body: {
285
+ id: reqId,
286
+ sealed: sealPayload(
287
+ walletToDappKey,
288
+ session.channelId,
289
+ 0,
290
+ { _ok: true, _result: { signature: '0x...' } },
291
+ { type: 'res', from: walletKp.publicKeyB64, id: reqId },
292
+ ),
293
+ },
294
+ } as ProtocolMessage)
295
+
296
+ const result = await promise
297
+ expect(result).toEqual({ signature: '0x...' })
298
+ })
259
299
 
260
300
  it('rejects on error response', async () => {
261
- const promise = session.request('wallet_signMessage', { message: 'Hi' });
262
- await flushMicrotasks();
301
+ const promise = session.request('wallet_signMessage', { message: 'Hi' })
302
+ await flushMicrotasks()
263
303
 
264
- const reqMsg = transport.sent.find(m => m.t === 'req') as any;
265
- const reqId = reqMsg.body.id;
304
+ const reqMsg = transport.sent.find((m) => m.t === 'req') as any
305
+ const reqId = reqMsg.body.id
266
306
 
267
307
  transport.receive({
268
- v: 1, t: 'res', ch: session.channelId,
269
- ts: Date.now(), from: walletKp.publicKeyB64,
270
- body: { id: reqId, sealed: sealPayload(walletToDappKey, session.channelId, 0, { _ok: false, code: 'user_rejected', message: 'User rejected' }, { type: 'res', from: walletKp.publicKeyB64, id: reqId }) },
271
- } as ProtocolMessage);
272
-
273
- await expect(promise).rejects.toThrow('User rejected');
274
- });
308
+ v: 1,
309
+ t: 'res',
310
+ ch: session.channelId,
311
+ ts: Date.now(),
312
+ from: walletKp.publicKeyB64,
313
+ body: {
314
+ id: reqId,
315
+ sealed: sealPayload(
316
+ walletToDappKey,
317
+ session.channelId,
318
+ 0,
319
+ { _ok: false, code: 'user_rejected', message: 'User rejected' },
320
+ { type: 'res', from: walletKp.publicKeyB64, id: reqId },
321
+ ),
322
+ },
323
+ } as ProtocolMessage)
324
+
325
+ await expect(promise).rejects.toThrow('User rejected')
326
+ })
275
327
 
276
328
  it('rejects on timeout', async () => {
277
- vi.useFakeTimers();
329
+ vi.useFakeTimers()
278
330
 
279
331
  const shortTimeoutSession = new DAppSession({
280
- transport, meta: { name: 'Test', description: 'Test', url: 'https://test.com', icon: 'https://test.com/icon.png' }, requestTimeout: 100,
281
- });
332
+ transport,
333
+ meta: {
334
+ name: 'Test',
335
+ description: 'Test',
336
+ url: 'https://test.com',
337
+ icon: 'https://test.com/icon.png',
338
+ },
339
+ requestTimeout: 100,
340
+ })
282
341
  // Manually set session state to connected
283
- (shortTimeoutSession as any).phase = 'connected';
284
- (shortTimeoutSession as any).sessionKey = sessionKey;
285
- (shortTimeoutSession as any).sendKey = new Uint8Array(32).fill(1);
286
- (shortTimeoutSession as any).channelId = session.channelId;
287
- (shortTimeoutSession as any).pubKeyB64 = 'test';
342
+ ;(shortTimeoutSession as any).phase = 'connected'
343
+ ;(shortTimeoutSession as any).sessionKey = sessionKey
344
+ ;(shortTimeoutSession as any).sendKey = new Uint8Array(32).fill(1)
345
+ ;(shortTimeoutSession as any).channelId = session.channelId
346
+ ;(shortTimeoutSession as any).pubKeyB64 = 'test'
288
347
 
289
- const promise = shortTimeoutSession.request('wallet_getAccounts');
290
- vi.advanceTimersByTime(200);
348
+ const promise = shortTimeoutSession.request('wallet_getAccounts')
349
+ vi.advanceTimersByTime(200)
291
350
 
292
- await expect(promise).rejects.toThrow('timed out');
293
- vi.useRealTimers();
294
- });
351
+ await expect(promise).rejects.toThrow('timed out')
352
+ vi.useRealTimers()
353
+ })
295
354
 
296
355
  it('emits response event', async () => {
297
- const handler = vi.fn();
298
- session.on('response', handler);
356
+ const handler = vi.fn()
357
+ session.on('response', handler)
299
358
 
300
- const promise = session.request('wallet_getAccounts');
301
- await flushMicrotasks();
359
+ const promise = session.request('wallet_getAccounts')
360
+ await flushMicrotasks()
302
361
 
303
- const reqMsg = transport.sent.find(m => m.t === 'req') as any;
304
- const reqId = reqMsg.body.id;
362
+ const reqMsg = transport.sent.find((m) => m.t === 'req') as any
363
+ const reqId = reqMsg.body.id
305
364
  transport.receive({
306
- v: 1, t: 'res', ch: session.channelId,
307
- ts: Date.now(), from: walletKp.publicKeyB64,
308
- body: { id: reqId, sealed: sealPayload(walletToDappKey, session.channelId, 0, { _ok: true, _result: ['0x123'] }, { type: 'res', from: walletKp.publicKeyB64, id: reqId }) },
309
- } as ProtocolMessage);
310
-
311
- await promise;
312
- expect(handler).toHaveBeenCalledWith({ id: reqId, ok: true, result: ['0x123'] });
313
- });
365
+ v: 1,
366
+ t: 'res',
367
+ ch: session.channelId,
368
+ ts: Date.now(),
369
+ from: walletKp.publicKeyB64,
370
+ body: {
371
+ id: reqId,
372
+ sealed: sealPayload(
373
+ walletToDappKey,
374
+ session.channelId,
375
+ 0,
376
+ { _ok: true, _result: ['0x123'] },
377
+ { type: 'res', from: walletKp.publicKeyB64, id: reqId },
378
+ ),
379
+ },
380
+ } as ProtocolMessage)
381
+
382
+ await promise
383
+ expect(handler).toHaveBeenCalledWith({ id: reqId, ok: true, result: ['0x123'] })
384
+ })
314
385
 
315
386
  it('rejects request when not connected', async () => {
316
- const idleSession = new DAppSession({ transport: new MockTransport(), meta: { name: 'Test', description: 'Test', url: 'https://test.com', icon: 'https://test.com/icon.png' } });
317
- await expect(idleSession.request('test')).rejects.toThrow('Not connected');
318
- });
319
- });
387
+ const idleSession = new DAppSession({
388
+ transport: new MockTransport(),
389
+ meta: {
390
+ name: 'Test',
391
+ description: 'Test',
392
+ url: 'https://test.com',
393
+ icon: 'https://test.com/icon.png',
394
+ },
395
+ })
396
+ await expect(idleSession.request('test')).rejects.toThrow('Not connected')
397
+ })
398
+ })
320
399
 
321
400
  describe('event handling', () => {
322
401
  it('emits event when wallet pushes evt', async () => {
323
- await session.createPairing();
324
- const walletKp = generateX25519KeyPair();
402
+ await session.createPairing()
403
+ const walletKp = generateX25519KeyPair()
325
404
 
326
- receiveFreshJoin(transport, session, walletKp);
405
+ receiveFreshJoin(transport, session, walletKp)
327
406
 
328
- const dappPub = b64urlDecode(transport.sent[0]!.from!);
329
- const shared = computeSharedSecret(walletKp.privateKey, dappPub);
330
- deriveSessionKey(shared, session.channelId);
407
+ const dappPub = b64urlDecode(transport.sent[0]?.from!)
408
+ const shared = computeSharedSecret(walletKp.privateKey, dappPub)
409
+ deriveSessionKey(shared, session.channelId)
331
410
 
332
- receiveConnected(transport, session, walletKp.publicKeyB64);
411
+ receiveConnected(transport, session, walletKp.publicKeyB64)
333
412
 
334
- const handler = vi.fn();
335
- session.on('event', handler);
413
+ const handler = vi.fn()
414
+ session.on('event', handler)
336
415
 
337
- const evtId = 'evt-1';
416
+ const evtId = 'evt-1'
338
417
  transport.receive({
339
- v: 1, t: 'evt', ch: session.channelId,
340
- ts: Date.now(), from: walletKp.publicKeyB64,
341
- body: { id: evtId, sealed: sealPayload((session as any).recvKey, session.channelId, 0, { _event: 'accountsChanged', accounts: ['0xabc'] }, { type: 'evt', from: walletKp.publicKeyB64, id: evtId }) },
342
- } as ProtocolMessage);
418
+ v: 1,
419
+ t: 'evt',
420
+ ch: session.channelId,
421
+ ts: Date.now(),
422
+ from: walletKp.publicKeyB64,
423
+ body: {
424
+ id: evtId,
425
+ sealed: sealPayload(
426
+ (session as any).recvKey,
427
+ session.channelId,
428
+ 0,
429
+ { _event: 'accountsChanged', accounts: ['0xabc'] },
430
+ { type: 'evt', from: walletKp.publicKeyB64, id: evtId },
431
+ ),
432
+ },
433
+ } as ProtocolMessage)
343
434
 
344
435
  expect(handler).toHaveBeenCalledWith({
345
436
  event: 'accountsChanged',
346
437
  data: { accounts: ['0xabc'] },
347
- });
348
- });
349
- });
438
+ })
439
+ })
440
+ })
350
441
 
351
442
  describe('ping/pong', () => {
352
443
  it('responds to ping with pong', async () => {
353
- await session.createPairing();
354
- const walletPubB64 = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
355
- (session as any).remotePubKey = b64urlDecode(walletPubB64);
356
- receiveConnected(transport, session, walletPubB64);
444
+ await session.createPairing()
445
+ const walletPubB64 = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
446
+ ;(session as any).remotePubKey = b64urlDecode(walletPubB64)
447
+ receiveConnected(transport, session, walletPubB64)
357
448
 
358
449
  transport.receive({
359
- v: 1, t: 'ping', ch: session.channelId,
360
- ts: 1000, from: walletPubB64, body: {},
361
- } as ProtocolMessage);
362
-
363
- const pong = transport.sent.find(m => m.t === 'pong');
364
- expect(pong).toBeTruthy();
365
- expect(pong!.ts).toBeTypeOf('number');
366
- });
450
+ v: 1,
451
+ t: 'ping',
452
+ ch: session.channelId,
453
+ ts: 1000,
454
+ from: walletPubB64,
455
+ body: {},
456
+ } as ProtocolMessage)
457
+
458
+ const pong = transport.sent.find((m) => m.t === 'pong')
459
+ expect(pong).toBeTruthy()
460
+ expect(pong?.ts).toBeTypeOf('number')
461
+ })
367
462
 
368
463
  it('sends ping', async () => {
369
- const walletPubB64 = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
370
- await session.createPairing();
371
- (session as any).remotePubKey = b64urlDecode(walletPubB64);
372
- receiveConnected(transport, session, walletPubB64);
464
+ const walletPubB64 = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
465
+ await session.createPairing()
466
+ ;(session as any).remotePubKey = b64urlDecode(walletPubB64)
467
+ receiveConnected(transport, session, walletPubB64)
373
468
 
374
- session.ping();
375
- const ping = transport.sent.find(m => m.t === 'ping');
376
- expect(ping).toBeTruthy();
377
- });
378
- });
469
+ session.ping()
470
+ const ping = transport.sent.find((m) => m.t === 'ping')
471
+ expect(ping).toBeTruthy()
472
+ })
473
+ })
379
474
 
380
475
  describe('close', () => {
381
476
  it('sends close message and transitions to closed', async () => {
382
- await session.createPairing();
383
- session.close();
477
+ await session.createPairing()
478
+ session.close()
384
479
 
385
- const closeMsg = transport.sent.find(m => m.t === 'close');
386
- expect(closeMsg).toBeTruthy();
387
- expect((closeMsg as any).body.reason).toBe('normal');
388
- expect(session.phase).toBe('closed');
389
- });
480
+ const closeMsg = transport.sent.find((m) => m.t === 'close')
481
+ expect(closeMsg).toBeTruthy()
482
+ expect((closeMsg as any).body.reason).toBe('normal')
483
+ expect(session.phase).toBe('closed')
484
+ })
390
485
 
391
486
  it('rejects all pending requests on close', async () => {
392
- await session.createPairing();
393
- const walletKp = generateX25519KeyPair();
487
+ await session.createPairing()
488
+ const walletKp = generateX25519KeyPair()
394
489
 
395
- receiveFreshJoin(transport, session, walletKp);
396
- receiveConnected(transport, session, walletKp.publicKeyB64);
490
+ receiveFreshJoin(transport, session, walletKp)
491
+ receiveConnected(transport, session, walletKp.publicKeyB64)
397
492
 
398
- const promise = session.request('test');
399
- session.close();
493
+ const promise = session.request('test')
494
+ session.close()
400
495
 
401
- await expect(promise).rejects.toThrow('Session closed');
402
- });
403
- });
496
+ await expect(promise).rejects.toThrow('Session closed')
497
+ })
498
+ })
404
499
 
405
500
  describe('serialize/restore', () => {
406
501
  it('round-trips session state', async () => {
407
- await session.createPairing();
408
- const walletKp = generateX25519KeyPair();
409
-
410
- receiveFreshJoin(transport, session, walletKp);
411
-
412
- const json = session.serialize();
413
- expect(json).toBeTruthy();
414
-
415
- const newTransport = new MockTransport();
416
- const restored = new DAppSession({ transport: newTransport, meta: { name: 'Test dApp', description: 'Test', url: 'https://test.com', icon: 'https://test.com/icon.png' } });
417
- expect(restored.restore(json)).toBe(true);
418
- expect(restored.channelId).toBe(session.channelId);
419
- });
502
+ await session.createPairing()
503
+ const walletKp = generateX25519KeyPair()
504
+
505
+ receiveFreshJoin(transport, session, walletKp)
506
+
507
+ const json = session.serialize()
508
+ expect(json).toBeTruthy()
509
+
510
+ const newTransport = new MockTransport()
511
+ const restored = new DAppSession({
512
+ transport: newTransport,
513
+ meta: {
514
+ name: 'Test dApp',
515
+ description: 'Test',
516
+ url: 'https://test.com',
517
+ icon: 'https://test.com/icon.png',
518
+ },
519
+ })
520
+ expect(restored.restore(json)).toBe(true)
521
+ expect(restored.channelId).toBe(session.channelId)
522
+ })
420
523
 
421
524
  it('returns false for invalid JSON', () => {
422
- const s = new DAppSession({ transport: new MockTransport(), meta: { name: 'Test dApp', description: 'Test', url: 'https://test.com', icon: 'https://test.com/icon.png' } });
423
- expect(s.restore('not json')).toBe(false);
424
- expect(s.restore('{}')).toBe(false);
425
- expect(s.restore('{"channelId":"abc"}')).toBe(false); // missing privKey
426
- });
427
- });
525
+ const s = new DAppSession({
526
+ transport: new MockTransport(),
527
+ meta: {
528
+ name: 'Test dApp',
529
+ description: 'Test',
530
+ url: 'https://test.com',
531
+ icon: 'https://test.com/icon.png',
532
+ },
533
+ })
534
+ expect(s.restore('not json')).toBe(false)
535
+ expect(s.restore('{}')).toBe(false)
536
+ expect(s.restore('{"channelId":"abc"}')).toBe(false) // missing privKey
537
+ })
538
+ })
428
539
 
429
540
  describe('auto-accept on rejoin', () => {
430
541
  it('auto-accepts known wallet on rejoin (no sealed_join on reconnect)', async () => {
431
- await session.createPairing();
432
- const walletKp = generateX25519KeyPair();
542
+ await session.createPairing()
543
+ const walletKp = generateX25519KeyPair()
433
544
 
434
545
  // First join carries sealed capabilities/meta (auto-accepted).
435
- receiveFreshJoin(transport, session, walletKp);
436
- receiveConnected(transport, session, walletKp.publicKeyB64);
546
+ receiveFreshJoin(transport, session, walletKp)
547
+ receiveConnected(transport, session, walletKp.publicKeyB64)
437
548
 
438
549
  // Second join (rejoin) without sealed_join — should auto-accept (same wallet, same approved scope)
439
550
  transport.receive({
440
- v: 1, t: 'join', ch: session.channelId,
441
- ts: Date.now(), from: walletKp.publicKeyB64,
551
+ v: 1,
552
+ t: 'join',
553
+ ch: session.channelId,
554
+ ts: Date.now(),
555
+ from: walletKp.publicKeyB64,
442
556
  body: { sealed_join: null },
443
- } as ProtocolMessage);
557
+ } as ProtocolMessage)
444
558
 
445
559
  // First join auto-accepted + rejoin auto-accepted = 2 accept messages
446
- const acceptMessages = transport.sent.filter(m => m.t === 'accept');
447
- expect(acceptMessages).toHaveLength(2);
448
- });
560
+ const acceptMessages = transport.sent.filter((m) => m.t === 'accept')
561
+ expect(acceptMessages).toHaveLength(2)
562
+ })
449
563
 
450
564
  it('auto-accepts new wallet on rejoin (different pubkey)', async () => {
451
- await session.createPairing();
452
- const walletKp = generateX25519KeyPair();
453
- const walletKp2 = generateX25519KeyPair();
565
+ await session.createPairing()
566
+ const walletKp = generateX25519KeyPair()
567
+ const walletKp2 = generateX25519KeyPair()
454
568
 
455
569
  // First join (auto-accepted)
456
- receiveFreshJoin(transport, session, walletKp);
457
- receiveConnected(transport, session, walletKp.publicKeyB64);
570
+ receiveFreshJoin(transport, session, walletKp)
571
+ receiveConnected(transport, session, walletKp.publicKeyB64)
458
572
 
459
573
  // Second join with different wallet — also auto-accepted (sealed_join decryption proves possession)
460
- receiveFreshJoin(transport, session, walletKp2);
574
+ receiveFreshJoin(transport, session, walletKp2)
461
575
 
462
- const acceptMessages = transport.sent.filter(m => m.t === 'accept');
463
- expect(acceptMessages).toHaveLength(2);
464
- });
465
- });
576
+ const acceptMessages = transport.sent.filter((m) => m.t === 'accept')
577
+ expect(acceptMessages).toHaveLength(2)
578
+ })
579
+ })
466
580
 
467
581
  describe('close message handling', () => {
468
582
  it('transitions to closed on receiving close', async () => {
469
- await session.createPairing();
470
- const walletKp = generateX25519KeyPair();
583
+ await session.createPairing()
584
+ const walletKp = generateX25519KeyPair()
471
585
  transport.receive({
472
- v: 1, t: 'close', ch: session.channelId,
473
- ts: Date.now(), from: walletKp.publicKeyB64,
586
+ v: 1,
587
+ t: 'close',
588
+ ch: session.channelId,
589
+ ts: Date.now(),
590
+ from: walletKp.publicKeyB64,
474
591
  body: { reason: 'timeout' },
475
- } as ProtocolMessage);
592
+ } as ProtocolMessage)
476
593
 
477
- expect(session.phase).toBe('closed');
478
- });
479
- });
594
+ expect(session.phase).toBe('closed')
595
+ })
596
+ })
480
597
 
481
598
  describe('destroy', () => {
482
599
  it('closes and removes all listeners', async () => {
483
- await session.createPairing();
484
- const handler = vi.fn();
485
- session.on('phase', handler);
486
- session.destroy();
600
+ await session.createPairing()
601
+ const handler = vi.fn()
602
+ session.on('phase', handler)
603
+ session.destroy()
487
604
 
488
- expect(session.phase).toBe('closed');
605
+ expect(session.phase).toBe('closed')
489
606
  // After destroy, emitting should not call handler
490
607
  // (removeAll was called)
491
- });
492
- });
608
+ })
609
+ })
493
610
 
494
611
  describe('protocol compliance', () => {
495
612
  it('rejects messages with from="_adapter" for peer types (§2)', async () => {
496
- await session.createPairing();
497
- const errorHandler = vi.fn();
498
- session.on('error', errorHandler);
613
+ await session.createPairing()
614
+ const errorHandler = vi.fn()
615
+ session.on('error', errorHandler)
499
616
 
500
617
  // Send a close message with from: '_adapter' — should be rejected
501
618
  transport.receive({
502
- v: 1, t: 'close', ch: session.channelId,
503
- ts: Date.now(), from: '_adapter',
619
+ v: 1,
620
+ t: 'close',
621
+ ch: session.channelId,
622
+ ts: Date.now(),
623
+ from: '_adapter',
504
624
  body: { reason: 'normal' },
505
- } as ProtocolMessage);
625
+ } as ProtocolMessage)
506
626
 
507
- expect(errorHandler).toHaveBeenCalledWith(expect.objectContaining({
508
- message: expect.stringContaining('_adapter'),
509
- }));
627
+ expect(errorHandler).toHaveBeenCalledWith(
628
+ expect.objectContaining({
629
+ message: expect.stringContaining('_adapter'),
630
+ }),
631
+ )
510
632
  // Should NOT have processed it as a real close
511
- expect(session.phase).not.toBe('closed');
512
- });
633
+ expect(session.phase).not.toBe('closed')
634
+ })
513
635
 
514
636
  it('rejects messages with unsupported version (§15 rule 12)', async () => {
515
- await session.createPairing();
637
+ await session.createPairing()
516
638
 
517
639
  // Send message with v: 2 — should close with unsupported_version
518
640
  transport.receive({
519
- v: 2, t: 'close', ch: session.channelId,
520
- ts: Date.now(), from: 'somepubkey',
641
+ v: 2,
642
+ t: 'close',
643
+ ch: session.channelId,
644
+ ts: Date.now(),
645
+ from: 'somepubkey',
521
646
  body: { reason: 'normal' },
522
- } as unknown as ProtocolMessage);
647
+ } as unknown as ProtocolMessage)
523
648
 
524
- expect(session.phase).toBe('closed');
525
- const closeMsg = transport.sent.find(m => m.t === 'close') as any;
526
- expect(closeMsg).toBeTruthy();
527
- expect(closeMsg.body.reason).toBe('unsupported_version');
528
- });
649
+ expect(session.phase).toBe('closed')
650
+ const closeMsg = transport.sent.find((m) => m.t === 'close') as any
651
+ expect(closeMsg).toBeTruthy()
652
+ expect(closeMsg.body.reason).toBe('unsupported_version')
653
+ })
529
654
 
530
655
  it('uses null for missing walletMeta in session context', async () => {
531
- await session.createPairing();
656
+ await session.createPairing()
532
657
 
533
658
  // Before any wallet joins, walletMeta should be undefined
534
- expect(session.walletMeta).toBeUndefined();
659
+ expect(session.walletMeta).toBeUndefined()
535
660
 
536
661
  // Access the private sessionContext to verify it uses null (not {})
537
662
  const context = (session as any).sessionContext(
538
663
  'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
539
664
  undefined,
540
665
  undefined,
541
- );
542
- expect(context.walletMeta).toBeNull();
543
- });
544
- });
666
+ )
667
+ expect(context.walletMeta).toBeNull()
668
+ })
669
+ })
545
670
 
546
671
  describe('auto-accept flow (first join)', () => {
547
672
  it('auto-accepts first wallet join with valid sealed_join, skipping pending_accept', async () => {
548
- const phases: string[] = [];
549
- session.on('phase', (p) => phases.push(p));
673
+ const phases: string[] = []
674
+ session.on('phase', (p) => phases.push(p))
550
675
 
551
- await session.createPairing();
552
- const walletKp = generateX25519KeyPair();
676
+ await session.createPairing()
677
+ const walletKp = generateX25519KeyPair()
553
678
 
554
- receiveFreshJoin(transport, session, walletKp);
679
+ receiveFreshJoin(transport, session, walletKp)
555
680
 
556
681
  // Auto-accept should have sent an accept message without manual acceptWallet()
557
- const acceptMsg = transport.sent.find(m => m.t === 'accept');
558
- expect(acceptMsg).toBeTruthy();
682
+ const acceptMsg = transport.sent.find((m) => m.t === 'accept')
683
+ expect(acceptMsg).toBeTruthy()
559
684
 
560
685
  // Simulate relay responding with ready.connected
561
- receiveConnected(transport, session, walletKp.publicKeyB64);
686
+ receiveConnected(transport, session, walletKp.publicKeyB64)
562
687
 
563
- expect(session.phase).toBe('connected');
688
+ expect(session.phase).toBe('connected')
564
689
  // Phase goes waiting → pending_accept → (auto-accept) → accepting → connected
565
690
  // pending_accept is emitted briefly before auto-accept kicks in
566
- expect(phases).toContain('pending_accept');
567
- });
568
- });
691
+ expect(phases).toContain('pending_accept')
692
+ })
693
+ })
569
694
 
570
695
  describe('session fingerprint after createPairing', () => {
571
696
  it('sessionFingerprint is a 4-digit string and event was emitted', async () => {
572
- const fpHandler = vi.fn();
573
- session.on('sessionFingerprint', fpHandler);
697
+ const fpHandler = vi.fn()
698
+ session.on('sessionFingerprint', fpHandler)
574
699
 
575
- await session.createPairing();
700
+ await session.createPairing()
576
701
 
577
- expect(session.sessionFingerprint).toMatch(/^\d{4}$/);
578
- expect(fpHandler).toHaveBeenCalledTimes(1);
579
- expect(fpHandler).toHaveBeenCalledWith(session.sessionFingerprint);
580
- });
581
- });
702
+ expect(session.sessionFingerprint).toMatch(/^\d{4}$/)
703
+ expect(fpHandler).toHaveBeenCalledTimes(1)
704
+ expect(fpHandler).toHaveBeenCalledWith(session.sessionFingerprint)
705
+ })
706
+ })
582
707
 
583
708
  describe('session fingerprint matches wallet side', () => {
584
709
  it('dApp and wallet compute the same fingerprint', async () => {
585
- await session.createPairing();
586
- const dappFingerprint = session.sessionFingerprint;
710
+ await session.createPairing()
711
+ const dappFingerprint = session.sessionFingerprint
587
712
 
588
713
  // The dApp computes fingerprint from its own pubkey + channelId
589
714
  // The wallet computes it from (channelId, dappPubKeyB64) — same inputs
590
- const dappPubB64 = dappPubKeyFromCreate(transport);
591
- const walletSideFingerprint = computeSessionFingerprint(session.channelId, dappPubB64);
715
+ const dappPubB64 = dappPubKeyFromCreate(transport)
716
+ const walletSideFingerprint = computeSessionFingerprint(session.channelId, dappPubB64)
592
717
 
593
- expect(dappFingerprint).toBe(walletSideFingerprint);
594
- expect(dappFingerprint).toMatch(/^\d{4}$/);
595
- });
596
- });
718
+ expect(dappFingerprint).toBe(walletSideFingerprint)
719
+ expect(dappFingerprint).toMatch(/^\d{4}$/)
720
+ })
721
+ })
597
722
 
598
723
  describe('session TTL enforcement', () => {
599
724
  it('closes with reason timeout after TTL expires', async () => {
600
- vi.useFakeTimers();
725
+ vi.useFakeTimers()
601
726
 
602
- const shortTtlTransport = new MockTransport();
727
+ const shortTtlTransport = new MockTransport()
603
728
  const shortTtlSession = new DAppSession({
604
729
  transport: shortTtlTransport,
605
730
  meta: { name: 'T', description: 'T', url: 'https://t.com', icon: 'https://t.com/i.png' },
606
731
  sessionTtl: 100,
607
- });
732
+ })
608
733
 
609
- await shortTtlSession.createPairing();
610
- const walletKp = generateX25519KeyPair();
734
+ await shortTtlSession.createPairing()
735
+ const walletKp = generateX25519KeyPair()
611
736
 
612
737
  // Simulate wallet join
613
738
  shortTtlTransport.receive({
614
- v: 1, t: 'join', ch: shortTtlSession.channelId,
615
- ts: Date.now(), from: walletKp.publicKeyB64,
616
- body: makeJoinBody(shortTtlSession.channelId, shortTtlTransport.sent.find(m => m.t === 'create')!.from!, walletKp),
617
- } as ProtocolMessage);
739
+ v: 1,
740
+ t: 'join',
741
+ ch: shortTtlSession.channelId,
742
+ ts: Date.now(),
743
+ from: walletKp.publicKeyB64,
744
+ body: makeJoinBody(
745
+ shortTtlSession.channelId,
746
+ shortTtlTransport.sent.find((m) => m.t === 'create')?.from!,
747
+ walletKp,
748
+ ),
749
+ } as ProtocolMessage)
618
750
 
619
751
  // Simulate ready.connected (this starts the TTL timer)
620
752
  shortTtlTransport.receive({
621
- v: 1, t: 'ready', ch: shortTtlSession.channelId,
622
- ts: Date.now(), from: '_adapter',
753
+ v: 1,
754
+ t: 'ready',
755
+ ch: shortTtlSession.channelId,
756
+ ts: Date.now(),
757
+ from: '_adapter',
623
758
  body: { state: 'connected', reconnect: false, remote: walletKp.publicKeyB64 },
624
- } as ProtocolMessage);
759
+ } as ProtocolMessage)
625
760
 
626
- expect(shortTtlSession.phase).toBe('connected');
761
+ expect(shortTtlSession.phase).toBe('connected')
627
762
 
628
- const errorHandler = vi.fn();
629
- shortTtlSession.on('error', errorHandler);
763
+ const errorHandler = vi.fn()
764
+ shortTtlSession.on('error', errorHandler)
630
765
 
631
766
  // Advance time past the TTL
632
- vi.advanceTimersByTime(150);
767
+ vi.advanceTimersByTime(150)
633
768
 
634
- expect(shortTtlSession.phase).toBe('closed');
635
- expect(errorHandler).toHaveBeenCalledWith(expect.objectContaining({
636
- message: expect.stringContaining('expired'),
637
- }));
769
+ expect(shortTtlSession.phase).toBe('closed')
770
+ expect(errorHandler).toHaveBeenCalledWith(
771
+ expect.objectContaining({
772
+ message: expect.stringContaining('expired'),
773
+ }),
774
+ )
638
775
 
639
776
  // Verify close message was sent with reason 'timeout'
640
- const closeMsg = shortTtlTransport.sent.find(m => m.t === 'close');
641
- expect(closeMsg).toBeTruthy();
642
- expect((closeMsg as any).body.reason).toBe('timeout');
643
-
644
- vi.useRealTimers();
645
- });
646
- });
647
- });
777
+ const closeMsg = shortTtlTransport.sent.find((m) => m.t === 'close')
778
+ expect(closeMsg).toBeTruthy()
779
+ expect((closeMsg as any).body.reason).toBe('timeout')
780
+
781
+ vi.useRealTimers()
782
+ })
783
+ })
784
+ })