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
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* that invalid messages are properly detected.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { describe, expect, it } from 'vitest'
|
|
10
|
-
import {
|
|
9
|
+
import { describe, expect, it } from 'vitest'
|
|
10
|
+
import { b64urlEncode, generateChannelId, generateX25519KeyPair } from '../../crypto.js'
|
|
11
11
|
|
|
12
12
|
// ---------------------------------------------------------------------------
|
|
13
13
|
// Helpers
|
|
@@ -22,71 +22,72 @@ function validEnvelope(overrides: Partial<Record<string, unknown>> = {}): Record
|
|
|
22
22
|
from: b64urlEncode(generateX25519KeyPair().publicKey),
|
|
23
23
|
body: {},
|
|
24
24
|
...overrides,
|
|
25
|
-
}
|
|
25
|
+
}
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
/** Validate that the envelope has all required fields per Section 4.1. */
|
|
29
29
|
function validateEnvelope(msg: Record<string, unknown>): string[] {
|
|
30
|
-
const errors: string[] = []
|
|
31
|
-
if (msg.v !== 1) errors.push('v must be 1')
|
|
32
|
-
if (typeof msg.t !== 'string' || msg.t.length === 0) errors.push('t must be a non-empty string')
|
|
30
|
+
const errors: string[] = []
|
|
31
|
+
if (msg.v !== 1) errors.push('v must be 1')
|
|
32
|
+
if (typeof msg.t !== 'string' || msg.t.length === 0) errors.push('t must be a non-empty string')
|
|
33
33
|
if (typeof msg.ch !== 'string' || !/^[0-9a-f]{64}$/.test(msg.ch as string)) {
|
|
34
|
-
errors.push('ch must be 64 lowercase hex chars')
|
|
34
|
+
errors.push('ch must be 64 lowercase hex chars')
|
|
35
35
|
}
|
|
36
|
-
if (typeof msg.ts !== 'number') errors.push('ts must be a number')
|
|
36
|
+
if (typeof msg.ts !== 'number') errors.push('ts must be a number')
|
|
37
37
|
if (typeof msg.from !== 'string' || (msg.from as string).length === 0) {
|
|
38
|
-
errors.push('from must be a non-empty string')
|
|
38
|
+
errors.push('from must be a non-empty string')
|
|
39
39
|
}
|
|
40
40
|
if (typeof msg.body !== 'object' || msg.body === null || Array.isArray(msg.body)) {
|
|
41
|
-
errors.push('body must be a non-null object')
|
|
41
|
+
errors.push('body must be a non-null object')
|
|
42
42
|
}
|
|
43
|
-
return errors
|
|
43
|
+
return errors
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
/** Validate body schema per Section 4.2. */
|
|
47
47
|
function validateBody(t: string, body: Record<string, unknown>): string[] {
|
|
48
|
-
const errors: string[] = []
|
|
48
|
+
const errors: string[] = []
|
|
49
49
|
switch (t) {
|
|
50
50
|
case 'create':
|
|
51
|
-
if (!body.meta || typeof body.meta !== 'object')
|
|
52
|
-
|
|
51
|
+
if (!body.meta || typeof body.meta !== 'object')
|
|
52
|
+
errors.push('create body requires meta object')
|
|
53
|
+
break
|
|
53
54
|
case 'join':
|
|
54
|
-
if (!('sealed_join' in body)) errors.push('join body requires sealed_join field')
|
|
55
|
-
break
|
|
55
|
+
if (!('sealed_join' in body)) errors.push('join body requires sealed_join field')
|
|
56
|
+
break
|
|
56
57
|
case 'accept':
|
|
57
|
-
if (typeof body.target !== 'string') errors.push('accept body requires target string')
|
|
58
|
-
break
|
|
58
|
+
if (typeof body.target !== 'string') errors.push('accept body requires target string')
|
|
59
|
+
break
|
|
59
60
|
case 'ready':
|
|
60
61
|
for (const field of ['state', 'role', 'self', 'remote', 'reconnect']) {
|
|
61
|
-
if (!(field in body)) errors.push(`ready body requires ${field}`)
|
|
62
|
+
if (!(field in body)) errors.push(`ready body requires ${field}`)
|
|
62
63
|
}
|
|
63
|
-
break
|
|
64
|
+
break
|
|
64
65
|
case 'req':
|
|
65
66
|
case 'res':
|
|
66
67
|
case 'evt':
|
|
67
|
-
if (typeof body.id !== 'string') errors.push(`${t} body requires id string`)
|
|
68
|
-
if (typeof body.sealed !== 'string') errors.push(`${t} body requires sealed string`)
|
|
69
|
-
break
|
|
68
|
+
if (typeof body.id !== 'string') errors.push(`${t} body requires id string`)
|
|
69
|
+
if (typeof body.sealed !== 'string') errors.push(`${t} body requires sealed string`)
|
|
70
|
+
break
|
|
70
71
|
case 'ping':
|
|
71
72
|
case 'pong':
|
|
72
73
|
// empty body is valid
|
|
73
|
-
break
|
|
74
|
+
break
|
|
74
75
|
case 'close':
|
|
75
|
-
if (typeof body.reason !== 'string') errors.push('close body requires reason string')
|
|
76
|
-
break
|
|
76
|
+
if (typeof body.reason !== 'string') errors.push('close body requires reason string')
|
|
77
|
+
break
|
|
77
78
|
case 'terminate':
|
|
78
|
-
if (typeof body.reason !== 'string') errors.push('terminate body requires reason string')
|
|
79
|
-
break
|
|
79
|
+
if (typeof body.reason !== 'string') errors.push('terminate body requires reason string')
|
|
80
|
+
break
|
|
80
81
|
default:
|
|
81
|
-
errors.push(`unknown message type: ${t}`)
|
|
82
|
+
errors.push(`unknown message type: ${t}`)
|
|
82
83
|
}
|
|
83
|
-
return errors
|
|
84
|
+
return errors
|
|
84
85
|
}
|
|
85
86
|
|
|
86
87
|
/** Check wire size limit per Section 15 rule 10. */
|
|
87
88
|
function checkSizeLimit(msg: Record<string, unknown>): boolean {
|
|
88
|
-
const wire = JSON.stringify(msg)
|
|
89
|
-
return new TextEncoder().encode(wire).length <= 64 * 1024
|
|
89
|
+
const wire = JSON.stringify(msg)
|
|
90
|
+
return new TextEncoder().encode(wire).length <= 64 * 1024
|
|
90
91
|
}
|
|
91
92
|
|
|
92
93
|
// ---------------------------------------------------------------------------
|
|
@@ -95,50 +96,66 @@ function checkSizeLimit(msg: Record<string, unknown>): boolean {
|
|
|
95
96
|
|
|
96
97
|
describe('Section 4.1 — Envelope required fields', () => {
|
|
97
98
|
it('valid envelope passes validation', () => {
|
|
98
|
-
expect(validateEnvelope(validEnvelope())).toEqual([])
|
|
99
|
-
})
|
|
99
|
+
expect(validateEnvelope(validEnvelope())).toEqual([])
|
|
100
|
+
})
|
|
100
101
|
|
|
101
102
|
it('v must be 1', () => {
|
|
102
|
-
const errors = validateEnvelope(validEnvelope({ v: 2 }))
|
|
103
|
-
expect(errors).toContain('v must be 1')
|
|
104
|
-
})
|
|
103
|
+
const errors = validateEnvelope(validEnvelope({ v: 2 }))
|
|
104
|
+
expect(errors).toContain('v must be 1')
|
|
105
|
+
})
|
|
105
106
|
|
|
106
107
|
it('v=0 is rejected', () => {
|
|
107
|
-
const errors = validateEnvelope(validEnvelope({ v: 0 }))
|
|
108
|
-
expect(errors).toContain('v must be 1')
|
|
109
|
-
})
|
|
108
|
+
const errors = validateEnvelope(validEnvelope({ v: 0 }))
|
|
109
|
+
expect(errors).toContain('v must be 1')
|
|
110
|
+
})
|
|
110
111
|
|
|
111
112
|
it('t must be a non-empty string', () => {
|
|
112
|
-
expect(validateEnvelope(validEnvelope({ t: '' }))).toContain('t must be a non-empty string')
|
|
113
|
-
expect(validateEnvelope(validEnvelope({ t: 123 }))).toContain('t must be a non-empty string')
|
|
114
|
-
})
|
|
113
|
+
expect(validateEnvelope(validEnvelope({ t: '' }))).toContain('t must be a non-empty string')
|
|
114
|
+
expect(validateEnvelope(validEnvelope({ t: 123 }))).toContain('t must be a non-empty string')
|
|
115
|
+
})
|
|
115
116
|
|
|
116
117
|
it('ch must be 64 lowercase hex chars', () => {
|
|
117
|
-
expect(validateEnvelope(validEnvelope({ ch: 'ABC' }))).toContain(
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
118
|
+
expect(validateEnvelope(validEnvelope({ ch: 'ABC' }))).toContain(
|
|
119
|
+
'ch must be 64 lowercase hex chars',
|
|
120
|
+
)
|
|
121
|
+
expect(validateEnvelope(validEnvelope({ ch: 'zz'.repeat(32) }))).toContain(
|
|
122
|
+
'ch must be 64 lowercase hex chars',
|
|
123
|
+
)
|
|
124
|
+
expect(validateEnvelope(validEnvelope({ ch: 'aa'.repeat(31) }))).toContain(
|
|
125
|
+
'ch must be 64 lowercase hex chars',
|
|
126
|
+
)
|
|
127
|
+
})
|
|
121
128
|
|
|
122
129
|
it('valid 64-hex-char ch passes', () => {
|
|
123
|
-
const msg = validEnvelope({ ch: 'ab'.repeat(32) })
|
|
124
|
-
expect(validateEnvelope(msg)).toEqual([])
|
|
125
|
-
})
|
|
130
|
+
const msg = validEnvelope({ ch: 'ab'.repeat(32) })
|
|
131
|
+
expect(validateEnvelope(msg)).toEqual([])
|
|
132
|
+
})
|
|
126
133
|
|
|
127
134
|
it('ts must be a number', () => {
|
|
128
|
-
expect(validateEnvelope(validEnvelope({ ts: 'not a number' }))).toContain('ts must be a number')
|
|
129
|
-
})
|
|
135
|
+
expect(validateEnvelope(validEnvelope({ ts: 'not a number' }))).toContain('ts must be a number')
|
|
136
|
+
})
|
|
130
137
|
|
|
131
138
|
it('from must be a non-empty string', () => {
|
|
132
|
-
expect(validateEnvelope(validEnvelope({ from: '' }))).toContain(
|
|
133
|
-
|
|
134
|
-
|
|
139
|
+
expect(validateEnvelope(validEnvelope({ from: '' }))).toContain(
|
|
140
|
+
'from must be a non-empty string',
|
|
141
|
+
)
|
|
142
|
+
expect(validateEnvelope(validEnvelope({ from: 123 }))).toContain(
|
|
143
|
+
'from must be a non-empty string',
|
|
144
|
+
)
|
|
145
|
+
})
|
|
135
146
|
|
|
136
147
|
it('body must be a non-null object', () => {
|
|
137
|
-
expect(validateEnvelope(validEnvelope({ body: null }))).toContain(
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
148
|
+
expect(validateEnvelope(validEnvelope({ body: null }))).toContain(
|
|
149
|
+
'body must be a non-null object',
|
|
150
|
+
)
|
|
151
|
+
expect(validateEnvelope(validEnvelope({ body: [1, 2] }))).toContain(
|
|
152
|
+
'body must be a non-null object',
|
|
153
|
+
)
|
|
154
|
+
expect(validateEnvelope(validEnvelope({ body: 'string' }))).toContain(
|
|
155
|
+
'body must be a non-null object',
|
|
156
|
+
)
|
|
157
|
+
})
|
|
158
|
+
})
|
|
142
159
|
|
|
143
160
|
// ---------------------------------------------------------------------------
|
|
144
161
|
// Channel ID validation (Section 3)
|
|
@@ -146,30 +163,30 @@ describe('Section 4.1 — Envelope required fields', () => {
|
|
|
146
163
|
|
|
147
164
|
describe('Section 3 — Channel ID validation', () => {
|
|
148
165
|
it('valid channel ID: 64 lowercase hex chars', () => {
|
|
149
|
-
const ch = generateChannelId()
|
|
150
|
-
expect(ch).toMatch(/^[0-9a-f]{64}$/)
|
|
151
|
-
})
|
|
166
|
+
const ch = generateChannelId()
|
|
167
|
+
expect(ch).toMatch(/^[0-9a-f]{64}$/)
|
|
168
|
+
})
|
|
152
169
|
|
|
153
170
|
it('rejects uppercase hex', () => {
|
|
154
|
-
const ch = 'AB'.repeat(32)
|
|
155
|
-
expect(/^[0-9a-f]{64}$/.test(ch)).toBe(false)
|
|
156
|
-
})
|
|
171
|
+
const ch = 'AB'.repeat(32)
|
|
172
|
+
expect(/^[0-9a-f]{64}$/.test(ch)).toBe(false)
|
|
173
|
+
})
|
|
157
174
|
|
|
158
175
|
it('rejects 63 chars', () => {
|
|
159
|
-
const ch = 'a'.repeat(63)
|
|
160
|
-
expect(/^[0-9a-f]{64}$/.test(ch)).toBe(false)
|
|
161
|
-
})
|
|
176
|
+
const ch = 'a'.repeat(63)
|
|
177
|
+
expect(/^[0-9a-f]{64}$/.test(ch)).toBe(false)
|
|
178
|
+
})
|
|
162
179
|
|
|
163
180
|
it('rejects 65 chars', () => {
|
|
164
|
-
const ch = 'a'.repeat(65)
|
|
165
|
-
expect(/^[0-9a-f]{64}$/.test(ch)).toBe(false)
|
|
166
|
-
})
|
|
181
|
+
const ch = 'a'.repeat(65)
|
|
182
|
+
expect(/^[0-9a-f]{64}$/.test(ch)).toBe(false)
|
|
183
|
+
})
|
|
167
184
|
|
|
168
185
|
it('rejects non-hex chars', () => {
|
|
169
|
-
const ch = 'g'.repeat(64)
|
|
170
|
-
expect(/^[0-9a-f]{64}$/.test(ch)).toBe(false)
|
|
171
|
-
})
|
|
172
|
-
})
|
|
186
|
+
const ch = 'g'.repeat(64)
|
|
187
|
+
expect(/^[0-9a-f]{64}$/.test(ch)).toBe(false)
|
|
188
|
+
})
|
|
189
|
+
})
|
|
173
190
|
|
|
174
191
|
// ---------------------------------------------------------------------------
|
|
175
192
|
// Body schema validation (Section 4.2)
|
|
@@ -177,56 +194,58 @@ describe('Section 3 — Channel ID validation', () => {
|
|
|
177
194
|
|
|
178
195
|
describe('Section 4.2 — Body schemas', () => {
|
|
179
196
|
it('create: requires meta object', () => {
|
|
180
|
-
expect(
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
197
|
+
expect(
|
|
198
|
+
validateBody('create', { meta: { name: 'X', description: 'Y', url: 'Z', icon: 'W' } }),
|
|
199
|
+
).toEqual([])
|
|
200
|
+
expect(validateBody('create', {})).toContain('create body requires meta object')
|
|
201
|
+
expect(validateBody('create', { meta: null })).toContain('create body requires meta object')
|
|
202
|
+
})
|
|
184
203
|
|
|
185
204
|
it('join: requires sealed_join field (may be null for reconnect)', () => {
|
|
186
|
-
expect(validateBody('join', { sealed_join: 'abc123' })).toEqual([])
|
|
187
|
-
expect(validateBody('join', { sealed_join: null })).toEqual([])
|
|
188
|
-
expect(validateBody('join', {})).toContain('join body requires sealed_join field')
|
|
189
|
-
})
|
|
205
|
+
expect(validateBody('join', { sealed_join: 'abc123' })).toEqual([])
|
|
206
|
+
expect(validateBody('join', { sealed_join: null })).toEqual([])
|
|
207
|
+
expect(validateBody('join', {})).toContain('join body requires sealed_join field')
|
|
208
|
+
})
|
|
190
209
|
|
|
191
210
|
it('accept: requires target string', () => {
|
|
192
|
-
expect(validateBody('accept', { target: 'pubkey_b64' })).toEqual([])
|
|
193
|
-
expect(validateBody('accept', {})).toContain('accept body requires target string')
|
|
194
|
-
expect(validateBody('accept', { target: 123 })).toContain('accept body requires target string')
|
|
195
|
-
})
|
|
211
|
+
expect(validateBody('accept', { target: 'pubkey_b64' })).toEqual([])
|
|
212
|
+
expect(validateBody('accept', {})).toContain('accept body requires target string')
|
|
213
|
+
expect(validateBody('accept', { target: 123 })).toContain('accept body requires target string')
|
|
214
|
+
})
|
|
196
215
|
|
|
197
216
|
it('ready: requires state, role, self, remote, reconnect', () => {
|
|
198
|
-
const valid = { state: 'connected', role: 'dapp', self: 'pk1', remote: 'pk2', reconnect: false }
|
|
199
|
-
expect(validateBody('ready', valid)).toEqual([])
|
|
217
|
+
const valid = { state: 'connected', role: 'dapp', self: 'pk1', remote: 'pk2', reconnect: false }
|
|
218
|
+
expect(validateBody('ready', valid)).toEqual([])
|
|
200
219
|
for (const field of ['state', 'role', 'self', 'remote', 'reconnect']) {
|
|
201
|
-
const missing = { ...valid }
|
|
202
|
-
delete (missing as
|
|
203
|
-
expect(validateBody('ready', missing)).toContain(`ready body requires ${field}`)
|
|
220
|
+
const missing = { ...valid }
|
|
221
|
+
delete (missing as Record<string, unknown>)[field]
|
|
222
|
+
expect(validateBody('ready', missing)).toContain(`ready body requires ${field}`)
|
|
204
223
|
}
|
|
205
|
-
})
|
|
224
|
+
})
|
|
206
225
|
|
|
207
226
|
it('req/res/evt: requires id and sealed strings', () => {
|
|
208
227
|
for (const t of ['req', 'res', 'evt']) {
|
|
209
|
-
expect(validateBody(t, { id: 'uuid', sealed: 'base64data' })).toEqual([])
|
|
210
|
-
expect(validateBody(t, { sealed: 'base64data' })).toContain(`${t} body requires id string`)
|
|
211
|
-
expect(validateBody(t, { id: 'uuid' })).toContain(`${t} body requires sealed string`)
|
|
228
|
+
expect(validateBody(t, { id: 'uuid', sealed: 'base64data' })).toEqual([])
|
|
229
|
+
expect(validateBody(t, { sealed: 'base64data' })).toContain(`${t} body requires id string`)
|
|
230
|
+
expect(validateBody(t, { id: 'uuid' })).toContain(`${t} body requires sealed string`)
|
|
212
231
|
}
|
|
213
|
-
})
|
|
232
|
+
})
|
|
214
233
|
|
|
215
234
|
it('ping/pong: empty body is valid', () => {
|
|
216
|
-
expect(validateBody('ping', {})).toEqual([])
|
|
217
|
-
expect(validateBody('pong', {})).toEqual([])
|
|
218
|
-
})
|
|
235
|
+
expect(validateBody('ping', {})).toEqual([])
|
|
236
|
+
expect(validateBody('pong', {})).toEqual([])
|
|
237
|
+
})
|
|
219
238
|
|
|
220
239
|
it('close: requires reason string', () => {
|
|
221
|
-
expect(validateBody('close', { reason: 'normal' })).toEqual([])
|
|
222
|
-
expect(validateBody('close', {})).toContain('close body requires reason string')
|
|
223
|
-
})
|
|
240
|
+
expect(validateBody('close', { reason: 'normal' })).toEqual([])
|
|
241
|
+
expect(validateBody('close', {})).toContain('close body requires reason string')
|
|
242
|
+
})
|
|
224
243
|
|
|
225
244
|
it('terminate: requires reason string', () => {
|
|
226
|
-
expect(validateBody('terminate', { reason: 'timeout' })).toEqual([])
|
|
227
|
-
expect(validateBody('terminate', {})).toContain('terminate body requires reason string')
|
|
228
|
-
})
|
|
229
|
-
})
|
|
245
|
+
expect(validateBody('terminate', { reason: 'timeout' })).toEqual([])
|
|
246
|
+
expect(validateBody('terminate', {})).toContain('terminate body requires reason string')
|
|
247
|
+
})
|
|
248
|
+
})
|
|
230
249
|
|
|
231
250
|
// ---------------------------------------------------------------------------
|
|
232
251
|
// from = "_adapter" rejection (Section 2)
|
|
@@ -235,39 +254,49 @@ describe('Section 4.2 — Body schemas', () => {
|
|
|
235
254
|
describe('Section 2 — _adapter from rejection', () => {
|
|
236
255
|
it('"_adapter" is reserved for adapter-sent messages only', () => {
|
|
237
256
|
// Peer message types that MUST NOT have from = "_adapter"
|
|
238
|
-
const peerTypes = ['create', 'join', 'accept', 'req', 'res', 'evt', 'ping', 'pong', 'close']
|
|
257
|
+
const peerTypes = ['create', 'join', 'accept', 'req', 'res', 'evt', 'ping', 'pong', 'close']
|
|
239
258
|
for (const t of peerTypes) {
|
|
240
|
-
const msg = validEnvelope({ t, from: '_adapter' })
|
|
259
|
+
const msg = validEnvelope({ t, from: '_adapter' })
|
|
241
260
|
// Peers MUST reject any peer-sent message where from = "_adapter"
|
|
242
|
-
expect(msg.from).toBe('_adapter')
|
|
261
|
+
expect(msg.from).toBe('_adapter')
|
|
243
262
|
// Validation: from="_adapter" is only valid for adapter-sent types
|
|
244
|
-
const isAdapterType = t === 'ready' || t === 'terminate'
|
|
245
|
-
expect(isAdapterType).toBe(false)
|
|
263
|
+
const isAdapterType = t === 'ready' || t === 'terminate'
|
|
264
|
+
expect(isAdapterType).toBe(false)
|
|
246
265
|
}
|
|
247
|
-
})
|
|
266
|
+
})
|
|
248
267
|
|
|
249
268
|
it('"_adapter" is valid for ready and terminate messages', () => {
|
|
250
|
-
const adapterTypes = ['ready', 'terminate']
|
|
269
|
+
const adapterTypes = ['ready', 'terminate']
|
|
251
270
|
for (const t of adapterTypes) {
|
|
252
|
-
const isAdapterType = t === 'ready' || t === 'terminate'
|
|
253
|
-
expect(isAdapterType).toBe(true)
|
|
271
|
+
const isAdapterType = t === 'ready' || t === 'terminate'
|
|
272
|
+
expect(isAdapterType).toBe(true)
|
|
254
273
|
}
|
|
255
|
-
})
|
|
274
|
+
})
|
|
256
275
|
|
|
257
276
|
it('from = "_adapter" MUST be rejected for peer message types', () => {
|
|
258
277
|
// This test documents the rule: implementations MUST reject peer messages
|
|
259
278
|
// with from = "_adapter" to prevent adapter impersonation
|
|
260
279
|
function isPeerMessageWithAdapterFrom(msg: { t: string; from: string }): boolean {
|
|
261
|
-
const peerTypes = new Set([
|
|
262
|
-
|
|
280
|
+
const peerTypes = new Set([
|
|
281
|
+
'create',
|
|
282
|
+
'join',
|
|
283
|
+
'accept',
|
|
284
|
+
'req',
|
|
285
|
+
'res',
|
|
286
|
+
'evt',
|
|
287
|
+
'ping',
|
|
288
|
+
'pong',
|
|
289
|
+
'close',
|
|
290
|
+
])
|
|
291
|
+
return peerTypes.has(msg.t) && msg.from === '_adapter'
|
|
263
292
|
}
|
|
264
293
|
|
|
265
|
-
expect(isPeerMessageWithAdapterFrom({ t: 'req', from: '_adapter' })).toBe(true)
|
|
266
|
-
expect(isPeerMessageWithAdapterFrom({ t: 'req', from: 'some_pubkey' })).toBe(false)
|
|
267
|
-
expect(isPeerMessageWithAdapterFrom({ t: 'ready', from: '_adapter' })).toBe(false)
|
|
268
|
-
expect(isPeerMessageWithAdapterFrom({ t: 'terminate', from: '_adapter' })).toBe(false)
|
|
269
|
-
})
|
|
270
|
-
})
|
|
294
|
+
expect(isPeerMessageWithAdapterFrom({ t: 'req', from: '_adapter' })).toBe(true)
|
|
295
|
+
expect(isPeerMessageWithAdapterFrom({ t: 'req', from: 'some_pubkey' })).toBe(false)
|
|
296
|
+
expect(isPeerMessageWithAdapterFrom({ t: 'ready', from: '_adapter' })).toBe(false)
|
|
297
|
+
expect(isPeerMessageWithAdapterFrom({ t: 'terminate', from: '_adapter' })).toBe(false)
|
|
298
|
+
})
|
|
299
|
+
})
|
|
271
300
|
|
|
272
301
|
// ---------------------------------------------------------------------------
|
|
273
302
|
// Message size limit (Section 15 rule 10)
|
|
@@ -275,26 +304,26 @@ describe('Section 2 — _adapter from rejection', () => {
|
|
|
275
304
|
|
|
276
305
|
describe('Section 15 rule 10 — Message size limit (64 KB)', () => {
|
|
277
306
|
it('a normal message is within the limit', () => {
|
|
278
|
-
const msg = validEnvelope()
|
|
279
|
-
expect(checkSizeLimit(msg)).toBe(true)
|
|
280
|
-
})
|
|
307
|
+
const msg = validEnvelope()
|
|
308
|
+
expect(checkSizeLimit(msg)).toBe(true)
|
|
309
|
+
})
|
|
281
310
|
|
|
282
311
|
it('a message exceeding 64 KB is rejected', () => {
|
|
283
312
|
const msg = validEnvelope({
|
|
284
313
|
body: { sealed: 'x'.repeat(70000) },
|
|
285
|
-
})
|
|
286
|
-
expect(checkSizeLimit(msg)).toBe(false)
|
|
287
|
-
})
|
|
314
|
+
})
|
|
315
|
+
expect(checkSizeLimit(msg)).toBe(false)
|
|
316
|
+
})
|
|
288
317
|
|
|
289
318
|
it('exactly 64 KB is within the limit', () => {
|
|
290
319
|
// Build a message that is exactly at the boundary
|
|
291
|
-
const base = validEnvelope({ body: { sealed: '' } })
|
|
292
|
-
const baseSize = new TextEncoder().encode(JSON.stringify(base)).length
|
|
293
|
-
const remaining = 64 * 1024 - baseSize
|
|
294
|
-
const msg = validEnvelope({ body: { sealed: 'a'.repeat(remaining) } })
|
|
295
|
-
expect(checkSizeLimit(msg)).toBe(true)
|
|
296
|
-
})
|
|
297
|
-
})
|
|
320
|
+
const base = validEnvelope({ body: { sealed: '' } })
|
|
321
|
+
const baseSize = new TextEncoder().encode(JSON.stringify(base)).length
|
|
322
|
+
const remaining = 64 * 1024 - baseSize
|
|
323
|
+
const msg = validEnvelope({ body: { sealed: 'a'.repeat(remaining) } })
|
|
324
|
+
expect(checkSizeLimit(msg)).toBe(true)
|
|
325
|
+
})
|
|
326
|
+
})
|
|
298
327
|
|
|
299
328
|
// ---------------------------------------------------------------------------
|
|
300
329
|
// Close / terminate reasons (Section 12.3)
|
|
@@ -313,7 +342,7 @@ describe('Section 12.3 — Close and terminate reasons', () => {
|
|
|
313
342
|
'rate_limited',
|
|
314
343
|
'payload_too_large',
|
|
315
344
|
'protocol_error',
|
|
316
|
-
]
|
|
345
|
+
]
|
|
317
346
|
|
|
318
347
|
const adapterReasons = [
|
|
319
348
|
'channel_not_found',
|
|
@@ -325,21 +354,21 @@ describe('Section 12.3 — Close and terminate reasons', () => {
|
|
|
325
354
|
'rate_limited',
|
|
326
355
|
'payload_too_large',
|
|
327
356
|
'protocol_error',
|
|
328
|
-
]
|
|
357
|
+
]
|
|
329
358
|
|
|
330
359
|
it('all peer close reasons are valid strings', () => {
|
|
331
360
|
for (const reason of peerReasons) {
|
|
332
|
-
expect(typeof reason).toBe('string')
|
|
333
|
-
expect(reason.length).toBeGreaterThan(0)
|
|
361
|
+
expect(typeof reason).toBe('string')
|
|
362
|
+
expect(reason.length).toBeGreaterThan(0)
|
|
334
363
|
}
|
|
335
|
-
})
|
|
364
|
+
})
|
|
336
365
|
|
|
337
366
|
it('all adapter terminate reasons are valid strings', () => {
|
|
338
367
|
for (const reason of adapterReasons) {
|
|
339
|
-
expect(typeof reason).toBe('string')
|
|
340
|
-
expect(reason.length).toBeGreaterThan(0)
|
|
368
|
+
expect(typeof reason).toBe('string')
|
|
369
|
+
expect(reason.length).toBeGreaterThan(0)
|
|
341
370
|
}
|
|
342
|
-
})
|
|
371
|
+
})
|
|
343
372
|
|
|
344
373
|
it('terminate is adapter-only (from = "_adapter")', () => {
|
|
345
374
|
// Section 12.2: Only the adapter sends terminate. Peers MUST NOT send it.
|
|
@@ -350,7 +379,7 @@ describe('Section 12.3 — Close and terminate reasons', () => {
|
|
|
350
379
|
ts: Date.now(),
|
|
351
380
|
from: '_adapter',
|
|
352
381
|
body: { reason: 'timeout' },
|
|
353
|
-
}
|
|
354
|
-
expect(validTerminate.from).toBe('_adapter')
|
|
355
|
-
})
|
|
356
|
-
})
|
|
382
|
+
}
|
|
383
|
+
expect(validTerminate.from).toBe('_adapter')
|
|
384
|
+
})
|
|
385
|
+
})
|