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.
- package/README.md +13 -0
- package/dist/ble/framing.d.ts.map +1 -1
- package/dist/ble/framing.js +2 -2
- package/dist/ble/framing.js.map +1 -1
- package/dist/ble/index.d.ts +2 -2
- package/dist/ble/index.d.ts.map +1 -1
- package/dist/ble/index.js +2 -2
- package/dist/ble/index.js.map +1 -1
- package/dist/ble/web-ble-transport.d.ts +1 -1
- package/dist/ble/web-ble-transport.d.ts.map +1 -1
- package/dist/ble/web-ble-transport.js +23 -12
- package/dist/ble/web-ble-transport.js.map +1 -1
- package/dist/crypto.d.ts.map +1 -1
- package/dist/crypto.js +29 -12
- package/dist/crypto.js.map +1 -1
- package/dist/dapp-session.d.ts.map +1 -1
- package/dist/dapp-session.js +15 -5
- package/dist/dapp-session.js.map +1 -1
- package/dist/emitter.d.ts +1 -3
- package/dist/emitter.d.ts.map +1 -1
- package/dist/emitter.js +4 -2
- package/dist/emitter.js.map +1 -1
- package/dist/evm/eip1193.d.ts +2 -2
- package/dist/evm/eip1193.d.ts.map +1 -1
- package/dist/evm/eip1193.js +32 -18
- package/dist/evm/eip1193.js.map +1 -1
- package/dist/evm/index.d.ts +2 -2
- package/dist/evm/index.d.ts.map +1 -1
- package/dist/evm/index.js.map +1 -1
- package/dist/wallet-session.d.ts.map +1 -1
- package/dist/wallet-session.js +4 -3
- package/dist/wallet-session.js.map +1 -1
- package/dist/ws-transport.d.ts +3 -2
- package/dist/ws-transport.d.ts.map +1 -1
- package/dist/ws-transport.js +13 -4
- package/dist/ws-transport.js.map +1 -1
- package/package.json +20 -1
- package/src/__tests__/adversarial/crypto-attacks.test.ts +240 -233
- package/src/__tests__/adversarial/malicious-dapp.test.ts +228 -194
- package/src/__tests__/adversarial/malicious-relay.test.ts +292 -220
- package/src/__tests__/adversarial/malicious-wallet.test.ts +246 -180
- package/src/__tests__/spec-compliance/canonical-json.test.ts +105 -105
- package/src/__tests__/spec-compliance/crypto-vectors.test.ts +149 -154
- package/src/__tests__/spec-compliance/message-format.test.ts +180 -151
- package/src/__tests__/spec-compliance/sequence-numbers.test.ts +142 -149
- package/src/__tests__/spec-compliance/state-machine.test.ts +203 -180
- package/src/ble/framing.test.ts +122 -114
- package/src/ble/framing.ts +48 -51
- package/src/ble/index.ts +7 -7
- package/src/ble/web-ble-transport.test.ts +93 -84
- package/src/ble/web-ble-transport.ts +70 -57
- package/src/ble/web-bluetooth.d.ts +19 -19
- package/src/canonical-json.test.ts +301 -285
- package/src/crypto-directional.test.ts +155 -129
- package/src/crypto-hardening.test.ts +292 -283
- package/src/crypto.test.ts +364 -346
- package/src/crypto.ts +185 -175
- package/src/dapp-session.test.ts +522 -385
- package/src/dapp-session.ts +17 -11
- package/src/emitter.test.ts +122 -122
- package/src/emitter.ts +20 -18
- package/src/evm/eip1193.test.ts +283 -205
- package/src/evm/eip1193.ts +162 -138
- package/src/evm/index.ts +5 -5
- package/src/evm/wagmi.test.ts +1 -1
- package/src/integration.test.ts +329 -201
- package/src/security.test.ts +331 -238
- package/src/sequence-validation.test.ts +6 -9
- package/src/test-helpers.ts +102 -78
- package/src/types.test.ts +45 -50
- package/src/wallet-session.test.ts +611 -383
- package/src/wallet-session.ts +7 -9
- package/src/ws-transport.test.ts +141 -139
- package/src/ws-transport.ts +52 -41
package/src/ble/framing.test.ts
CHANGED
|
@@ -1,196 +1,204 @@
|
|
|
1
|
-
import { describe,
|
|
2
|
-
import {
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { DEFAULT_FRAME_PAYLOAD, Defragmenter, frameMessage, MIN_FRAME_PAYLOAD } from './framing.js'
|
|
3
3
|
|
|
4
4
|
describe('frameMessage', () => {
|
|
5
5
|
it('frames empty string as single frame with both flags', () => {
|
|
6
|
-
const frames = frameMessage('')
|
|
7
|
-
expect(frames).toHaveLength(1)
|
|
8
|
-
|
|
9
|
-
expect(
|
|
10
|
-
|
|
6
|
+
const frames = frameMessage('')
|
|
7
|
+
expect(frames).toHaveLength(1)
|
|
8
|
+
const f0 = frames[0]
|
|
9
|
+
expect(f0).toBeDefined()
|
|
10
|
+
expect(f0?.[0]).toBe(0x03) // first + last
|
|
11
|
+
expect(f0).toHaveLength(3) // header only
|
|
12
|
+
})
|
|
11
13
|
|
|
12
14
|
it('frames short message as single frame', () => {
|
|
13
|
-
const msg = '{"v":1,"t":"ping"}'
|
|
14
|
-
const frames = frameMessage(msg)
|
|
15
|
-
expect(frames).toHaveLength(1)
|
|
16
|
-
|
|
15
|
+
const msg = '{"v":1,"t":"ping"}'
|
|
16
|
+
const frames = frameMessage(msg)
|
|
17
|
+
expect(frames).toHaveLength(1)
|
|
18
|
+
const f0 = frames[0]
|
|
19
|
+
expect(f0).toBeDefined()
|
|
20
|
+
expect(f0?.[0]).toBe(0x03) // first + last
|
|
17
21
|
|
|
18
22
|
// Verify total length in header
|
|
19
|
-
const payload = new TextEncoder().encode(msg)
|
|
20
|
-
const totalLen = (
|
|
21
|
-
expect(totalLen).toBe(payload.length)
|
|
23
|
+
const payload = new TextEncoder().encode(msg)
|
|
24
|
+
const totalLen = ((f0?.[1] ?? 0) << 8) | (f0?.[2] ?? 0)
|
|
25
|
+
expect(totalLen).toBe(payload.length)
|
|
22
26
|
|
|
23
27
|
// Verify payload
|
|
24
|
-
const extracted =
|
|
25
|
-
expect(new TextDecoder().decode(extracted)).toBe(msg)
|
|
26
|
-
})
|
|
28
|
+
const extracted = f0?.subarray(3)
|
|
29
|
+
expect(new TextDecoder().decode(extracted)).toBe(msg)
|
|
30
|
+
})
|
|
27
31
|
|
|
28
32
|
it('fragments message exceeding maxPayload', () => {
|
|
29
|
-
const longMsg = 'A'.repeat(100)
|
|
30
|
-
const frames = frameMessage(longMsg, 30)
|
|
33
|
+
const longMsg = 'A'.repeat(100)
|
|
34
|
+
const frames = frameMessage(longMsg, 30)
|
|
31
35
|
|
|
32
|
-
expect(frames.length).toBeGreaterThan(1)
|
|
36
|
+
expect(frames.length).toBeGreaterThan(1)
|
|
33
37
|
|
|
34
38
|
// First frame: flag=0x01 (first only)
|
|
35
|
-
|
|
36
|
-
expect(
|
|
39
|
+
const f0 = frames[0]
|
|
40
|
+
expect(f0).toBeDefined()
|
|
41
|
+
expect((f0?.[0] ?? 0) & 0x01).toBe(0x01) // first bit set
|
|
42
|
+
expect((f0?.[0] ?? 0) & 0x02).toBe(0x00) // last bit not set
|
|
37
43
|
|
|
38
44
|
// Last frame: flag=0x02 (last only)
|
|
39
|
-
const lastFrame = frames[frames.length - 1]
|
|
40
|
-
expect(lastFrame
|
|
41
|
-
expect(lastFrame[0]
|
|
45
|
+
const lastFrame = frames[frames.length - 1]
|
|
46
|
+
expect(lastFrame).toBeDefined()
|
|
47
|
+
expect((lastFrame?.[0] ?? 0) & 0x01).toBe(0x00) // first bit not set
|
|
48
|
+
expect((lastFrame?.[0] ?? 0) & 0x02).toBe(0x02) // last bit set
|
|
42
49
|
|
|
43
50
|
// Middle frames (if any): flag=0x00
|
|
44
51
|
for (let i = 1; i < frames.length - 1; i++) {
|
|
45
|
-
expect(frames[i]
|
|
52
|
+
expect(frames[i]?.[0]).toBe(0x00)
|
|
46
53
|
}
|
|
47
|
-
})
|
|
54
|
+
})
|
|
48
55
|
|
|
49
56
|
it('first frame contains total length in header', () => {
|
|
50
|
-
const msg = 'X'.repeat(200)
|
|
51
|
-
const frames = frameMessage(msg, 50)
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
57
|
+
const msg = 'X'.repeat(200)
|
|
58
|
+
const frames = frameMessage(msg, 50)
|
|
59
|
+
const f0 = frames[0]
|
|
60
|
+
const totalLen = ((f0?.[1] ?? 0) << 8) | (f0?.[2] ?? 0)
|
|
61
|
+
expect(totalLen).toBe(new TextEncoder().encode(msg).length)
|
|
62
|
+
})
|
|
55
63
|
|
|
56
64
|
it('respects MIN_FRAME_PAYLOAD even when maxPayload is smaller', () => {
|
|
57
|
-
const msg = 'A'.repeat(100)
|
|
58
|
-
const frames = frameMessage(msg, 5)
|
|
65
|
+
const msg = 'A'.repeat(100)
|
|
66
|
+
const frames = frameMessage(msg, 5) // smaller than MIN_FRAME_PAYLOAD
|
|
59
67
|
// Each payload chunk should be at least MIN_FRAME_PAYLOAD
|
|
60
68
|
for (const frame of frames) {
|
|
61
|
-
const payloadSize = frame.length - 3
|
|
69
|
+
const payloadSize = frame.length - 3
|
|
62
70
|
if (payloadSize > 0) {
|
|
63
71
|
// Last frame might be shorter
|
|
64
|
-
if ((frame[0]
|
|
65
|
-
expect(payloadSize).toBeGreaterThanOrEqual(MIN_FRAME_PAYLOAD)
|
|
72
|
+
if (((frame[0] ?? 0) & 0x02) === 0) {
|
|
73
|
+
expect(payloadSize).toBeGreaterThanOrEqual(MIN_FRAME_PAYLOAD)
|
|
66
74
|
}
|
|
67
75
|
}
|
|
68
76
|
}
|
|
69
|
-
})
|
|
77
|
+
})
|
|
70
78
|
|
|
71
79
|
it('uses DEFAULT_FRAME_PAYLOAD when no maxPayload specified', () => {
|
|
72
|
-
const msg = 'B'.repeat(DEFAULT_FRAME_PAYLOAD + 100)
|
|
73
|
-
const frames = frameMessage(msg)
|
|
74
|
-
expect(frames.length).toBe(2)
|
|
75
|
-
})
|
|
76
|
-
})
|
|
80
|
+
const msg = 'B'.repeat(DEFAULT_FRAME_PAYLOAD + 100)
|
|
81
|
+
const frames = frameMessage(msg)
|
|
82
|
+
expect(frames.length).toBe(2) // slightly over one full frame
|
|
83
|
+
})
|
|
84
|
+
})
|
|
77
85
|
|
|
78
86
|
describe('Defragmenter', () => {
|
|
79
87
|
it('reassembles single-frame message', () => {
|
|
80
|
-
const defrag = new Defragmenter()
|
|
81
|
-
const msg = '{"hello":"world"}'
|
|
82
|
-
const frames = frameMessage(msg)
|
|
88
|
+
const defrag = new Defragmenter()
|
|
89
|
+
const msg = '{"hello":"world"}'
|
|
90
|
+
const frames = frameMessage(msg)
|
|
83
91
|
|
|
84
|
-
expect(frames).toHaveLength(1)
|
|
85
|
-
const result = defrag.push(frames[0]
|
|
86
|
-
expect(result).toBe(msg)
|
|
87
|
-
})
|
|
92
|
+
expect(frames).toHaveLength(1)
|
|
93
|
+
const result = defrag.push(frames[0] ?? new Uint8Array())
|
|
94
|
+
expect(result).toBe(msg)
|
|
95
|
+
})
|
|
88
96
|
|
|
89
97
|
it('reassembles multi-frame message', () => {
|
|
90
|
-
const defrag = new Defragmenter()
|
|
91
|
-
const msg = 'A'.repeat(200)
|
|
92
|
-
const frames = frameMessage(msg, 50)
|
|
98
|
+
const defrag = new Defragmenter()
|
|
99
|
+
const msg = 'A'.repeat(200)
|
|
100
|
+
const frames = frameMessage(msg, 50)
|
|
93
101
|
|
|
94
|
-
expect(frames.length).toBeGreaterThan(1)
|
|
102
|
+
expect(frames.length).toBeGreaterThan(1)
|
|
95
103
|
|
|
96
104
|
for (let i = 0; i < frames.length - 1; i++) {
|
|
97
|
-
const result = defrag.push(frames[i]
|
|
98
|
-
expect(result).toBeNull()
|
|
105
|
+
const result = defrag.push(frames[i] ?? new Uint8Array())
|
|
106
|
+
expect(result).toBeNull() // not complete yet
|
|
99
107
|
}
|
|
100
108
|
|
|
101
|
-
const result = defrag.push(frames[frames.length - 1]
|
|
102
|
-
expect(result).toBe(msg)
|
|
103
|
-
})
|
|
109
|
+
const result = defrag.push(frames[frames.length - 1] ?? new Uint8Array())
|
|
110
|
+
expect(result).toBe(msg)
|
|
111
|
+
})
|
|
104
112
|
|
|
105
113
|
it('handles multiple messages in sequence', () => {
|
|
106
|
-
const defrag = new Defragmenter()
|
|
114
|
+
const defrag = new Defragmenter()
|
|
107
115
|
|
|
108
|
-
const msg1 = '{"first":true}'
|
|
109
|
-
const msg2 = '{"second":true}'
|
|
116
|
+
const msg1 = '{"first":true}'
|
|
117
|
+
const msg2 = '{"second":true}'
|
|
110
118
|
|
|
111
|
-
const frames1 = frameMessage(msg1)
|
|
112
|
-
const frames2 = frameMessage(msg2)
|
|
119
|
+
const frames1 = frameMessage(msg1)
|
|
120
|
+
const frames2 = frameMessage(msg2)
|
|
113
121
|
|
|
114
|
-
expect(defrag.push(frames1[0]
|
|
115
|
-
expect(defrag.push(frames2[0]
|
|
116
|
-
})
|
|
122
|
+
expect(defrag.push(frames1[0] ?? new Uint8Array())).toBe(msg1)
|
|
123
|
+
expect(defrag.push(frames2[0] ?? new Uint8Array())).toBe(msg2)
|
|
124
|
+
})
|
|
117
125
|
|
|
118
126
|
it('handles interleaved fragmented messages correctly via reset', () => {
|
|
119
|
-
const defrag = new Defragmenter()
|
|
127
|
+
const defrag = new Defragmenter()
|
|
120
128
|
|
|
121
129
|
// Start a message
|
|
122
|
-
const longMsg = 'X'.repeat(100)
|
|
123
|
-
const frames = frameMessage(longMsg, 30)
|
|
124
|
-
defrag.push(frames[0]
|
|
130
|
+
const longMsg = 'X'.repeat(100)
|
|
131
|
+
const frames = frameMessage(longMsg, 30)
|
|
132
|
+
defrag.push(frames[0] ?? new Uint8Array()) // first fragment
|
|
125
133
|
|
|
126
134
|
// Reset and start new message
|
|
127
|
-
defrag.reset()
|
|
128
|
-
const shortMsg = '{"ok":true}'
|
|
129
|
-
const shortFrames = frameMessage(shortMsg)
|
|
130
|
-
expect(defrag.push(shortFrames[0]
|
|
131
|
-
})
|
|
135
|
+
defrag.reset()
|
|
136
|
+
const shortMsg = '{"ok":true}'
|
|
137
|
+
const shortFrames = frameMessage(shortMsg)
|
|
138
|
+
expect(defrag.push(shortFrames[0] ?? new Uint8Array())).toBe(shortMsg)
|
|
139
|
+
})
|
|
132
140
|
|
|
133
141
|
it('ignores frames shorter than 3 bytes', () => {
|
|
134
|
-
const defrag = new Defragmenter()
|
|
135
|
-
expect(defrag.push(new Uint8Array([0x03, 0x00]))).toBeNull()
|
|
136
|
-
expect(defrag.push(new Uint8Array([0x03]))).toBeNull()
|
|
137
|
-
expect(defrag.push(new Uint8Array([]))).toBeNull()
|
|
138
|
-
})
|
|
142
|
+
const defrag = new Defragmenter()
|
|
143
|
+
expect(defrag.push(new Uint8Array([0x03, 0x00]))).toBeNull()
|
|
144
|
+
expect(defrag.push(new Uint8Array([0x03]))).toBeNull()
|
|
145
|
+
expect(defrag.push(new Uint8Array([]))).toBeNull()
|
|
146
|
+
})
|
|
139
147
|
|
|
140
148
|
it('reassembles unicode content correctly', () => {
|
|
141
|
-
const defrag = new Defragmenter()
|
|
142
|
-
const msg = '{"text":"你好世界🌍"}'
|
|
143
|
-
const frames = frameMessage(msg, 20)
|
|
149
|
+
const defrag = new Defragmenter()
|
|
150
|
+
const msg = '{"text":"你好世界🌍"}'
|
|
151
|
+
const frames = frameMessage(msg, 20)
|
|
144
152
|
|
|
145
|
-
let result: string | null = null
|
|
153
|
+
let result: string | null = null
|
|
146
154
|
for (const frame of frames) {
|
|
147
|
-
result = defrag.push(frame)
|
|
155
|
+
result = defrag.push(frame)
|
|
148
156
|
}
|
|
149
|
-
expect(result).toBe(msg)
|
|
150
|
-
})
|
|
157
|
+
expect(result).toBe(msg)
|
|
158
|
+
})
|
|
151
159
|
|
|
152
160
|
it('handles large messages (> 64KB total length)', () => {
|
|
153
|
-
const defrag = new Defragmenter()
|
|
161
|
+
const defrag = new Defragmenter()
|
|
154
162
|
// total_length field is 2 bytes, so max representable is 65535
|
|
155
163
|
// But the code handles growth beyond that via the safety check
|
|
156
|
-
const msg = 'Z'.repeat(1000)
|
|
157
|
-
const frames = frameMessage(msg, 100)
|
|
164
|
+
const msg = 'Z'.repeat(1000)
|
|
165
|
+
const frames = frameMessage(msg, 100)
|
|
158
166
|
|
|
159
|
-
let result: string | null = null
|
|
167
|
+
let result: string | null = null
|
|
160
168
|
for (const frame of frames) {
|
|
161
|
-
result = defrag.push(frame)
|
|
169
|
+
result = defrag.push(frame)
|
|
162
170
|
}
|
|
163
|
-
expect(result).toBe(msg)
|
|
164
|
-
})
|
|
171
|
+
expect(result).toBe(msg)
|
|
172
|
+
})
|
|
165
173
|
|
|
166
174
|
it('reset() clears internal state', () => {
|
|
167
|
-
const defrag = new Defragmenter()
|
|
168
|
-
const longMsg = 'Y'.repeat(100)
|
|
169
|
-
const frames = frameMessage(longMsg, 30)
|
|
175
|
+
const defrag = new Defragmenter()
|
|
176
|
+
const longMsg = 'Y'.repeat(100)
|
|
177
|
+
const frames = frameMessage(longMsg, 30)
|
|
170
178
|
|
|
171
179
|
// Push first frame
|
|
172
|
-
defrag.push(frames[0]
|
|
173
|
-
defrag.reset()
|
|
180
|
+
defrag.push(frames[0] ?? new Uint8Array())
|
|
181
|
+
defrag.reset()
|
|
174
182
|
|
|
175
183
|
// After reset, pushing a complete single-frame message should work
|
|
176
|
-
const shortMsg = '{"reset":true}'
|
|
177
|
-
const shortFrames = frameMessage(shortMsg)
|
|
178
|
-
expect(defrag.push(shortFrames[0]
|
|
179
|
-
})
|
|
184
|
+
const shortMsg = '{"reset":true}'
|
|
185
|
+
const shortFrames = frameMessage(shortMsg)
|
|
186
|
+
expect(defrag.push(shortFrames[0] ?? new Uint8Array())).toBe(shortMsg)
|
|
187
|
+
})
|
|
180
188
|
|
|
181
189
|
it('frame/defrag round-trip preserves exact content for various sizes', () => {
|
|
182
|
-
const defrag = new Defragmenter()
|
|
190
|
+
const defrag = new Defragmenter()
|
|
183
191
|
for (const size of [0, 1, 19, 20, 21, 50, 100, 509, 510, 1000]) {
|
|
184
|
-
defrag.reset()
|
|
192
|
+
defrag.reset()
|
|
185
193
|
// Size 0 frame has no payload text, skip
|
|
186
|
-
if (size === 0) continue
|
|
187
|
-
const msg = JSON.stringify({ data: 'x'.repeat(size) })
|
|
188
|
-
const frames = frameMessage(msg, 50)
|
|
189
|
-
let result: string | null = null
|
|
194
|
+
if (size === 0) continue
|
|
195
|
+
const msg = JSON.stringify({ data: 'x'.repeat(size) })
|
|
196
|
+
const frames = frameMessage(msg, 50)
|
|
197
|
+
let result: string | null = null
|
|
190
198
|
for (const frame of frames) {
|
|
191
|
-
result = defrag.push(frame)
|
|
199
|
+
result = defrag.push(frame)
|
|
192
200
|
}
|
|
193
|
-
expect(result).toBe(msg)
|
|
201
|
+
expect(result).toBe(msg)
|
|
194
202
|
}
|
|
195
|
-
})
|
|
196
|
-
})
|
|
203
|
+
})
|
|
204
|
+
})
|
package/src/ble/framing.ts
CHANGED
|
@@ -7,94 +7,91 @@
|
|
|
7
7
|
* flags bit 1: last fragment
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
const FLAG_FIRST = 0x01
|
|
11
|
-
const FLAG_LAST = 0x02
|
|
10
|
+
const FLAG_FIRST = 0x01
|
|
11
|
+
const FLAG_LAST = 0x02
|
|
12
12
|
|
|
13
|
-
export const DEFAULT_FRAME_PAYLOAD = 509
|
|
14
|
-
export const MIN_FRAME_PAYLOAD = 20
|
|
13
|
+
export const DEFAULT_FRAME_PAYLOAD = 509
|
|
14
|
+
export const MIN_FRAME_PAYLOAD = 20
|
|
15
15
|
|
|
16
|
-
export const BLE_SERVICE_UUID = 'e3a10001-7770-4270-8000-000077700001'
|
|
17
|
-
export const BLE_WRITE_CHAR_UUID = 'e3a10002-7770-4270-8000-000077700001'
|
|
18
|
-
export const BLE_NOTIFY_CHAR_UUID = 'e3a10003-7770-4270-8000-000077700001'
|
|
16
|
+
export const BLE_SERVICE_UUID = 'e3a10001-7770-4270-8000-000077700001'
|
|
17
|
+
export const BLE_WRITE_CHAR_UUID = 'e3a10002-7770-4270-8000-000077700001'
|
|
18
|
+
export const BLE_NOTIFY_CHAR_UUID = 'e3a10003-7770-4270-8000-000077700001'
|
|
19
19
|
|
|
20
20
|
/** Split a JSON string into BLE frames. */
|
|
21
|
-
export function frameMessage(
|
|
22
|
-
jsonStr
|
|
23
|
-
|
|
24
|
-
): Uint8Array[] {
|
|
25
|
-
const payload = new TextEncoder().encode(jsonStr);
|
|
26
|
-
const frames: Uint8Array[] = [];
|
|
21
|
+
export function frameMessage(jsonStr: string, maxPayload = DEFAULT_FRAME_PAYLOAD): Uint8Array[] {
|
|
22
|
+
const payload = new TextEncoder().encode(jsonStr)
|
|
23
|
+
const frames: Uint8Array[] = []
|
|
27
24
|
|
|
28
25
|
if (payload.length === 0) {
|
|
29
|
-
const frame = new Uint8Array(3)
|
|
30
|
-
frame[0] = FLAG_FIRST | FLAG_LAST
|
|
31
|
-
return [frame]
|
|
26
|
+
const frame = new Uint8Array(3)
|
|
27
|
+
frame[0] = FLAG_FIRST | FLAG_LAST
|
|
28
|
+
return [frame]
|
|
32
29
|
}
|
|
33
30
|
|
|
34
|
-
const chunkSize = Math.max(maxPayload, MIN_FRAME_PAYLOAD)
|
|
31
|
+
const chunkSize = Math.max(maxPayload, MIN_FRAME_PAYLOAD)
|
|
35
32
|
|
|
36
33
|
for (let offset = 0; offset < payload.length; offset += chunkSize) {
|
|
37
|
-
const isFirst = offset === 0
|
|
38
|
-
const end = Math.min(offset + chunkSize, payload.length)
|
|
39
|
-
const isLast = end === payload.length
|
|
40
|
-
const fragment = payload.subarray(offset, end)
|
|
34
|
+
const isFirst = offset === 0
|
|
35
|
+
const end = Math.min(offset + chunkSize, payload.length)
|
|
36
|
+
const isLast = end === payload.length
|
|
37
|
+
const fragment = payload.subarray(offset, end)
|
|
41
38
|
|
|
42
|
-
const frame = new Uint8Array(3 + fragment.length)
|
|
43
|
-
frame[0] = (isFirst ? FLAG_FIRST : 0) | (isLast ? FLAG_LAST : 0)
|
|
39
|
+
const frame = new Uint8Array(3 + fragment.length)
|
|
40
|
+
frame[0] = (isFirst ? FLAG_FIRST : 0) | (isLast ? FLAG_LAST : 0)
|
|
44
41
|
if (isFirst) {
|
|
45
|
-
frame[1] = (payload.length >> 8) & 0xff
|
|
46
|
-
frame[2] = payload.length & 0xff
|
|
42
|
+
frame[1] = (payload.length >> 8) & 0xff
|
|
43
|
+
frame[2] = payload.length & 0xff
|
|
47
44
|
}
|
|
48
|
-
frame.set(fragment, 3)
|
|
49
|
-
frames.push(frame)
|
|
45
|
+
frame.set(fragment, 3)
|
|
46
|
+
frames.push(frame)
|
|
50
47
|
}
|
|
51
48
|
|
|
52
|
-
return frames
|
|
49
|
+
return frames
|
|
53
50
|
}
|
|
54
51
|
|
|
55
52
|
/** Accumulates BLE frames and emits complete JSON strings. */
|
|
56
53
|
export class Defragmenter {
|
|
57
|
-
private buffer: Uint8Array | null = null
|
|
58
|
-
private offset = 0
|
|
54
|
+
private buffer: Uint8Array | null = null
|
|
55
|
+
private offset = 0
|
|
59
56
|
|
|
60
57
|
push(data: Uint8Array): string | null {
|
|
61
|
-
if (data.length < 3) return null
|
|
58
|
+
if (data.length < 3) return null
|
|
62
59
|
|
|
63
|
-
const flags = data[0]
|
|
64
|
-
const isFirst = !!(flags & FLAG_FIRST)
|
|
65
|
-
const isLast = !!(flags & FLAG_LAST)
|
|
66
|
-
const fragment = data.subarray(3)
|
|
60
|
+
const flags = data[0] ?? 0
|
|
61
|
+
const isFirst = !!(flags & FLAG_FIRST)
|
|
62
|
+
const isLast = !!(flags & FLAG_LAST)
|
|
63
|
+
const fragment = data.subarray(3)
|
|
67
64
|
|
|
68
65
|
if (isFirst) {
|
|
69
|
-
const totalLength = (data[1]
|
|
70
|
-
this.buffer = new Uint8Array(totalLength || fragment.length)
|
|
71
|
-
this.offset = 0
|
|
66
|
+
const totalLength = ((data[1] ?? 0) << 8) | (data[2] ?? 0)
|
|
67
|
+
this.buffer = new Uint8Array(totalLength || fragment.length)
|
|
68
|
+
this.offset = 0
|
|
72
69
|
}
|
|
73
70
|
|
|
74
71
|
if (this.buffer) {
|
|
75
72
|
if (this.offset + fragment.length <= this.buffer.length) {
|
|
76
|
-
this.buffer.set(fragment, this.offset)
|
|
73
|
+
this.buffer.set(fragment, this.offset)
|
|
77
74
|
} else {
|
|
78
|
-
const grown = new Uint8Array(this.offset + fragment.length)
|
|
79
|
-
grown.set(this.buffer.subarray(0, this.offset))
|
|
80
|
-
grown.set(fragment, this.offset)
|
|
81
|
-
this.buffer = grown
|
|
75
|
+
const grown = new Uint8Array(this.offset + fragment.length)
|
|
76
|
+
grown.set(this.buffer.subarray(0, this.offset))
|
|
77
|
+
grown.set(fragment, this.offset)
|
|
78
|
+
this.buffer = grown
|
|
82
79
|
}
|
|
83
|
-
this.offset += fragment.length
|
|
80
|
+
this.offset += fragment.length
|
|
84
81
|
}
|
|
85
82
|
|
|
86
83
|
if (isLast && this.buffer) {
|
|
87
|
-
const result = new TextDecoder().decode(this.buffer.subarray(0, this.offset))
|
|
88
|
-
this.buffer = null
|
|
89
|
-
this.offset = 0
|
|
90
|
-
return result
|
|
84
|
+
const result = new TextDecoder().decode(this.buffer.subarray(0, this.offset))
|
|
85
|
+
this.buffer = null
|
|
86
|
+
this.offset = 0
|
|
87
|
+
return result
|
|
91
88
|
}
|
|
92
89
|
|
|
93
|
-
return null
|
|
90
|
+
return null
|
|
94
91
|
}
|
|
95
92
|
|
|
96
93
|
reset(): void {
|
|
97
|
-
this.buffer = null
|
|
98
|
-
this.offset = 0
|
|
94
|
+
this.buffer = null
|
|
95
|
+
this.offset = 0
|
|
99
96
|
}
|
|
100
97
|
}
|
package/src/ble/index.ts
CHANGED
|
@@ -6,13 +6,13 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
export {
|
|
9
|
-
|
|
10
|
-
Defragmenter,
|
|
11
|
-
DEFAULT_FRAME_PAYLOAD,
|
|
12
|
-
MIN_FRAME_PAYLOAD,
|
|
9
|
+
BLE_NOTIFY_CHAR_UUID,
|
|
13
10
|
BLE_SERVICE_UUID,
|
|
14
11
|
BLE_WRITE_CHAR_UUID,
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
DEFAULT_FRAME_PAYLOAD,
|
|
13
|
+
Defragmenter,
|
|
14
|
+
frameMessage,
|
|
15
|
+
MIN_FRAME_PAYLOAD,
|
|
16
|
+
} from './framing.js'
|
|
17
17
|
|
|
18
|
-
export {
|
|
18
|
+
export { isWebBleSupported, WebBleCentralTransport } from './web-ble-transport.js'
|