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.
- 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 +1 -1
- package/dist/ws-transport.d.ts.map +1 -1
- package/dist/ws-transport.js +12 -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 +51 -41
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
* H. Cross-implementation compatibility vectors
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import { describe,
|
|
19
|
-
import { canonicalJson, sha256Hex } from './crypto.js'
|
|
18
|
+
import { describe, expect, it } from 'vitest'
|
|
19
|
+
import { canonicalJson, sha256Hex } from './crypto.js'
|
|
20
20
|
|
|
21
21
|
// ═══════════════════════════════════════════════════════════════════════
|
|
22
22
|
// A. Key sorting
|
|
@@ -24,59 +24,59 @@ import { canonicalJson, sha256Hex } from './crypto.js';
|
|
|
24
24
|
|
|
25
25
|
describe('canonicalJson — key sorting', () => {
|
|
26
26
|
it('sorts ASCII keys lexicographically', () => {
|
|
27
|
-
expect(canonicalJson({ z: 1, a: 2, m: 3 })).toBe('{"a":2,"m":3,"z":1}')
|
|
28
|
-
})
|
|
27
|
+
expect(canonicalJson({ z: 1, a: 2, m: 3 })).toBe('{"a":2,"m":3,"z":1}')
|
|
28
|
+
})
|
|
29
29
|
|
|
30
30
|
it('uppercase sorts before lowercase (UTF-16 order)', () => {
|
|
31
31
|
// 'A' = 0x41, 'a' = 0x61 → 'A' < 'a'
|
|
32
|
-
expect(canonicalJson({ a: 1, A: 2 })).toBe('{"A":2,"a":1}')
|
|
33
|
-
})
|
|
32
|
+
expect(canonicalJson({ a: 1, A: 2 })).toBe('{"A":2,"a":1}')
|
|
33
|
+
})
|
|
34
34
|
|
|
35
35
|
it('digits sort before letters', () => {
|
|
36
36
|
// '0' = 0x30, 'A' = 0x41
|
|
37
|
-
expect(canonicalJson({ a: 1, '0': 2 })).toBe('{"0":2,"a":1}')
|
|
38
|
-
})
|
|
37
|
+
expect(canonicalJson({ a: 1, '0': 2 })).toBe('{"0":2,"a":1}')
|
|
38
|
+
})
|
|
39
39
|
|
|
40
40
|
it('empty string key sorts first', () => {
|
|
41
|
-
expect(canonicalJson({ b: 2, '': 0, a: 1 })).toBe('{"":0,"a":1,"b":2}')
|
|
42
|
-
})
|
|
41
|
+
expect(canonicalJson({ b: 2, '': 0, a: 1 })).toBe('{"":0,"a":1,"b":2}')
|
|
42
|
+
})
|
|
43
43
|
|
|
44
44
|
it('underscore sorts between uppercase Z and lowercase a', () => {
|
|
45
45
|
// '_' = 0x5F, 'Z' = 0x5A, 'a' = 0x61
|
|
46
|
-
expect(canonicalJson({ a: 1, _: 2, Z: 3 })).toBe('{"Z":3,"_":2,"a":1}')
|
|
47
|
-
})
|
|
46
|
+
expect(canonicalJson({ a: 1, _: 2, Z: 3 })).toBe('{"Z":3,"_":2,"a":1}')
|
|
47
|
+
})
|
|
48
48
|
|
|
49
49
|
it('numeric string keys sort lexicographically not numerically', () => {
|
|
50
50
|
// "10" < "2" lexicographically because '1' < '2'
|
|
51
|
-
expect(canonicalJson({ '2': 'b', '10': 'a', '1': 'c' })).toBe('{"1":"c","10":"a","2":"b"}')
|
|
52
|
-
})
|
|
51
|
+
expect(canonicalJson({ '2': 'b', '10': 'a', '1': 'c' })).toBe('{"1":"c","10":"a","2":"b"}')
|
|
52
|
+
})
|
|
53
53
|
|
|
54
54
|
it('sorts keys with common prefixes correctly', () => {
|
|
55
|
-
expect(canonicalJson({ ab: 1, abc: 2, a: 3 })).toBe('{"a":3,"ab":1,"abc":2}')
|
|
56
|
-
})
|
|
55
|
+
expect(canonicalJson({ ab: 1, abc: 2, a: 3 })).toBe('{"a":3,"ab":1,"abc":2}')
|
|
56
|
+
})
|
|
57
57
|
|
|
58
58
|
it('handles single-key object', () => {
|
|
59
|
-
expect(canonicalJson({ x: 1 })).toBe('{"x":1}')
|
|
60
|
-
})
|
|
59
|
+
expect(canonicalJson({ x: 1 })).toBe('{"x":1}')
|
|
60
|
+
})
|
|
61
61
|
|
|
62
62
|
it('already-sorted keys produce same output', () => {
|
|
63
|
-
expect(canonicalJson({ a: 1, b: 2, c: 3 })).toBe('{"a":1,"b":2,"c":3}')
|
|
64
|
-
})
|
|
63
|
+
expect(canonicalJson({ a: 1, b: 2, c: 3 })).toBe('{"a":1,"b":2,"c":3}')
|
|
64
|
+
})
|
|
65
65
|
|
|
66
66
|
it('reverse-sorted keys are reordered', () => {
|
|
67
|
-
expect(canonicalJson({ c: 3, b: 2, a: 1 })).toBe('{"a":1,"b":2,"c":3}')
|
|
68
|
-
})
|
|
67
|
+
expect(canonicalJson({ c: 3, b: 2, a: 1 })).toBe('{"a":1,"b":2,"c":3}')
|
|
68
|
+
})
|
|
69
69
|
|
|
70
70
|
it('sorts unicode keys by UTF-16 code unit order', () => {
|
|
71
71
|
// For BMP characters, UTF-16 and UTF-8 sort order match for ASCII
|
|
72
72
|
// But for non-ASCII: é (U+00E9) vs ê (U+00EA) → é < ê
|
|
73
|
-
expect(canonicalJson({
|
|
74
|
-
})
|
|
73
|
+
expect(canonicalJson({ ê: 2, é: 1 })).toBe('{"é":1,"ê":2}')
|
|
74
|
+
})
|
|
75
75
|
|
|
76
76
|
it('handles keys with special JSON characters', () => {
|
|
77
|
-
expect(canonicalJson({ 'a"b': 1, 'a\\c': 2 })).toBe('{"a\\"b":1,"a\\\\c":2}')
|
|
78
|
-
})
|
|
79
|
-
})
|
|
77
|
+
expect(canonicalJson({ 'a"b': 1, 'a\\c': 2 })).toBe('{"a\\"b":1,"a\\\\c":2}')
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
80
|
|
|
81
81
|
// ═══════════════════════════════════════════════════════════════════════
|
|
82
82
|
// B. Value types
|
|
@@ -84,51 +84,51 @@ describe('canonicalJson — key sorting', () => {
|
|
|
84
84
|
|
|
85
85
|
describe('canonicalJson — value types', () => {
|
|
86
86
|
it('null → "null"', () => {
|
|
87
|
-
expect(canonicalJson(null)).toBe('null')
|
|
88
|
-
})
|
|
87
|
+
expect(canonicalJson(null)).toBe('null')
|
|
88
|
+
})
|
|
89
89
|
|
|
90
90
|
it('true → "true"', () => {
|
|
91
|
-
expect(canonicalJson(true)).toBe('true')
|
|
92
|
-
})
|
|
91
|
+
expect(canonicalJson(true)).toBe('true')
|
|
92
|
+
})
|
|
93
93
|
|
|
94
94
|
it('false → "false"', () => {
|
|
95
|
-
expect(canonicalJson(false)).toBe('false')
|
|
96
|
-
})
|
|
95
|
+
expect(canonicalJson(false)).toBe('false')
|
|
96
|
+
})
|
|
97
97
|
|
|
98
98
|
it('integer → decimal string', () => {
|
|
99
|
-
expect(canonicalJson(42)).toBe('42')
|
|
100
|
-
expect(canonicalJson(0)).toBe('0')
|
|
101
|
-
expect(canonicalJson(-1)).toBe('-1')
|
|
102
|
-
})
|
|
99
|
+
expect(canonicalJson(42)).toBe('42')
|
|
100
|
+
expect(canonicalJson(0)).toBe('0')
|
|
101
|
+
expect(canonicalJson(-1)).toBe('-1')
|
|
102
|
+
})
|
|
103
103
|
|
|
104
104
|
it('string → quoted string', () => {
|
|
105
|
-
expect(canonicalJson('hello')).toBe('"hello"')
|
|
106
|
-
})
|
|
105
|
+
expect(canonicalJson('hello')).toBe('"hello"')
|
|
106
|
+
})
|
|
107
107
|
|
|
108
108
|
it('empty string → \'""\'', () => {
|
|
109
|
-
expect(canonicalJson('')).toBe('""')
|
|
110
|
-
})
|
|
109
|
+
expect(canonicalJson('')).toBe('""')
|
|
110
|
+
})
|
|
111
111
|
|
|
112
112
|
it('array → ordered elements', () => {
|
|
113
|
-
expect(canonicalJson([1, 'two', null, true])).toBe('[1,"two",null,true]')
|
|
114
|
-
})
|
|
113
|
+
expect(canonicalJson([1, 'two', null, true])).toBe('[1,"two",null,true]')
|
|
114
|
+
})
|
|
115
115
|
|
|
116
116
|
it('empty array → "[]"', () => {
|
|
117
|
-
expect(canonicalJson([])).toBe('[]')
|
|
118
|
-
})
|
|
117
|
+
expect(canonicalJson([])).toBe('[]')
|
|
118
|
+
})
|
|
119
119
|
|
|
120
120
|
it('empty object → "{}"', () => {
|
|
121
|
-
expect(canonicalJson({})).toBe('{}')
|
|
122
|
-
})
|
|
121
|
+
expect(canonicalJson({})).toBe('{}')
|
|
122
|
+
})
|
|
123
123
|
|
|
124
124
|
it('nested null in object', () => {
|
|
125
|
-
expect(canonicalJson({ a: null })).toBe('{"a":null}')
|
|
126
|
-
})
|
|
125
|
+
expect(canonicalJson({ a: null })).toBe('{"a":null}')
|
|
126
|
+
})
|
|
127
127
|
|
|
128
128
|
it('nested null in array', () => {
|
|
129
|
-
expect(canonicalJson([null, null])).toBe('[null,null]')
|
|
130
|
-
})
|
|
131
|
-
})
|
|
129
|
+
expect(canonicalJson([null, null])).toBe('[null,null]')
|
|
130
|
+
})
|
|
131
|
+
})
|
|
132
132
|
|
|
133
133
|
// ═══════════════════════════════════════════════════════════════════════
|
|
134
134
|
// C. Number edge cases
|
|
@@ -136,48 +136,48 @@ describe('canonicalJson — value types', () => {
|
|
|
136
136
|
|
|
137
137
|
describe('canonicalJson — numbers', () => {
|
|
138
138
|
it('negative zero → "0"', () => {
|
|
139
|
-
expect(canonicalJson(-0)).toBe('0')
|
|
139
|
+
expect(canonicalJson(-0)).toBe('0')
|
|
140
140
|
// Also in object context
|
|
141
|
-
expect(canonicalJson({ v: -0 })).toBe('{"v":0}')
|
|
142
|
-
})
|
|
141
|
+
expect(canonicalJson({ v: -0 })).toBe('{"v":0}')
|
|
142
|
+
})
|
|
143
143
|
|
|
144
144
|
it('floats use shortest representation', () => {
|
|
145
|
-
expect(canonicalJson(1.5)).toBe('1.5')
|
|
146
|
-
expect(canonicalJson(0.1)).toBe('0.1')
|
|
147
|
-
expect(canonicalJson(100.0)).toBe('100')
|
|
148
|
-
})
|
|
145
|
+
expect(canonicalJson(1.5)).toBe('1.5')
|
|
146
|
+
expect(canonicalJson(0.1)).toBe('0.1')
|
|
147
|
+
expect(canonicalJson(100.0)).toBe('100') // no trailing .0
|
|
148
|
+
})
|
|
149
149
|
|
|
150
150
|
it('large integers', () => {
|
|
151
|
-
expect(canonicalJson(1000000)).toBe('1000000')
|
|
152
|
-
expect(canonicalJson(Number.MAX_SAFE_INTEGER)).toBe('9007199254740991')
|
|
153
|
-
})
|
|
151
|
+
expect(canonicalJson(1000000)).toBe('1000000')
|
|
152
|
+
expect(canonicalJson(Number.MAX_SAFE_INTEGER)).toBe('9007199254740991')
|
|
153
|
+
})
|
|
154
154
|
|
|
155
155
|
it('very small floats', () => {
|
|
156
156
|
// JS serializes 5e-7 as "5e-7" and 0.000001 as "0.000001"
|
|
157
|
-
expect(canonicalJson(0.000001)).toBe('0.000001')
|
|
158
|
-
})
|
|
157
|
+
expect(canonicalJson(0.000001)).toBe('0.000001')
|
|
158
|
+
})
|
|
159
159
|
|
|
160
160
|
it('scientific notation when JS uses it', () => {
|
|
161
161
|
// JS uses scientific notation for very large/small numbers
|
|
162
|
-
const result = canonicalJson(1e20)
|
|
163
|
-
expect(result).toBe('100000000000000000000')
|
|
162
|
+
const result = canonicalJson(1e20)
|
|
163
|
+
expect(result).toBe('100000000000000000000')
|
|
164
164
|
// 1e21 triggers scientific notation in JS
|
|
165
|
-
expect(canonicalJson(1e21)).toBe('1e+21')
|
|
166
|
-
})
|
|
165
|
+
expect(canonicalJson(1e21)).toBe('1e+21')
|
|
166
|
+
})
|
|
167
167
|
|
|
168
168
|
it('NaN → throws (RFC 8785 rejects non-finite numbers)', () => {
|
|
169
|
-
expect(() => canonicalJson(NaN)).toThrow()
|
|
170
|
-
})
|
|
169
|
+
expect(() => canonicalJson(NaN)).toThrow()
|
|
170
|
+
})
|
|
171
171
|
|
|
172
172
|
it('Infinity → throws (RFC 8785 rejects non-finite numbers)', () => {
|
|
173
|
-
expect(() => canonicalJson(Infinity)).toThrow()
|
|
174
|
-
expect(() => canonicalJson(-Infinity)).toThrow()
|
|
175
|
-
})
|
|
173
|
+
expect(() => canonicalJson(Infinity)).toThrow()
|
|
174
|
+
expect(() => canonicalJson(-Infinity)).toThrow()
|
|
175
|
+
})
|
|
176
176
|
|
|
177
177
|
it('NaN and Infinity in objects → throws', () => {
|
|
178
|
-
expect(() => canonicalJson({ a: NaN, b: Infinity })).toThrow()
|
|
179
|
-
})
|
|
180
|
-
})
|
|
178
|
+
expect(() => canonicalJson({ a: NaN, b: Infinity })).toThrow()
|
|
179
|
+
})
|
|
180
|
+
})
|
|
181
181
|
|
|
182
182
|
// ═══════════════════════════════════════════════════════════════════════
|
|
183
183
|
// D. String edge cases
|
|
@@ -187,76 +187,76 @@ describe('canonicalJson — strings', () => {
|
|
|
187
187
|
// --- Control characters ---
|
|
188
188
|
it('escapes all C0 control characters (U+0000–U+001F)', () => {
|
|
189
189
|
// Tab, newline, carriage return use short form
|
|
190
|
-
expect(canonicalJson('\t')).toBe('"\\t"')
|
|
191
|
-
expect(canonicalJson('\n')).toBe('"\\n"')
|
|
192
|
-
expect(canonicalJson('\r')).toBe('"\\r"')
|
|
193
|
-
expect(canonicalJson('\b')).toBe('"\\b"')
|
|
194
|
-
expect(canonicalJson('\f')).toBe('"\\f"')
|
|
195
|
-
})
|
|
190
|
+
expect(canonicalJson('\t')).toBe('"\\t"')
|
|
191
|
+
expect(canonicalJson('\n')).toBe('"\\n"')
|
|
192
|
+
expect(canonicalJson('\r')).toBe('"\\r"')
|
|
193
|
+
expect(canonicalJson('\b')).toBe('"\\b"')
|
|
194
|
+
expect(canonicalJson('\f')).toBe('"\\f"')
|
|
195
|
+
})
|
|
196
196
|
|
|
197
197
|
it('escapes NUL character', () => {
|
|
198
|
-
const result = canonicalJson('\x00')
|
|
199
|
-
expect(result).toBe('"\\u0000"')
|
|
200
|
-
})
|
|
198
|
+
const result = canonicalJson('\x00')
|
|
199
|
+
expect(result).toBe('"\\u0000"')
|
|
200
|
+
})
|
|
201
201
|
|
|
202
202
|
it('escapes other C0 controls with \\uXXXX', () => {
|
|
203
203
|
// U+0001 through U+001F (excluding those with short forms)
|
|
204
|
-
const result = canonicalJson('\x01')
|
|
205
|
-
expect(result).toBe('"\\u0001"')
|
|
206
|
-
expect(canonicalJson('\x1f')).toBe('"\\u001f"')
|
|
207
|
-
})
|
|
204
|
+
const result = canonicalJson('\x01')
|
|
205
|
+
expect(result).toBe('"\\u0001"')
|
|
206
|
+
expect(canonicalJson('\x1f')).toBe('"\\u001f"')
|
|
207
|
+
})
|
|
208
208
|
|
|
209
209
|
// --- Mandatory escapes ---
|
|
210
210
|
it('escapes backslash', () => {
|
|
211
|
-
expect(canonicalJson('\\')).toBe('"\\\\"')
|
|
212
|
-
})
|
|
211
|
+
expect(canonicalJson('\\')).toBe('"\\\\"')
|
|
212
|
+
})
|
|
213
213
|
|
|
214
214
|
it('escapes double quote', () => {
|
|
215
|
-
expect(canonicalJson('"')).toBe('"\\""')
|
|
216
|
-
})
|
|
215
|
+
expect(canonicalJson('"')).toBe('"\\""')
|
|
216
|
+
})
|
|
217
217
|
|
|
218
218
|
it('does NOT escape forward slash', () => {
|
|
219
|
-
expect(canonicalJson('/')).toBe('"/"')
|
|
220
|
-
expect(canonicalJson('a/b/c')).toBe('"a/b/c"')
|
|
221
|
-
})
|
|
219
|
+
expect(canonicalJson('/')).toBe('"/"')
|
|
220
|
+
expect(canonicalJson('a/b/c')).toBe('"a/b/c"')
|
|
221
|
+
})
|
|
222
222
|
|
|
223
223
|
// --- Unicode ---
|
|
224
224
|
it('preserves non-ASCII Unicode as literal UTF-8', () => {
|
|
225
|
-
expect(canonicalJson('中文')).toBe('"中文"')
|
|
226
|
-
expect(canonicalJson('éàü')).toBe('"éàü"')
|
|
227
|
-
expect(canonicalJson('日本語')).toBe('"日本語"')
|
|
228
|
-
})
|
|
225
|
+
expect(canonicalJson('中文')).toBe('"中文"')
|
|
226
|
+
expect(canonicalJson('éàü')).toBe('"éàü"')
|
|
227
|
+
expect(canonicalJson('日本語')).toBe('"日本語"')
|
|
228
|
+
})
|
|
229
229
|
|
|
230
230
|
it('handles emoji (supplementary plane)', () => {
|
|
231
|
-
expect(canonicalJson('🔑')).toBe('"🔑"')
|
|
232
|
-
expect(canonicalJson('👨👩👧')).toBe('"👨👩👧"')
|
|
233
|
-
})
|
|
231
|
+
expect(canonicalJson('🔑')).toBe('"🔑"')
|
|
232
|
+
expect(canonicalJson('👨👩👧')).toBe('"👨👩👧"')
|
|
233
|
+
})
|
|
234
234
|
|
|
235
235
|
it('handles mixed ASCII and non-ASCII', () => {
|
|
236
|
-
expect(canonicalJson('hello 世界 🌍')).toBe('"hello 世界 🌍"')
|
|
237
|
-
})
|
|
236
|
+
expect(canonicalJson('hello 世界 🌍')).toBe('"hello 世界 🌍"')
|
|
237
|
+
})
|
|
238
238
|
|
|
239
239
|
// --- Long strings ---
|
|
240
240
|
it('handles very long strings', () => {
|
|
241
|
-
const long = 'x'.repeat(10000)
|
|
242
|
-
const result = canonicalJson(long)
|
|
243
|
-
expect(result).toBe(`"${long}"`)
|
|
244
|
-
expect(result.length).toBe(10002)
|
|
245
|
-
})
|
|
241
|
+
const long = 'x'.repeat(10000)
|
|
242
|
+
const result = canonicalJson(long)
|
|
243
|
+
expect(result).toBe(`"${long}"`)
|
|
244
|
+
expect(result.length).toBe(10002) // quotes
|
|
245
|
+
})
|
|
246
246
|
|
|
247
247
|
// --- Strings that look like other types ---
|
|
248
248
|
it('does not confuse string "null" with null', () => {
|
|
249
|
-
expect(canonicalJson('null')).toBe('"null"')
|
|
250
|
-
})
|
|
249
|
+
expect(canonicalJson('null')).toBe('"null"')
|
|
250
|
+
})
|
|
251
251
|
|
|
252
252
|
it('does not confuse string "true" with true', () => {
|
|
253
|
-
expect(canonicalJson('true')).toBe('"true"')
|
|
254
|
-
})
|
|
253
|
+
expect(canonicalJson('true')).toBe('"true"')
|
|
254
|
+
})
|
|
255
255
|
|
|
256
256
|
it('does not confuse string "123" with number', () => {
|
|
257
|
-
expect(canonicalJson('123')).toBe('"123"')
|
|
258
|
-
})
|
|
259
|
-
})
|
|
257
|
+
expect(canonicalJson('123')).toBe('"123"')
|
|
258
|
+
})
|
|
259
|
+
})
|
|
260
260
|
|
|
261
261
|
// ═══════════════════════════════════════════════════════════════════════
|
|
262
262
|
// E. Structural edge cases
|
|
@@ -264,42 +264,45 @@ describe('canonicalJson — strings', () => {
|
|
|
264
264
|
|
|
265
265
|
describe('canonicalJson — structural', () => {
|
|
266
266
|
it('deeply nested objects (5 levels)', () => {
|
|
267
|
-
const input = { a: { b: { c: { d: { e: 1 } } } } }
|
|
268
|
-
expect(canonicalJson(input)).toBe('{"a":{"b":{"c":{"d":{"e":1}}}}}')
|
|
269
|
-
})
|
|
267
|
+
const input = { a: { b: { c: { d: { e: 1 } } } } }
|
|
268
|
+
expect(canonicalJson(input)).toBe('{"a":{"b":{"c":{"d":{"e":1}}}}}')
|
|
269
|
+
})
|
|
270
270
|
|
|
271
271
|
it('deeply nested arrays', () => {
|
|
272
|
-
expect(canonicalJson([[[1]]])).toBe('[[[1]]]')
|
|
273
|
-
})
|
|
272
|
+
expect(canonicalJson([[[1]]])).toBe('[[[1]]]')
|
|
273
|
+
})
|
|
274
274
|
|
|
275
275
|
it('mixed array/object nesting with key sorting at each level', () => {
|
|
276
|
-
const input = { z: [{ b: 2, a: 1 }], a: { y: [3], x: [1, 2] } }
|
|
277
|
-
expect(canonicalJson(input)).toBe('{"a":{"x":[1,2],"y":[3]},"z":[{"a":1,"b":2}]}')
|
|
278
|
-
})
|
|
276
|
+
const input = { z: [{ b: 2, a: 1 }], a: { y: [3], x: [1, 2] } }
|
|
277
|
+
expect(canonicalJson(input)).toBe('{"a":{"x":[1,2],"y":[3]},"z":[{"a":1,"b":2}]}')
|
|
278
|
+
})
|
|
279
279
|
|
|
280
280
|
it('array of objects preserves array order but sorts each object', () => {
|
|
281
|
-
const input = [
|
|
282
|
-
|
|
283
|
-
|
|
281
|
+
const input = [
|
|
282
|
+
{ z: 1, a: 2 },
|
|
283
|
+
{ y: 3, b: 4 },
|
|
284
|
+
]
|
|
285
|
+
expect(canonicalJson(input)).toBe('[{"a":2,"z":1},{"b":4,"y":3}]')
|
|
286
|
+
})
|
|
284
287
|
|
|
285
288
|
it('object with many keys', () => {
|
|
286
|
-
const obj: Record<string, number> = {}
|
|
289
|
+
const obj: Record<string, number> = {}
|
|
287
290
|
for (let i = 0; i < 26; i++) {
|
|
288
|
-
obj[String.fromCharCode(122 - i)] = i
|
|
291
|
+
obj[String.fromCharCode(122 - i)] = i // z=0, y=1, ..., a=25
|
|
289
292
|
}
|
|
290
|
-
const result = canonicalJson(obj)
|
|
293
|
+
const result = canonicalJson(obj)
|
|
291
294
|
// Should start with "a" and end with "z"
|
|
292
|
-
expect(result.startsWith('{"a":25')).toBe(true)
|
|
293
|
-
expect(result.endsWith('"z":0}')).toBe(true)
|
|
294
|
-
})
|
|
295
|
+
expect(result.startsWith('{"a":25')).toBe(true)
|
|
296
|
+
expect(result.endsWith('"z":0}')).toBe(true)
|
|
297
|
+
})
|
|
295
298
|
|
|
296
299
|
it('handles object with boolean, null, number, string, array, object values', () => {
|
|
297
|
-
const input = { str: 'hello', num: 42, bool: true, nil: null, arr: [1], obj: { x: 1 } }
|
|
300
|
+
const input = { str: 'hello', num: 42, bool: true, nil: null, arr: [1], obj: { x: 1 } }
|
|
298
301
|
expect(canonicalJson(input)).toBe(
|
|
299
302
|
'{"arr":[1],"bool":true,"nil":null,"num":42,"obj":{"x":1},"str":"hello"}',
|
|
300
|
-
)
|
|
301
|
-
})
|
|
302
|
-
})
|
|
303
|
+
)
|
|
304
|
+
})
|
|
305
|
+
})
|
|
303
306
|
|
|
304
307
|
// ═══════════════════════════════════════════════════════════════════════
|
|
305
308
|
// F. undefined handling
|
|
@@ -307,23 +310,23 @@ describe('canonicalJson — structural', () => {
|
|
|
307
310
|
|
|
308
311
|
describe('canonicalJson — undefined behavior', () => {
|
|
309
312
|
it('top-level undefined → "null"', () => {
|
|
310
|
-
expect(canonicalJson(undefined)).toBe('null')
|
|
311
|
-
})
|
|
313
|
+
expect(canonicalJson(undefined)).toBe('null')
|
|
314
|
+
})
|
|
312
315
|
|
|
313
316
|
it('undefined object values are omitted (matches JSON.stringify)', () => {
|
|
314
317
|
// Keys with undefined values are omitted, matching JSON.stringify behavior
|
|
315
318
|
// and RFC 8785 / I-JSON which have no concept of undefined
|
|
316
|
-
const result = canonicalJson({ a: 1, b: undefined, c: 3 })
|
|
317
|
-
expect(result).toBe('{"a":1,"c":3}')
|
|
318
|
-
expect(result).not.toContain('"b"')
|
|
319
|
-
})
|
|
319
|
+
const result = canonicalJson({ a: 1, b: undefined, c: 3 })
|
|
320
|
+
expect(result).toBe('{"a":1,"c":3}')
|
|
321
|
+
expect(result).not.toContain('"b"')
|
|
322
|
+
})
|
|
320
323
|
|
|
321
324
|
it('undefined in array → "null"', () => {
|
|
322
325
|
// JSON.stringify([undefined]) → "[null]"
|
|
323
|
-
expect(canonicalJson([undefined])).toBe('[null]')
|
|
324
|
-
expect(canonicalJson([1, undefined, 3])).toBe('[1,null,3]')
|
|
325
|
-
})
|
|
326
|
-
})
|
|
326
|
+
expect(canonicalJson([undefined])).toBe('[null]')
|
|
327
|
+
expect(canonicalJson([1, undefined, 3])).toBe('[1,null,3]')
|
|
328
|
+
})
|
|
329
|
+
})
|
|
327
330
|
|
|
328
331
|
// ═══════════════════════════════════════════════════════════════════════
|
|
329
332
|
// G. No whitespace
|
|
@@ -336,14 +339,14 @@ describe('canonicalJson — no whitespace', () => {
|
|
|
336
339
|
events: ['accountsChanged', 'chainChanged'],
|
|
337
340
|
chains: ['eip155:1', 'eip155:137'],
|
|
338
341
|
meta: { name: 'My Wallet', description: 'A test wallet' },
|
|
339
|
-
}
|
|
340
|
-
const result = canonicalJson(complex)
|
|
342
|
+
}
|
|
343
|
+
const result = canonicalJson(complex)
|
|
341
344
|
// Content strings may contain spaces, but structural whitespace should not exist
|
|
342
345
|
// Remove string content to check structural whitespace
|
|
343
|
-
const structural = result.replace(/"[^"]*"/g, '""')
|
|
344
|
-
expect(structural).not.toMatch(/[\s]/)
|
|
345
|
-
})
|
|
346
|
-
})
|
|
346
|
+
const structural = result.replace(/"[^"]*"/g, '""')
|
|
347
|
+
expect(structural).not.toMatch(/[\s]/)
|
|
348
|
+
})
|
|
349
|
+
})
|
|
347
350
|
|
|
348
351
|
// ═══════════════════════════════════════════════════════════════════════
|
|
349
352
|
// H. Determinism and idempotency
|
|
@@ -351,31 +354,35 @@ describe('canonicalJson — no whitespace', () => {
|
|
|
351
354
|
|
|
352
355
|
describe('canonicalJson — determinism', () => {
|
|
353
356
|
it('output is idempotent (parsing output and re-serializing yields same result)', () => {
|
|
354
|
-
const input = { z: [3, 1], a: { y: 'hello', x: true } }
|
|
355
|
-
const first = canonicalJson(input)
|
|
356
|
-
const reparsed = JSON.parse(first)
|
|
357
|
-
const second = canonicalJson(reparsed)
|
|
358
|
-
expect(second).toBe(first)
|
|
359
|
-
})
|
|
357
|
+
const input = { z: [3, 1], a: { y: 'hello', x: true } }
|
|
358
|
+
const first = canonicalJson(input)
|
|
359
|
+
const reparsed = JSON.parse(first)
|
|
360
|
+
const second = canonicalJson(reparsed)
|
|
361
|
+
expect(second).toBe(first)
|
|
362
|
+
})
|
|
360
363
|
|
|
361
364
|
it('different insertion order produces same output', () => {
|
|
362
|
-
const a: Record<string, number> = {}
|
|
363
|
-
a.x = 1
|
|
365
|
+
const a: Record<string, number> = {}
|
|
366
|
+
a.x = 1
|
|
367
|
+
a.a = 2
|
|
368
|
+
a.m = 3
|
|
364
369
|
|
|
365
|
-
const b: Record<string, number> = {}
|
|
366
|
-
b.a = 2
|
|
370
|
+
const b: Record<string, number> = {}
|
|
371
|
+
b.a = 2
|
|
372
|
+
b.m = 3
|
|
373
|
+
b.x = 1
|
|
367
374
|
|
|
368
|
-
expect(canonicalJson(a)).toBe(canonicalJson(b))
|
|
369
|
-
})
|
|
375
|
+
expect(canonicalJson(a)).toBe(canonicalJson(b))
|
|
376
|
+
})
|
|
370
377
|
|
|
371
378
|
it('100 runs produce identical output', () => {
|
|
372
|
-
const input = { z: 1, a: [{ c: 3, b: 2 }], m: null }
|
|
373
|
-
const expected = canonicalJson(input)
|
|
379
|
+
const input = { z: 1, a: [{ c: 3, b: 2 }], m: null }
|
|
380
|
+
const expected = canonicalJson(input)
|
|
374
381
|
for (let i = 0; i < 100; i++) {
|
|
375
|
-
expect(canonicalJson(input)).toBe(expected)
|
|
382
|
+
expect(canonicalJson(input)).toBe(expected)
|
|
376
383
|
}
|
|
377
|
-
})
|
|
378
|
-
})
|
|
384
|
+
})
|
|
385
|
+
})
|
|
379
386
|
|
|
380
387
|
// ═══════════════════════════════════════════════════════════════════════
|
|
381
388
|
// I. Cross-implementation compatibility vectors
|
|
@@ -383,7 +390,7 @@ describe('canonicalJson — determinism', () => {
|
|
|
383
390
|
|
|
384
391
|
describe('canonicalJson — protocol spec test vectors (all SHA-256 verified)', () => {
|
|
385
392
|
function hash(s: string): string {
|
|
386
|
-
return sha256Hex(new TextEncoder().encode(s))
|
|
393
|
+
return sha256Hex(new TextEncoder().encode(s))
|
|
387
394
|
}
|
|
388
395
|
|
|
389
396
|
it('vector 1: capabilities (key sorting, nested)', () => {
|
|
@@ -391,11 +398,12 @@ describe('canonicalJson — protocol spec test vectors (all SHA-256 verified)',
|
|
|
391
398
|
methods: ['wallet_signTransaction', 'wallet_signMessage'],
|
|
392
399
|
events: ['accountsChanged', 'chainChanged'],
|
|
393
400
|
chains: ['eip155:1', 'eip155:137'],
|
|
394
|
-
})
|
|
395
|
-
const expected =
|
|
396
|
-
|
|
397
|
-
expect(
|
|
398
|
-
|
|
401
|
+
})
|
|
402
|
+
const expected =
|
|
403
|
+
'{"chains":["eip155:1","eip155:137"],"events":["accountsChanged","chainChanged"],"methods":["wallet_signTransaction","wallet_signMessage"]}'
|
|
404
|
+
expect(output).toBe(expected)
|
|
405
|
+
expect(hash(output)).toBe('4da366e2aae26b47b3d90fff52410752348733350ce2525dce7d64510f571333')
|
|
406
|
+
})
|
|
399
407
|
|
|
400
408
|
it('vector 2: join plaintext (nested objects + meta, all fields required)', () => {
|
|
401
409
|
const output = canonicalJson({
|
|
@@ -410,77 +418,81 @@ describe('canonicalJson — protocol spec test vectors (all SHA-256 verified)',
|
|
|
410
418
|
url: 'https://mywallet.app',
|
|
411
419
|
icon: 'https://mywallet.app/icon.png',
|
|
412
420
|
},
|
|
413
|
-
})
|
|
414
|
-
const expected =
|
|
415
|
-
|
|
416
|
-
expect(
|
|
417
|
-
|
|
421
|
+
})
|
|
422
|
+
const expected =
|
|
423
|
+
'{"capabilities":{"chains":["eip155:1","eip155:137"],"events":["accountsChanged","chainChanged"],"methods":["wallet_signTransaction","wallet_signMessage"]},"meta":{"description":"A multi-chain wallet","icon":"https://mywallet.app/icon.png","name":"MyWallet","url":"https://mywallet.app"}}'
|
|
424
|
+
expect(output).toBe(expected)
|
|
425
|
+
expect(hash(output)).toBe('9f4f3b71b0db39ba8b86173b8c78182799d0a745c68b6e89e5d8f0d3def52594')
|
|
426
|
+
})
|
|
418
427
|
|
|
419
428
|
it('vector 3a: null', () => {
|
|
420
|
-
const output = canonicalJson(null)
|
|
421
|
-
expect(output).toBe('null')
|
|
422
|
-
expect(hash(output)).toBe('74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b')
|
|
423
|
-
})
|
|
429
|
+
const output = canonicalJson(null)
|
|
430
|
+
expect(output).toBe('null')
|
|
431
|
+
expect(hash(output)).toBe('74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b')
|
|
432
|
+
})
|
|
424
433
|
|
|
425
434
|
it('vector 3b: true', () => {
|
|
426
|
-
const output = canonicalJson(true)
|
|
427
|
-
expect(output).toBe('true')
|
|
428
|
-
expect(hash(output)).toBe('b5bea41b6c623f7c09f1bf24dcae58ebab3c0cdd90ad966bc43a45b44867e12b')
|
|
429
|
-
})
|
|
435
|
+
const output = canonicalJson(true)
|
|
436
|
+
expect(output).toBe('true')
|
|
437
|
+
expect(hash(output)).toBe('b5bea41b6c623f7c09f1bf24dcae58ebab3c0cdd90ad966bc43a45b44867e12b')
|
|
438
|
+
})
|
|
430
439
|
|
|
431
440
|
it('vector 3c: 42', () => {
|
|
432
|
-
const output = canonicalJson(42)
|
|
433
|
-
expect(output).toBe('42')
|
|
434
|
-
expect(hash(output)).toBe('73475cb40a568e8da8a045ced110137e159f890ac4da883b6b17dc651b3a8049')
|
|
435
|
-
})
|
|
441
|
+
const output = canonicalJson(42)
|
|
442
|
+
expect(output).toBe('42')
|
|
443
|
+
expect(hash(output)).toBe('73475cb40a568e8da8a045ced110137e159f890ac4da883b6b17dc651b3a8049')
|
|
444
|
+
})
|
|
436
445
|
|
|
437
446
|
it('vector 3d: "hello" (string)', () => {
|
|
438
|
-
const output = canonicalJson('hello')
|
|
439
|
-
expect(output).toBe('"hello"')
|
|
440
|
-
expect(hash(output)).toBe('5aa762ae383fbb727af3c7a36d4940a5b8c40a989452d2304fc958ff3f354e7a')
|
|
441
|
-
})
|
|
447
|
+
const output = canonicalJson('hello')
|
|
448
|
+
expect(output).toBe('"hello"')
|
|
449
|
+
expect(hash(output)).toBe('5aa762ae383fbb727af3c7a36d4940a5b8c40a989452d2304fc958ff3f354e7a')
|
|
450
|
+
})
|
|
442
451
|
|
|
443
452
|
it('vector 4a: empty object {}', () => {
|
|
444
|
-
const output = canonicalJson({})
|
|
445
|
-
expect(output).toBe('{}')
|
|
446
|
-
expect(hash(output)).toBe('44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a')
|
|
447
|
-
})
|
|
453
|
+
const output = canonicalJson({})
|
|
454
|
+
expect(output).toBe('{}')
|
|
455
|
+
expect(hash(output)).toBe('44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a')
|
|
456
|
+
})
|
|
448
457
|
|
|
449
458
|
it('vector 4b: empty array []', () => {
|
|
450
|
-
const output = canonicalJson([])
|
|
451
|
-
expect(output).toBe('[]')
|
|
452
|
-
expect(hash(output)).toBe('4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945')
|
|
453
|
-
})
|
|
459
|
+
const output = canonicalJson([])
|
|
460
|
+
expect(output).toBe('[]')
|
|
461
|
+
expect(hash(output)).toBe('4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945')
|
|
462
|
+
})
|
|
454
463
|
|
|
455
464
|
it('vector 5: negative zero → 0', () => {
|
|
456
|
-
const output = canonicalJson(-0)
|
|
457
|
-
expect(output).toBe('0')
|
|
458
|
-
expect(hash(output)).toBe('5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9')
|
|
459
|
-
})
|
|
465
|
+
const output = canonicalJson(-0)
|
|
466
|
+
expect(output).toBe('0')
|
|
467
|
+
expect(hash(output)).toBe('5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9')
|
|
468
|
+
})
|
|
460
469
|
|
|
461
470
|
it('vector 6: escaped control character (lowercase hex)', () => {
|
|
462
|
-
const output = canonicalJson('\u0001')
|
|
463
|
-
expect(output).toBe('"\\u0001"')
|
|
464
|
-
expect(hash(output)).toBe('b81cfb0a6715e53b373345b49e8ad94eb55fd777519dc539373d0634973c186e')
|
|
465
|
-
})
|
|
466
|
-
})
|
|
471
|
+
const output = canonicalJson('\u0001')
|
|
472
|
+
expect(output).toBe('"\\u0001"')
|
|
473
|
+
expect(hash(output)).toBe('b81cfb0a6715e53b373345b49e8ad94eb55fd777519dc539373d0634973c186e')
|
|
474
|
+
})
|
|
475
|
+
})
|
|
467
476
|
|
|
468
477
|
describe('canonicalJson — additional cross-implementation vectors', () => {
|
|
469
478
|
it('empty capabilities with null meta', () => {
|
|
470
|
-
const output = canonicalJson({
|
|
471
|
-
|
|
472
|
-
|
|
479
|
+
const output = canonicalJson({
|
|
480
|
+
capabilities: { methods: [], events: [], chains: [] },
|
|
481
|
+
meta: null,
|
|
482
|
+
})
|
|
483
|
+
expect(output).toBe('{"capabilities":{"chains":[],"events":[],"methods":[]},"meta":null}')
|
|
484
|
+
})
|
|
473
485
|
|
|
474
486
|
it('unicode key and value', () => {
|
|
475
|
-
const output = canonicalJson({ name: '钱包', chains: ['eip155:1'] })
|
|
476
|
-
expect(output).toBe('{"chains":["eip155:1"],"name":"钱包"}')
|
|
477
|
-
})
|
|
487
|
+
const output = canonicalJson({ name: '钱包', chains: ['eip155:1'] })
|
|
488
|
+
expect(output).toBe('{"chains":["eip155:1"],"name":"钱包"}')
|
|
489
|
+
})
|
|
478
490
|
|
|
479
491
|
it('boolean and null mix with sorting', () => {
|
|
480
|
-
const output = canonicalJson({ z: null, a: true, m: false })
|
|
481
|
-
expect(output).toBe('{"a":true,"m":false,"z":null}')
|
|
482
|
-
})
|
|
483
|
-
})
|
|
492
|
+
const output = canonicalJson({ z: null, a: true, m: false })
|
|
493
|
+
expect(output).toBe('{"a":true,"m":false,"z":null}')
|
|
494
|
+
})
|
|
495
|
+
})
|
|
484
496
|
|
|
485
497
|
// ═══════════════════════════════════════════════════════════════════════
|
|
486
498
|
// J. RFC 8785 compliance — specific requirements
|
|
@@ -490,102 +502,106 @@ describe('canonicalJson — RFC 8785 specific compliance', () => {
|
|
|
490
502
|
// --- \uXXXX must be lowercase hex (RFC 8785 §3.2.2.2) ---
|
|
491
503
|
it('\\u escapes use lowercase hex digits', () => {
|
|
492
504
|
// U+0001 → \u0001 (not \u0001 with uppercase)
|
|
493
|
-
const result = canonicalJson('\u0001')
|
|
494
|
-
expect(result).toBe('"\\u0001"')
|
|
495
|
-
expect(result).not.toMatch(/\\u[0-9a-f]*[A-F]/)
|
|
496
|
-
})
|
|
505
|
+
const result = canonicalJson('\u0001')
|
|
506
|
+
expect(result).toBe('"\\u0001"')
|
|
507
|
+
expect(result).not.toMatch(/\\u[0-9a-f]*[A-F]/) // no uppercase hex digits
|
|
508
|
+
})
|
|
497
509
|
|
|
498
510
|
it('\\u001f uses lowercase f', () => {
|
|
499
|
-
const result = canonicalJson('\u001f')
|
|
500
|
-
expect(result).toBe('"\\u001f"')
|
|
511
|
+
const result = canonicalJson('\u001f')
|
|
512
|
+
expect(result).toBe('"\\u001f"')
|
|
501
513
|
// Verify the 'f' is lowercase (0x66) not uppercase 'F' (0x46)
|
|
502
|
-
expect(result.charAt(6)).toBe('f')
|
|
503
|
-
})
|
|
514
|
+
expect(result.charAt(6)).toBe('f')
|
|
515
|
+
})
|
|
504
516
|
|
|
505
517
|
it('all C0 control characters (U+0000–U+001F) are properly escaped', () => {
|
|
506
518
|
const shortForms: Record<number, string> = {
|
|
507
|
-
|
|
508
|
-
|
|
519
|
+
8: '\\b',
|
|
520
|
+
9: '\\t',
|
|
521
|
+
10: '\\n',
|
|
522
|
+
12: '\\f',
|
|
523
|
+
13: '\\r',
|
|
524
|
+
}
|
|
509
525
|
for (let cp = 0; cp <= 0x1f; cp++) {
|
|
510
|
-
const result = canonicalJson(String.fromCharCode(cp))
|
|
526
|
+
const result = canonicalJson(String.fromCharCode(cp))
|
|
511
527
|
if (shortForms[cp]) {
|
|
512
|
-
expect(result).toBe(`"${shortForms[cp]}"`)
|
|
528
|
+
expect(result).toBe(`"${shortForms[cp]}"`)
|
|
513
529
|
} else {
|
|
514
|
-
const hex = cp.toString(16).padStart(4, '0')
|
|
515
|
-
expect(result).toBe(`"\\u${hex}"`)
|
|
530
|
+
const hex = cp.toString(16).padStart(4, '0')
|
|
531
|
+
expect(result).toBe(`"\\u${hex}"`)
|
|
516
532
|
}
|
|
517
533
|
}
|
|
518
|
-
})
|
|
534
|
+
})
|
|
519
535
|
|
|
520
536
|
// --- Lone surrogates (ES2019 well-formed JSON.stringify) ---
|
|
521
537
|
it('lone high surrogate is escaped as \\udXXX', () => {
|
|
522
|
-
const result = canonicalJson('\uD800')
|
|
523
|
-
expect(result).toBe('"\\ud800"')
|
|
524
|
-
})
|
|
538
|
+
const result = canonicalJson('\uD800')
|
|
539
|
+
expect(result).toBe('"\\ud800"')
|
|
540
|
+
})
|
|
525
541
|
|
|
526
542
|
it('lone low surrogate is escaped as \\udcXX', () => {
|
|
527
|
-
const result = canonicalJson('\uDC00')
|
|
528
|
-
expect(result).toBe('"\\udc00"')
|
|
529
|
-
})
|
|
543
|
+
const result = canonicalJson('\uDC00')
|
|
544
|
+
expect(result).toBe('"\\udc00"')
|
|
545
|
+
})
|
|
530
546
|
|
|
531
547
|
it('valid surrogate pair (emoji) is NOT escaped', () => {
|
|
532
548
|
// U+1F511 = 🔑 = \uD83D\uDD11 (surrogate pair)
|
|
533
|
-
const result = canonicalJson('🔑')
|
|
534
|
-
expect(result).toBe('"🔑"')
|
|
535
|
-
expect(result).not.toContain('\\u')
|
|
536
|
-
})
|
|
549
|
+
const result = canonicalJson('🔑')
|
|
550
|
+
expect(result).toBe('"🔑"')
|
|
551
|
+
expect(result).not.toContain('\\u')
|
|
552
|
+
})
|
|
537
553
|
|
|
538
554
|
// --- Number serialization matches ECMAScript Number.toString() ---
|
|
539
555
|
it('1e20 outputs as full decimal (no scientific notation)', () => {
|
|
540
|
-
expect(canonicalJson(1e20)).toBe('100000000000000000000')
|
|
541
|
-
})
|
|
556
|
+
expect(canonicalJson(1e20)).toBe('100000000000000000000')
|
|
557
|
+
})
|
|
542
558
|
|
|
543
559
|
it('1e21 uses scientific notation (JS threshold)', () => {
|
|
544
|
-
expect(canonicalJson(1e21)).toBe('1e+21')
|
|
545
|
-
})
|
|
560
|
+
expect(canonicalJson(1e21)).toBe('1e+21')
|
|
561
|
+
})
|
|
546
562
|
|
|
547
563
|
it('5e-7 uses scientific notation', () => {
|
|
548
|
-
expect(canonicalJson(5e-7)).toBe('5e-7')
|
|
549
|
-
})
|
|
564
|
+
expect(canonicalJson(5e-7)).toBe('5e-7')
|
|
565
|
+
})
|
|
550
566
|
|
|
551
567
|
it('0.000001 uses decimal notation', () => {
|
|
552
|
-
expect(canonicalJson(0.000001)).toBe('0.000001')
|
|
553
|
-
})
|
|
568
|
+
expect(canonicalJson(0.000001)).toBe('0.000001')
|
|
569
|
+
})
|
|
554
570
|
|
|
555
571
|
it('Number.MIN_VALUE in scientific notation', () => {
|
|
556
|
-
expect(canonicalJson(Number.MIN_VALUE)).toBe('5e-324')
|
|
557
|
-
})
|
|
572
|
+
expect(canonicalJson(Number.MIN_VALUE)).toBe('5e-324')
|
|
573
|
+
})
|
|
558
574
|
|
|
559
575
|
it('Number.MAX_SAFE_INTEGER preserves precision', () => {
|
|
560
|
-
expect(canonicalJson(9007199254740991)).toBe('9007199254740991')
|
|
561
|
-
})
|
|
576
|
+
expect(canonicalJson(9007199254740991)).toBe('9007199254740991')
|
|
577
|
+
})
|
|
562
578
|
|
|
563
579
|
// --- Duplicate key handling ---
|
|
564
580
|
it('JSON.parse deduplicates keys (last value wins)', () => {
|
|
565
581
|
// This tests that our implementation handles the JS-level dedup correctly.
|
|
566
582
|
// True duplicate rejection requires a custom JSON parser, which is out of scope
|
|
567
583
|
// for the SDK (implementers using other languages must reject at parse time).
|
|
568
|
-
const input = JSON.parse('{"a":1,"b":2,"a":3}')
|
|
569
|
-
expect(canonicalJson(input)).toBe('{"a":3,"b":2}')
|
|
570
|
-
})
|
|
584
|
+
const input = JSON.parse('{"a":1,"b":2,"a":3}')
|
|
585
|
+
expect(canonicalJson(input)).toBe('{"a":3,"b":2}')
|
|
586
|
+
})
|
|
571
587
|
|
|
572
588
|
// --- Output encoding ---
|
|
573
589
|
it('output bytes are valid UTF-8', () => {
|
|
574
|
-
const output = canonicalJson({ name: '钱包', emoji: '🔑' })
|
|
575
|
-
const bytes = new TextEncoder().encode(output)
|
|
590
|
+
const output = canonicalJson({ name: '钱包', emoji: '🔑' })
|
|
591
|
+
const bytes = new TextEncoder().encode(output)
|
|
576
592
|
// TextEncoder always produces UTF-8
|
|
577
593
|
// Verify round-trip
|
|
578
|
-
const decoded = new TextDecoder('utf-8', { fatal: true }).decode(bytes)
|
|
579
|
-
expect(decoded).toBe(output)
|
|
580
|
-
})
|
|
594
|
+
const decoded = new TextDecoder('utf-8', { fatal: true }).decode(bytes)
|
|
595
|
+
expect(decoded).toBe(output)
|
|
596
|
+
})
|
|
581
597
|
|
|
582
598
|
it('output has no BOM', () => {
|
|
583
|
-
const output = canonicalJson({ test: true })
|
|
584
|
-
const bytes = new TextEncoder().encode(output)
|
|
599
|
+
const output = canonicalJson({ test: true })
|
|
600
|
+
const bytes = new TextEncoder().encode(output)
|
|
585
601
|
// UTF-8 BOM = EF BB BF
|
|
586
|
-
expect(bytes[0]).not.toBe(0xef)
|
|
587
|
-
})
|
|
588
|
-
})
|
|
602
|
+
expect(bytes[0]).not.toBe(0xef)
|
|
603
|
+
})
|
|
604
|
+
})
|
|
589
605
|
|
|
590
606
|
// ═══════════════════════════════════════════════════════════════════════
|
|
591
607
|
// K. Protocol-specific: sealed payload uses canonical JSON
|
|
@@ -595,18 +611,18 @@ describe('canonicalJson — seal/unseal round-trip proves canonical encoding', (
|
|
|
595
611
|
it('object with unsorted keys round-trips correctly through seal/unseal', async () => {
|
|
596
612
|
// This proves that sealPayload uses canonicalJson internally,
|
|
597
613
|
// and unsealPayload returns the canonical form
|
|
598
|
-
const { sealPayload, unsealPayload } = await import('./crypto.js')
|
|
599
|
-
const key = new Uint8Array(32).fill(0xdd)
|
|
600
|
-
const ch = 'ee'.repeat(32)
|
|
614
|
+
const { sealPayload, unsealPayload } = await import('./crypto.js')
|
|
615
|
+
const key = new Uint8Array(32).fill(0xdd)
|
|
616
|
+
const ch = 'ee'.repeat(32)
|
|
601
617
|
|
|
602
618
|
// Input has keys in reverse order
|
|
603
|
-
const input = { z: 1, a: 2, m: 3 }
|
|
604
|
-
const sealed = sealPayload(key, ch, 0, input)
|
|
605
|
-
const { data, plaintextJson } = unsealPayload(key, ch, sealed)
|
|
619
|
+
const input = { z: 1, a: 2, m: 3 }
|
|
620
|
+
const sealed = sealPayload(key, ch, 0, input)
|
|
621
|
+
const { data, plaintextJson } = unsealPayload(key, ch, sealed)
|
|
606
622
|
|
|
607
623
|
// Data round-trips correctly
|
|
608
|
-
expect(data).toEqual(input)
|
|
624
|
+
expect(data).toEqual(input)
|
|
609
625
|
// The plaintext inside the ciphertext is canonical JSON
|
|
610
|
-
expect(plaintextJson).toBe('{"a":2,"m":3,"z":1}')
|
|
611
|
-
})
|
|
612
|
-
})
|
|
626
|
+
expect(plaintextJson).toBe('{"a":2,"m":3,"z":1}')
|
|
627
|
+
})
|
|
628
|
+
})
|