walletpair-sdk 1.0.3 → 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 +1 -1
  34. package/dist/ws-transport.d.ts.map +1 -1
  35. package/dist/ws-transport.js +12 -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 +51 -41
@@ -378,6 +378,7 @@ export class WalletSession extends Emitter<WalletSessionEvents> {
378
378
  }
379
379
  this.dappName = d.dappName ?? undefined
380
380
  this.sessionStartTime = d.sessionStartTime ?? null
381
+ this.setPhase('connected')
381
382
  return true
382
383
  } catch {
383
384
  return false
@@ -442,15 +443,14 @@ export class WalletSession extends Emitter<WalletSessionEvents> {
442
443
  }
443
444
 
444
445
  case 'req': {
446
+ if (this.phase !== 'connected') break
445
447
  const reqBody = msg.body as { id?: string; sealed?: string }
446
448
  if (this.remotePubKey && msg.from !== b64urlEncode(this.remotePubKey)) break
447
449
  // All requests MUST be sealed — reject unsealed requests to prevent
448
450
  // method injection by a malicious relay.
449
451
  if (!reqBody.sealed || !reqBody.id || !this.recvKey) {
450
452
  if (reqBody.id) {
451
- this.observeSend(
452
- this.reject(reqBody.id, 'protocol_error', 'Request must be encrypted'),
453
- )
453
+ this.observeSend(this.reject(reqBody.id, 'protocol_error', 'Request must be encrypted'))
454
454
  }
455
455
  break
456
456
  }
@@ -470,12 +470,10 @@ export class WalletSession extends Emitter<WalletSessionEvents> {
470
470
  const afterPersist = () => this.processRequest(requestId, data, plaintext)
471
471
  const persisted = this.persistSnapshot()
472
472
  if (isPromiseLike(persisted)) {
473
- void persisted
474
- .then(afterPersist)
475
- .catch((e) => {
476
- this.recvSeq = prevRecvSeq // rollback on persist failure
477
- this.emit('error', this.persistenceError(e))
478
- })
473
+ void persisted.then(afterPersist).catch((e) => {
474
+ this.recvSeq = prevRecvSeq // rollback on persist failure
475
+ this.emit('error', this.persistenceError(e))
476
+ })
479
477
  } else {
480
478
  afterPersist()
481
479
  }
@@ -1,231 +1,233 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
- import { WebSocketTransport } from './ws-transport.js';
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
2
+ import type { ProtocolMessage } from './types.js'
3
+ import { WebSocketTransport } from './ws-transport.js'
3
4
 
4
5
  // Mock WebSocket for Node.js environment
5
6
  class MockWebSocket {
6
- static instances: MockWebSocket[] = [];
7
+ static instances: MockWebSocket[] = []
7
8
 
8
- url: string;
9
- protocols: string[];
10
- readyState = 0; // CONNECTING
11
- onopen: (() => void) | null = null;
12
- onmessage: ((event: { data: string }) => void) | null = null;
13
- onclose: (() => void) | null = null;
14
- onerror: (() => void) | null = null;
15
- sentMessages: string[] = [];
9
+ url: string
10
+ protocols: string[]
11
+ readyState = 0 // CONNECTING
12
+ onopen: (() => void) | null = null
13
+ onmessage: ((event: { data: string }) => void) | null = null
14
+ onclose: (() => void) | null = null
15
+ onerror: (() => void) | null = null
16
+ sentMessages: string[] = []
16
17
 
17
18
  constructor(url: string, protocols?: string[]) {
18
- this.url = url;
19
- this.protocols = protocols ?? [];
20
- MockWebSocket.instances.push(this);
19
+ this.url = url
20
+ this.protocols = protocols ?? []
21
+ MockWebSocket.instances.push(this)
21
22
  }
22
23
 
23
24
  send(data: string) {
24
- this.sentMessages.push(data);
25
+ this.sentMessages.push(data)
25
26
  }
26
27
 
27
28
  close() {
28
- this.readyState = 3; // CLOSED
29
- this.onclose?.();
29
+ this.readyState = 3 // CLOSED
30
+ this.onclose?.()
30
31
  }
31
32
 
32
33
  // Test helpers
33
34
  simulateOpen() {
34
- this.readyState = 1; // OPEN
35
- this.onopen?.();
35
+ this.readyState = 1 // OPEN
36
+ this.onopen?.()
36
37
  }
37
38
 
38
39
  simulateMessage(data: string) {
39
- this.onmessage?.({ data });
40
+ this.onmessage?.({ data })
40
41
  }
41
42
 
42
43
  simulateClose() {
43
- this.readyState = 3;
44
- this.onclose?.();
44
+ this.readyState = 3
45
+ this.onclose?.()
45
46
  }
46
47
 
47
48
  simulateError() {
48
- this.onerror?.();
49
+ this.onerror?.()
49
50
  }
50
51
  }
51
52
 
52
53
  describe('WebSocketTransport', () => {
53
- let originalWebSocket: typeof globalThis.WebSocket;
54
+ let originalWebSocket: typeof globalThis.WebSocket
54
55
 
55
56
  beforeEach(() => {
56
- MockWebSocket.instances = [];
57
- originalWebSocket = globalThis.WebSocket;
58
- (globalThis as any).WebSocket = MockWebSocket;
59
- });
57
+ MockWebSocket.instances = []
58
+ originalWebSocket = globalThis.WebSocket
59
+ ;(globalThis as unknown as Record<string, unknown>).WebSocket = MockWebSocket
60
+ })
60
61
 
61
62
  afterEach(() => {
62
- globalThis.WebSocket = originalWebSocket;
63
- });
63
+ globalThis.WebSocket = originalWebSocket
64
+ })
64
65
 
65
66
  it('starts disconnected', () => {
66
- const t = new WebSocketTransport('ws://localhost:8080/v1');
67
- expect(t.state).toBe('disconnected');
68
- });
67
+ const t = new WebSocketTransport('ws://localhost:8080/v1')
68
+ expect(t.state).toBe('disconnected')
69
+ })
69
70
 
70
71
  it('accepts string URL constructor', () => {
71
- const t = new WebSocketTransport('ws://localhost:8080/v1');
72
- expect(t.state).toBe('disconnected');
73
- });
72
+ const t = new WebSocketTransport('ws://localhost:8080/v1')
73
+ expect(t.state).toBe('disconnected')
74
+ })
74
75
 
75
76
  it('accepts options object constructor', () => {
76
- const t = new WebSocketTransport({ url: 'ws://localhost:8080/v1', protocols: ['custom'] });
77
- expect(t.state).toBe('disconnected');
78
- });
77
+ const t = new WebSocketTransport({ url: 'ws://localhost:8080/v1', protocols: ['custom'] })
78
+ expect(t.state).toBe('disconnected')
79
+ })
79
80
 
80
81
  describe('connect', () => {
81
82
  it('resolves on successful connection', async () => {
82
- const t = new WebSocketTransport('ws://localhost:8080/v1');
83
+ const t = new WebSocketTransport('ws://localhost:8080/v1')
83
84
 
84
- const connectPromise = t.connect();
85
+ const connectPromise = t.connect()
85
86
 
86
87
  // Simulate WebSocket open
87
- const ws = MockWebSocket.instances[0]!;
88
- expect(ws.url).toBe('ws://localhost:8080/v1');
89
- expect(ws.protocols).toEqual(['walletpair.v1']);
88
+ const ws = MockWebSocket.instances[0]
89
+ expect(ws).toBeDefined()
90
+ expect(ws?.url).toBe('ws://localhost:8080/v1')
91
+ expect(ws?.protocols).toEqual(['walletpair.v1'])
90
92
 
91
- ws.simulateOpen();
93
+ ws?.simulateOpen()
92
94
 
93
- await connectPromise;
94
- expect(t.state).toBe('connected');
95
- });
95
+ await connectPromise
96
+ expect(t.state).toBe('connected')
97
+ })
96
98
 
97
99
  it('rejects on connection failure', async () => {
98
- const t = new WebSocketTransport('ws://localhost:8080/v1');
100
+ const t = new WebSocketTransport('ws://localhost:8080/v1')
99
101
 
100
- const connectPromise = t.connect();
102
+ const connectPromise = t.connect()
101
103
 
102
- const ws = MockWebSocket.instances[0]!;
103
- ws.simulateError();
104
- ws.simulateClose();
104
+ const ws = MockWebSocket.instances[0]
105
+ ws?.simulateError()
106
+ ws?.simulateClose()
105
107
 
106
- await expect(connectPromise).rejects.toThrow('WebSocket connection failed');
107
- expect(t.state).toBe('disconnected');
108
- });
108
+ await expect(connectPromise).rejects.toThrow('WebSocket connection failed')
109
+ expect(t.state).toBe('disconnected')
110
+ })
109
111
 
110
112
  it('calls onOpen handler on successful connection', async () => {
111
- const t = new WebSocketTransport('ws://localhost:8080/v1');
112
- const openHandler = vi.fn();
113
- t.onOpen(openHandler);
113
+ const t = new WebSocketTransport('ws://localhost:8080/v1')
114
+ const openHandler = vi.fn()
115
+ t.onOpen(openHandler)
114
116
 
115
- const promise = t.connect();
116
- MockWebSocket.instances[0]!.simulateOpen();
117
- await promise;
117
+ const promise = t.connect()
118
+ MockWebSocket.instances[0]?.simulateOpen()
119
+ await promise
118
120
 
119
- expect(openHandler).toHaveBeenCalledTimes(1);
120
- });
121
- });
121
+ expect(openHandler).toHaveBeenCalledTimes(1)
122
+ })
123
+ })
122
124
 
123
125
  describe('send', () => {
124
126
  it('sends JSON-serialized message', async () => {
125
- const t = new WebSocketTransport('ws://localhost:8080/v1');
126
- const promise = t.connect();
127
- MockWebSocket.instances[0]!.simulateOpen();
128
- await promise;
127
+ const t = new WebSocketTransport('ws://localhost:8080/v1')
128
+ const promise = t.connect()
129
+ MockWebSocket.instances[0]?.simulateOpen()
130
+ await promise
129
131
 
130
- const msg = { v: 1, t: 'ping', ch: 'abc', ts: 123 } as any;
131
- t.send(msg);
132
+ const msg = { v: 1, t: 'ping', ch: 'abc', ts: 123 } as unknown as ProtocolMessage
133
+ t.send(msg)
132
134
 
133
- const ws = MockWebSocket.instances[0]!;
134
- expect(ws.sentMessages).toHaveLength(1);
135
- expect(JSON.parse(ws.sentMessages[0]!)).toEqual(msg);
136
- });
135
+ const ws = MockWebSocket.instances[0]
136
+ expect(ws?.sentMessages).toHaveLength(1)
137
+ expect(JSON.parse(ws?.sentMessages[0] ?? '')).toEqual(msg)
138
+ })
137
139
 
138
140
  it('does nothing when not connected', () => {
139
- const t = new WebSocketTransport('ws://localhost:8080/v1');
141
+ const t = new WebSocketTransport('ws://localhost:8080/v1')
140
142
  // Not connected, should not throw
141
- t.send({ v: 1, t: 'ping', ch: 'abc', ts: 123 } as any);
142
- });
143
- });
143
+ t.send({ v: 1, t: 'ping', ch: 'abc', ts: 123 } as unknown as ProtocolMessage)
144
+ })
145
+ })
144
146
 
145
147
  describe('receive', () => {
146
148
  it('calls message handler on incoming messages', async () => {
147
- const t = new WebSocketTransport('ws://localhost:8080/v1');
148
- const handler = vi.fn();
149
- t.onMessage(handler);
149
+ const t = new WebSocketTransport('ws://localhost:8080/v1')
150
+ const handler = vi.fn()
151
+ t.onMessage(handler)
150
152
 
151
- const promise = t.connect();
152
- MockWebSocket.instances[0]!.simulateOpen();
153
- await promise;
153
+ const promise = t.connect()
154
+ MockWebSocket.instances[0]?.simulateOpen()
155
+ await promise
154
156
 
155
- const msg = { v: 1, t: 'ready', ch: 'abc', state: 'waiting' };
156
- MockWebSocket.instances[0]!.simulateMessage(JSON.stringify(msg));
157
+ const msg = { v: 1, t: 'ready', ch: 'abc', state: 'waiting' }
158
+ MockWebSocket.instances[0]?.simulateMessage(JSON.stringify(msg))
157
159
 
158
- expect(handler).toHaveBeenCalledWith(msg);
159
- });
160
+ expect(handler).toHaveBeenCalledWith(msg)
161
+ })
160
162
 
161
163
  it('ignores malformed JSON', async () => {
162
- const t = new WebSocketTransport('ws://localhost:8080/v1');
163
- const handler = vi.fn();
164
- t.onMessage(handler);
164
+ const t = new WebSocketTransport('ws://localhost:8080/v1')
165
+ const handler = vi.fn()
166
+ t.onMessage(handler)
165
167
 
166
- const promise = t.connect();
167
- MockWebSocket.instances[0]!.simulateOpen();
168
- await promise;
168
+ const promise = t.connect()
169
+ MockWebSocket.instances[0]?.simulateOpen()
170
+ await promise
169
171
 
170
- MockWebSocket.instances[0]!.simulateMessage('not json');
171
- expect(handler).not.toHaveBeenCalled();
172
- });
173
- });
172
+ MockWebSocket.instances[0]?.simulateMessage('not json')
173
+ expect(handler).not.toHaveBeenCalled()
174
+ })
175
+ })
174
176
 
175
177
  describe('disconnect', () => {
176
178
  it('transitions to disconnected', async () => {
177
- const t = new WebSocketTransport('ws://localhost:8080/v1');
178
- const promise = t.connect();
179
- MockWebSocket.instances[0]!.simulateOpen();
180
- await promise;
179
+ const t = new WebSocketTransport('ws://localhost:8080/v1')
180
+ const promise = t.connect()
181
+ MockWebSocket.instances[0]?.simulateOpen()
182
+ await promise
181
183
 
182
- t.disconnect();
183
- expect(t.state).toBe('disconnected');
184
- });
184
+ t.disconnect()
185
+ expect(t.state).toBe('disconnected')
186
+ })
185
187
 
186
188
  it('does not call close handler on intentional disconnect', async () => {
187
- const t = new WebSocketTransport('ws://localhost:8080/v1');
188
- const closeHandler = vi.fn();
189
- t.onClose(closeHandler);
189
+ const t = new WebSocketTransport('ws://localhost:8080/v1')
190
+ const closeHandler = vi.fn()
191
+ t.onClose(closeHandler)
190
192
 
191
- const promise = t.connect();
192
- MockWebSocket.instances[0]!.simulateOpen();
193
- await promise;
193
+ const promise = t.connect()
194
+ MockWebSocket.instances[0]?.simulateOpen()
195
+ await promise
194
196
 
195
- t.disconnect();
196
- expect(closeHandler).not.toHaveBeenCalled();
197
- });
198
- });
197
+ t.disconnect()
198
+ expect(closeHandler).not.toHaveBeenCalled()
199
+ })
200
+ })
199
201
 
200
202
  describe('unexpected close', () => {
201
203
  it('calls close handler on unexpected transport close', async () => {
202
- const t = new WebSocketTransport('ws://localhost:8080/v1');
203
- const closeHandler = vi.fn();
204
- t.onClose(closeHandler);
204
+ const t = new WebSocketTransport('ws://localhost:8080/v1')
205
+ const closeHandler = vi.fn()
206
+ t.onClose(closeHandler)
205
207
 
206
- const promise = t.connect();
207
- MockWebSocket.instances[0]!.simulateOpen();
208
- await promise;
208
+ const promise = t.connect()
209
+ MockWebSocket.instances[0]?.simulateOpen()
210
+ await promise
209
211
 
210
212
  // Simulate unexpected close (e.g., network drop)
211
- MockWebSocket.instances[0]!.simulateClose();
213
+ MockWebSocket.instances[0]?.simulateClose()
212
214
 
213
- expect(closeHandler).toHaveBeenCalledTimes(1);
214
- expect(t.state).toBe('disconnected');
215
- });
216
- });
215
+ expect(closeHandler).toHaveBeenCalledTimes(1)
216
+ expect(t.state).toBe('disconnected')
217
+ })
218
+ })
217
219
 
218
220
  describe('setUrl', () => {
219
221
  it('updates the URL for next connection', async () => {
220
- const t = new WebSocketTransport('ws://localhost:8080/v1');
221
- t.setUrl('ws://other:9090/v1');
222
-
223
- const promise = t.connect();
224
- const ws = MockWebSocket.instances[0]!;
225
- expect(ws.url).toBe('ws://other:9090/v1');
226
-
227
- ws.simulateOpen();
228
- await promise;
229
- });
230
- });
231
- });
222
+ const t = new WebSocketTransport('ws://localhost:8080/v1')
223
+ t.setUrl('ws://other:9090/v1')
224
+
225
+ const promise = t.connect()
226
+ const ws = MockWebSocket.instances[0]
227
+ expect(ws?.url).toBe('ws://other:9090/v1')
228
+
229
+ ws?.simulateOpen()
230
+ await promise
231
+ })
232
+ })
233
+ })
@@ -4,90 +4,100 @@
4
4
  * Works in browsers, Node.js 22+, Deno, Bun — anything with a global WebSocket.
5
5
  */
6
6
 
7
- import type { Transport, TransportState, ProtocolMessage } from './types.js';
7
+ import type { ProtocolMessage, Transport, TransportState } from './types.js'
8
8
 
9
9
  export interface WebSocketTransportOptions {
10
- url: string;
11
- protocols?: string[];
10
+ url: string
11
+ protocols?: string[]
12
12
  }
13
13
 
14
14
  export class WebSocketTransport implements Transport {
15
- state: TransportState = 'disconnected';
15
+ state: TransportState = 'disconnected'
16
16
 
17
- private ws: WebSocket | null = null;
17
+ private ws: WebSocket | null = null
18
18
  /** Current relay URL. Readable for channel hint injection. */
19
- url: string;
20
- private protocols: string[];
19
+ url: string
20
+ private protocols: string[]
21
21
 
22
- private messageHandler: ((msg: ProtocolMessage) => void) | null = null;
23
- private closeHandler: (() => void) | null = null;
24
- private openHandler: (() => void) | null = null;
22
+ private messageHandler: ((msg: ProtocolMessage) => void) | null = null
23
+ private closeHandler: (() => void) | null = null
24
+ private openHandler: (() => void) | null = null
25
25
 
26
26
  constructor(options: WebSocketTransportOptions | string) {
27
27
  if (typeof options === 'string') {
28
- this.url = options;
29
- this.protocols = ['walletpair.v1'];
28
+ this.url = options
29
+ this.protocols = ['walletpair.v1']
30
30
  } else {
31
- this.url = options.url;
32
- this.protocols = options.protocols ?? ['walletpair.v1'];
31
+ this.url = options.url
32
+ this.protocols = options.protocols ?? ['walletpair.v1']
33
33
  }
34
34
  }
35
35
 
36
- onMessage(handler: (msg: ProtocolMessage) => void): void { this.messageHandler = handler; }
37
- onClose(handler: () => void): void { this.closeHandler = handler; }
38
- onOpen(handler: () => void): void { this.openHandler = handler; }
36
+ onMessage(handler: (msg: ProtocolMessage) => void): void {
37
+ this.messageHandler = handler
38
+ }
39
+ onClose(handler: () => void): void {
40
+ this.closeHandler = handler
41
+ }
42
+ onOpen(handler: () => void): void {
43
+ this.openHandler = handler
44
+ }
39
45
 
40
46
  /** Update the relay URL (useful for reconnect to a different relay). */
41
47
  setUrl(url: string): void {
42
- this.url = url;
48
+ this.url = url
43
49
  }
44
50
 
45
51
  connect(): Promise<void> {
46
52
  return new Promise<void>((resolve, reject) => {
47
- this.state = 'connecting';
48
- const ws = new WebSocket(this.url, this.protocols);
53
+ this.state = 'connecting'
54
+ const ws = new WebSocket(this.url, this.protocols)
49
55
 
50
56
  ws.onopen = () => {
51
- this.state = 'connected';
52
- this.ws = ws;
53
- this.openHandler?.();
54
- resolve();
55
- };
57
+ this.state = 'connected'
58
+ this.ws = ws
59
+ this.openHandler?.()
60
+ resolve()
61
+ }
56
62
 
57
63
  ws.onmessage = (event: MessageEvent) => {
58
64
  if (this.messageHandler) {
59
- try { this.messageHandler(JSON.parse(event.data as string)); } catch { /* bad json */ }
65
+ try {
66
+ this.messageHandler(JSON.parse(event.data as string))
67
+ } catch {
68
+ /* bad json */
69
+ }
60
70
  }
61
- };
71
+ }
62
72
 
63
73
  ws.onclose = () => {
64
- const wasConnected = this.state === 'connected';
65
- this.state = 'disconnected';
66
- this.ws = null;
74
+ const wasConnected = this.state === 'connected'
75
+ this.state = 'disconnected'
76
+ this.ws = null
67
77
  if (wasConnected) {
68
- this.closeHandler?.();
78
+ this.closeHandler?.()
69
79
  } else {
70
- reject(new Error('WebSocket connection failed'));
80
+ reject(new Error('WebSocket connection failed'))
71
81
  }
72
- };
82
+ }
73
83
 
74
84
  ws.onerror = () => {
75
85
  // onclose will fire after onerror, which handles the reject
76
- };
77
- });
86
+ }
87
+ })
78
88
  }
79
89
 
80
90
  send(msg: ProtocolMessage): void {
81
- if (!this.ws || this.state !== 'connected') return;
82
- this.ws.send(JSON.stringify(msg));
91
+ if (!this.ws || this.state !== 'connected') return
92
+ this.ws.send(JSON.stringify(msg))
83
93
  }
84
94
 
85
95
  disconnect(): void {
86
96
  if (this.ws) {
87
- this.ws.onclose = null;
88
- this.ws.close();
89
- this.ws = null;
97
+ this.ws.onclose = null
98
+ this.ws.close()
99
+ this.ws = null
90
100
  }
91
- this.state = 'disconnected';
101
+ this.state = 'disconnected'
92
102
  }
93
103
  }