@johnhenry/wsh 0.0.0

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/src/cbor.mjs ADDED
@@ -0,0 +1,389 @@
1
+ /**
2
+ * CBOR codec with length-prefixed framing for wsh protocol.
3
+ *
4
+ * Wire format: [4-byte big-endian length][CBOR payload]
5
+ * Uses a minimal CBOR encoder/decoder (subset: maps, arrays, strings, ints,
6
+ * bytes, booleans, null, floats).
7
+ */
8
+
9
+ // ── CBOR major types ──────────────────────────────────────────────────
10
+ const MT_UNSIGNED = 0;
11
+ const MT_NEGATIVE = 1;
12
+ const MT_BYTES = 2;
13
+ const MT_TEXT = 3;
14
+ const MT_ARRAY = 4;
15
+ const MT_MAP = 5;
16
+ const MT_TAG = 6; // unused but recognized
17
+ const MT_SIMPLE = 7;
18
+
19
+ const SIMPLE_FALSE = 20;
20
+ const SIMPLE_TRUE = 21;
21
+ const SIMPLE_NULL = 22;
22
+ const FLOAT_16 = 25;
23
+ const FLOAT_32 = 26;
24
+ const FLOAT_64 = 27;
25
+ const BREAK = 31;
26
+
27
+ const encoder = new TextEncoder();
28
+ const decoder = new TextDecoder();
29
+
30
+ // ── Encoder ───────────────────────────────────────────────────────────
31
+
32
+ /**
33
+ * Encode a JS value into CBOR bytes.
34
+ * @param {*} value
35
+ * @returns {Uint8Array}
36
+ */
37
+ export function cborEncode(value) {
38
+ const parts = [];
39
+ encodeValue(value, parts);
40
+ return concat(parts);
41
+ }
42
+
43
+ function encodeValue(value, parts) {
44
+ if (value === null || value === undefined) {
45
+ parts.push(new Uint8Array([0xf6])); // null
46
+ return;
47
+ }
48
+ if (value === true) {
49
+ parts.push(new Uint8Array([0xf5]));
50
+ return;
51
+ }
52
+ if (value === false) {
53
+ parts.push(new Uint8Array([0xf4]));
54
+ return;
55
+ }
56
+ if (typeof value === 'number') {
57
+ if (Number.isInteger(value)) {
58
+ if (value >= 0) {
59
+ parts.push(encodeHead(MT_UNSIGNED, value));
60
+ } else {
61
+ parts.push(encodeHead(MT_NEGATIVE, -1 - value));
62
+ }
63
+ } else {
64
+ // float64
65
+ const buf = new ArrayBuffer(9);
66
+ const view = new DataView(buf);
67
+ view.setUint8(0, (MT_SIMPLE << 5) | FLOAT_64);
68
+ view.setFloat64(1, value);
69
+ parts.push(new Uint8Array(buf));
70
+ }
71
+ return;
72
+ }
73
+ if (typeof value === 'string') {
74
+ const bytes = encoder.encode(value);
75
+ parts.push(encodeHead(MT_TEXT, bytes.length));
76
+ parts.push(bytes);
77
+ return;
78
+ }
79
+ if (value instanceof Uint8Array) {
80
+ parts.push(encodeHead(MT_BYTES, value.length));
81
+ parts.push(value);
82
+ return;
83
+ }
84
+ if (ArrayBuffer.isView(value)) {
85
+ const bytes = new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
86
+ parts.push(encodeHead(MT_BYTES, bytes.length));
87
+ parts.push(bytes);
88
+ return;
89
+ }
90
+ if (Array.isArray(value)) {
91
+ parts.push(encodeHead(MT_ARRAY, value.length));
92
+ for (const item of value) encodeValue(item, parts);
93
+ return;
94
+ }
95
+ if (typeof value === 'object') {
96
+ const keys = Object.keys(value);
97
+ parts.push(encodeHead(MT_MAP, keys.length));
98
+ for (const key of keys) {
99
+ encodeValue(key, parts);
100
+ encodeValue(value[key], parts);
101
+ }
102
+ return;
103
+ }
104
+ throw new Error(`cborEncode: unsupported type ${typeof value}`);
105
+ }
106
+
107
+ function encodeHead(majorType, value) {
108
+ const mt = majorType << 5;
109
+ if (value < 24) {
110
+ return new Uint8Array([mt | value]);
111
+ }
112
+ if (value < 0x100) {
113
+ return new Uint8Array([mt | 24, value]);
114
+ }
115
+ if (value < 0x10000) {
116
+ const buf = new Uint8Array(3);
117
+ buf[0] = mt | 25;
118
+ buf[1] = (value >> 8) & 0xff;
119
+ buf[2] = value & 0xff;
120
+ return buf;
121
+ }
122
+ if (value < 0x100000000) {
123
+ const buf = new Uint8Array(5);
124
+ buf[0] = mt | 26;
125
+ buf[1] = (value >> 24) & 0xff;
126
+ buf[2] = (value >> 16) & 0xff;
127
+ buf[3] = (value >> 8) & 0xff;
128
+ buf[4] = value & 0xff;
129
+ return buf;
130
+ }
131
+ // 8-byte (use DataView for 64-bit)
132
+ const buf = new ArrayBuffer(9);
133
+ const view = new DataView(buf);
134
+ view.setUint8(0, mt | 27);
135
+ // JS safe integer fits in 53 bits
136
+ view.setUint32(1, Math.floor(value / 0x100000000));
137
+ view.setUint32(5, value >>> 0);
138
+ return new Uint8Array(buf);
139
+ }
140
+
141
+ // ── Decoder ───────────────────────────────────────────────────────────
142
+
143
+ /**
144
+ * Decode CBOR bytes into a JS value.
145
+ * @param {Uint8Array} data
146
+ * @returns {*}
147
+ */
148
+ export function cborDecode(data) {
149
+ const state = { data, offset: 0 };
150
+ const result = decodeValue(state);
151
+ return result;
152
+ }
153
+
154
+ function decodeValue(state) {
155
+ if (state.offset >= state.data.length) throw new Error('cborDecode: unexpected end');
156
+ const byte = state.data[state.offset++];
157
+ const mt = byte >> 5;
158
+ const ai = byte & 0x1f;
159
+
160
+ switch (mt) {
161
+ case MT_UNSIGNED: return decodeUint(state, ai);
162
+ case MT_NEGATIVE: return -1 - decodeUint(state, ai);
163
+ case MT_BYTES: {
164
+ if (ai === BREAK) return decodeIndefiniteBytes(state);
165
+ const len = decodeUint(state, ai);
166
+ const bytes = state.data.slice(state.offset, state.offset + len);
167
+ state.offset += len;
168
+ return bytes;
169
+ }
170
+ case MT_TEXT: {
171
+ if (ai === BREAK) return decodeIndefiniteText(state);
172
+ const len = decodeUint(state, ai);
173
+ const text = decoder.decode(state.data.subarray(state.offset, state.offset + len));
174
+ state.offset += len;
175
+ return text;
176
+ }
177
+ case MT_ARRAY: {
178
+ if (ai === BREAK) return decodeIndefiniteArray(state);
179
+ const len = decodeUint(state, ai);
180
+ const arr = new Array(len);
181
+ for (let i = 0; i < len; i++) arr[i] = decodeValue(state);
182
+ return arr;
183
+ }
184
+ case MT_MAP: {
185
+ if (ai === BREAK) return decodeIndefiniteMap(state);
186
+ const len = decodeUint(state, ai);
187
+ const obj = {};
188
+ for (let i = 0; i < len; i++) {
189
+ const key = decodeValue(state);
190
+ obj[key] = decodeValue(state);
191
+ }
192
+ return obj;
193
+ }
194
+ case MT_TAG: {
195
+ // skip tag number, return inner value
196
+ decodeUint(state, ai);
197
+ return decodeValue(state);
198
+ }
199
+ case MT_SIMPLE: {
200
+ if (ai === SIMPLE_FALSE) return false;
201
+ if (ai === SIMPLE_TRUE) return true;
202
+ if (ai === SIMPLE_NULL) return null;
203
+ if (ai === FLOAT_16) {
204
+ // half-precision float
205
+ const half = (state.data[state.offset] << 8) | state.data[state.offset + 1];
206
+ state.offset += 2;
207
+ return decodeFloat16(half);
208
+ }
209
+ if (ai === FLOAT_32) {
210
+ const view = new DataView(state.data.buffer, state.data.byteOffset + state.offset, 4);
211
+ state.offset += 4;
212
+ return view.getFloat32(0);
213
+ }
214
+ if (ai === FLOAT_64) {
215
+ const view = new DataView(state.data.buffer, state.data.byteOffset + state.offset, 8);
216
+ state.offset += 8;
217
+ return view.getFloat64(0);
218
+ }
219
+ if (ai === BREAK) return undefined; // break code
220
+ return ai; // simple value
221
+ }
222
+ default:
223
+ throw new Error(`cborDecode: unknown major type ${mt}`);
224
+ }
225
+ }
226
+
227
+ function decodeIndefiniteBytes(state) {
228
+ const chunks = [];
229
+ while (!consumeBreak(state)) {
230
+ const chunk = decodeValue(state);
231
+ if (!(chunk instanceof Uint8Array)) {
232
+ throw new Error('cborDecode: invalid chunk in indefinite byte string');
233
+ }
234
+ chunks.push(chunk);
235
+ }
236
+ return concat(chunks);
237
+ }
238
+
239
+ function decodeIndefiniteText(state) {
240
+ let text = '';
241
+ while (!consumeBreak(state)) {
242
+ const chunk = decodeValue(state);
243
+ if (typeof chunk !== 'string') {
244
+ throw new Error('cborDecode: invalid chunk in indefinite text string');
245
+ }
246
+ text += chunk;
247
+ }
248
+ return text;
249
+ }
250
+
251
+ function decodeIndefiniteArray(state) {
252
+ const arr = [];
253
+ while (!consumeBreak(state)) {
254
+ arr.push(decodeValue(state));
255
+ }
256
+ return arr;
257
+ }
258
+
259
+ function decodeIndefiniteMap(state) {
260
+ const obj = {};
261
+ while (!consumeBreak(state)) {
262
+ const key = decodeValue(state);
263
+ obj[key] = decodeValue(state);
264
+ }
265
+ return obj;
266
+ }
267
+
268
+ function consumeBreak(state) {
269
+ if (state.offset >= state.data.length) {
270
+ throw new Error('cborDecode: unexpected end in indefinite value');
271
+ }
272
+ if (state.data[state.offset] === 0xff) {
273
+ state.offset += 1;
274
+ return true;
275
+ }
276
+ return false;
277
+ }
278
+
279
+ function decodeUint(state, ai) {
280
+ if (ai < 24) return ai;
281
+ if (ai === 24) return state.data[state.offset++];
282
+ if (ai === 25) {
283
+ const val = (state.data[state.offset] << 8) | state.data[state.offset + 1];
284
+ state.offset += 2;
285
+ return val;
286
+ }
287
+ if (ai === 26) {
288
+ const view = new DataView(state.data.buffer, state.data.byteOffset + state.offset, 4);
289
+ state.offset += 4;
290
+ return view.getUint32(0);
291
+ }
292
+ if (ai === 27) {
293
+ const view = new DataView(state.data.buffer, state.data.byteOffset + state.offset, 8);
294
+ state.offset += 8;
295
+ const hi = view.getUint32(0);
296
+ const lo = view.getUint32(4);
297
+ return hi * 0x100000000 + lo;
298
+ }
299
+ throw new Error(`cborDecode: invalid additional info ${ai}`);
300
+ }
301
+
302
+ function decodeFloat16(half) {
303
+ const sign = (half >> 15) & 1;
304
+ const exp = (half >> 10) & 0x1f;
305
+ const mant = half & 0x3ff;
306
+ let val;
307
+ if (exp === 0) {
308
+ val = mant === 0 ? 0 : Math.pow(2, -14) * (mant / 1024);
309
+ } else if (exp === 31) {
310
+ val = mant === 0 ? Infinity : NaN;
311
+ } else {
312
+ val = Math.pow(2, exp - 15) * (1 + mant / 1024);
313
+ }
314
+ return sign ? -val : val;
315
+ }
316
+
317
+ // ── Length-prefixed framing ───────────────────────────────────────────
318
+
319
+ /**
320
+ * Frame a CBOR-encoded message with a 4-byte big-endian length prefix.
321
+ * @param {*} value - JS value to encode
322
+ * @returns {Uint8Array} [4-byte length][CBOR payload]
323
+ */
324
+ export function frameEncode(value) {
325
+ const payload = cborEncode(value);
326
+ const frame = new Uint8Array(4 + payload.length);
327
+ const view = new DataView(frame.buffer);
328
+ view.setUint32(0, payload.length);
329
+ frame.set(payload, 4);
330
+ return frame;
331
+ }
332
+
333
+ /**
334
+ * Streaming frame decoder. Feed it chunks and it yields complete messages.
335
+ */
336
+ export class FrameDecoder {
337
+ #buffer = new Uint8Array(0);
338
+
339
+ /**
340
+ * Feed bytes and return decoded messages.
341
+ * @param {Uint8Array} chunk
342
+ * @returns {Array} decoded JS values
343
+ */
344
+ feed(chunk) {
345
+ this.#buffer = appendBuffer(this.#buffer, chunk);
346
+ const messages = [];
347
+ while (this.#buffer.length >= 4) {
348
+ const view = new DataView(this.#buffer.buffer, this.#buffer.byteOffset, 4);
349
+ const len = view.getUint32(0);
350
+ if (this.#buffer.length < 4 + len) break;
351
+ const payload = this.#buffer.slice(4, 4 + len);
352
+ this.#buffer = this.#buffer.slice(4 + len);
353
+ messages.push(cborDecode(payload));
354
+ }
355
+ return messages;
356
+ }
357
+
358
+ /** Reset internal buffer. */
359
+ reset() {
360
+ this.#buffer = new Uint8Array(0);
361
+ }
362
+
363
+ /** Bytes remaining in buffer. */
364
+ get pending() {
365
+ return this.#buffer.length;
366
+ }
367
+ }
368
+
369
+ // ── Helpers ───────────────────────────────────────────────────────────
370
+
371
+ function concat(arrays) {
372
+ let total = 0;
373
+ for (const a of arrays) total += a.length;
374
+ const result = new Uint8Array(total);
375
+ let offset = 0;
376
+ for (const a of arrays) {
377
+ result.set(a, offset);
378
+ offset += a.length;
379
+ }
380
+ return result;
381
+ }
382
+
383
+ function appendBuffer(existing, chunk) {
384
+ if (existing.length === 0) return chunk;
385
+ const result = new Uint8Array(existing.length + chunk.length);
386
+ result.set(existing, 0);
387
+ result.set(chunk, existing.length);
388
+ return result;
389
+ }