@btoon/js 0.0.0-stage → 0.1.1
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/LICENSE +21 -0
- package/README.md +328 -2
- package/dist/cjs/constants.d.ts +34 -0
- package/dist/cjs/constants.js +32 -0
- package/dist/cjs/core.d.ts +15 -0
- package/dist/cjs/core.js +275 -0
- package/dist/cjs/decode.d.ts +10 -0
- package/dist/cjs/decode.js +554 -0
- package/dist/cjs/encode.d.ts +2 -0
- package/dist/cjs/encode.js +946 -0
- package/dist/cjs/errors.d.ts +53 -0
- package/dist/cjs/errors.js +45 -0
- package/dist/cjs/index.d.ts +68 -0
- package/dist/cjs/index.js +86 -0
- package/dist/cjs/inflate.d.ts +9 -0
- package/dist/cjs/inflate.js +353 -0
- package/dist/cjs/node.d.ts +64 -0
- package/dist/cjs/node.js +82 -0
- package/dist/cjs/options.d.ts +95 -0
- package/dist/cjs/options.js +128 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/platform-node.d.ts +3 -0
- package/dist/cjs/platform-node.js +132 -0
- package/dist/cjs/platform-portable.d.ts +7 -0
- package/dist/cjs/platform-portable.js +16 -0
- package/dist/cjs/sha256.d.ts +5 -0
- package/dist/cjs/sha256.js +141 -0
- package/dist/cjs/utf8.d.ts +19 -0
- package/dist/cjs/utf8.js +107 -0
- package/dist/cjs/values.d.ts +58 -0
- package/dist/cjs/values.js +129 -0
- package/dist/cjs/web.d.ts +25 -0
- package/dist/cjs/web.js +151 -0
- package/dist/esm/constants.d.ts +34 -0
- package/dist/esm/constants.js +29 -0
- package/dist/esm/core.d.ts +15 -0
- package/dist/esm/core.js +269 -0
- package/dist/esm/decode.d.ts +10 -0
- package/dist/esm/decode.js +549 -0
- package/dist/esm/encode.d.ts +2 -0
- package/dist/esm/encode.js +943 -0
- package/dist/esm/errors.d.ts +53 -0
- package/dist/esm/errors.js +39 -0
- package/dist/esm/index.d.ts +68 -0
- package/dist/esm/index.js +74 -0
- package/dist/esm/inflate.d.ts +9 -0
- package/dist/esm/inflate.js +349 -0
- package/dist/esm/node.d.ts +64 -0
- package/dist/esm/node.js +70 -0
- package/dist/esm/options.d.ts +95 -0
- package/dist/esm/options.js +124 -0
- package/dist/esm/platform-node.d.ts +3 -0
- package/dist/esm/platform-node.js +96 -0
- package/dist/esm/platform-portable.d.ts +7 -0
- package/dist/esm/platform-portable.js +13 -0
- package/dist/esm/sha256.d.ts +5 -0
- package/dist/esm/sha256.js +136 -0
- package/dist/esm/utf8.d.ts +19 -0
- package/dist/esm/utf8.js +99 -0
- package/dist/esm/values.d.ts +58 -0
- package/dist/esm/values.js +120 -0
- package/dist/esm/web.d.ts +25 -0
- package/dist/esm/web.js +143 -0
- package/package.json +85 -4
package/dist/cjs/core.js
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.encodeWith = encodeWith;
|
|
4
|
+
exports.decodeWith = decodeWith;
|
|
5
|
+
exports.encodeAsyncWith = encodeAsyncWith;
|
|
6
|
+
exports.decodeAsyncWith = decodeAsyncWith;
|
|
7
|
+
// encode/decode with the compression (SPEC §6) and signed (SPEC §8) envelopes, layered as
|
|
8
|
+
// sign(compress(encode(value))). The entry points bind these to a Platform: the set of
|
|
9
|
+
// synchronous codecs available in that environment.
|
|
10
|
+
const constants_js_1 = require("./constants.js");
|
|
11
|
+
const decode_js_1 = require("./decode.js");
|
|
12
|
+
const encode_js_1 = require("./encode.js");
|
|
13
|
+
const errors_js_1 = require("./errors.js");
|
|
14
|
+
const options_js_1 = require("./options.js");
|
|
15
|
+
const values_js_1 = require("./values.js");
|
|
16
|
+
const web_js_1 = require("./web.js");
|
|
17
|
+
const COMPRESSED_MAGIC = [0x42, 0x54, 0x4f, 0x4e]; // "BTON"
|
|
18
|
+
const SIGNED_MAGIC = [0x42, 0x54, 0x53, 0x47]; // "BTSG"
|
|
19
|
+
const SIGNED_HEADER = new Uint8Array([0x42, 0x54, 0x53, 0x47, 1, 1, 0, 0]);
|
|
20
|
+
const objectToString = Object.prototype.toString;
|
|
21
|
+
function hasMagic(b, magic) {
|
|
22
|
+
return b.length >= 4 && b[0] === magic[0] && b[1] === magic[1] && b[2] === magic[2] && b[3] === magic[3];
|
|
23
|
+
}
|
|
24
|
+
function inputBytes(data) {
|
|
25
|
+
if (ArrayBuffer.isView(data))
|
|
26
|
+
return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
27
|
+
const tag = objectToString.call(data);
|
|
28
|
+
if (tag === '[object ArrayBuffer]' || tag === '[object SharedArrayBuffer]')
|
|
29
|
+
return new Uint8Array(data);
|
|
30
|
+
throw (0, errors_js_1.fail)('invalid_argument', 'decode() expects a Uint8Array, ArrayBuffer or other ArrayBufferView');
|
|
31
|
+
}
|
|
32
|
+
function isShared(b) {
|
|
33
|
+
return typeof SharedArrayBuffer !== 'undefined' && b.buffer instanceof SharedArrayBuffer;
|
|
34
|
+
}
|
|
35
|
+
function isThenable(v) {
|
|
36
|
+
return (typeof v === 'object' || typeof v === 'function') && v !== null && typeof v.then === 'function';
|
|
37
|
+
}
|
|
38
|
+
function messageOf(e) {
|
|
39
|
+
return e instanceof Error ? e.message || e.name : String(e);
|
|
40
|
+
}
|
|
41
|
+
// --- compression envelope -----------------------------------------------------------------
|
|
42
|
+
function envelopeIfSmaller(raw, payload, alg) {
|
|
43
|
+
// SPEC §6: only when it saves at least 5%, i.e. 20 × envelope_size ≤ 19 × input_size.
|
|
44
|
+
if (20 * (constants_js_1.COMPRESSION_HEADER_SIZE + payload.length) > 19 * raw.length)
|
|
45
|
+
return raw;
|
|
46
|
+
const out = new Uint8Array(constants_js_1.COMPRESSION_HEADER_SIZE + payload.length);
|
|
47
|
+
out.set(COMPRESSED_MAGIC, 0);
|
|
48
|
+
out[4] = 1;
|
|
49
|
+
out[5] = constants_js_1.ALGORITHM_IDS[alg];
|
|
50
|
+
const view = new DataView(out.buffer);
|
|
51
|
+
view.setUint32(8, payload.length);
|
|
52
|
+
view.setUint32(12, raw.length);
|
|
53
|
+
out.set(payload, constants_js_1.COMPRESSION_HEADER_SIZE);
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
/** Checks the header of a compression envelope (before any decompression), or returns null. */
|
|
57
|
+
function compressionEnvelope(b, cfg) {
|
|
58
|
+
if (b.length < constants_js_1.COMPRESSION_HEADER_SIZE || !hasMagic(b, COMPRESSED_MAGIC))
|
|
59
|
+
return null;
|
|
60
|
+
if (!cfg.autoDecompress)
|
|
61
|
+
throw (0, errors_js_1.fail)('invalid_envelope', 'input is compressed but autoDecompress is disabled');
|
|
62
|
+
const view = new DataView(b.buffer, b.byteOffset, b.byteLength);
|
|
63
|
+
if (b[4] !== 1)
|
|
64
|
+
throw (0, errors_js_1.fail)('invalid_envelope', `unsupported compression envelope version ${b[4]}`);
|
|
65
|
+
if (view.getUint16(6) !== 0)
|
|
66
|
+
throw (0, errors_js_1.fail)('invalid_envelope', 'compression envelope reserved bytes must be zero');
|
|
67
|
+
if (view.getUint32(8) !== b.length - constants_js_1.COMPRESSION_HEADER_SIZE) {
|
|
68
|
+
throw (0, errors_js_1.fail)('invalid_envelope', 'compressed size does not match the envelope length');
|
|
69
|
+
}
|
|
70
|
+
const size = view.getUint32(12);
|
|
71
|
+
if (size > cfg.maxDecompressedSize) {
|
|
72
|
+
throw (0, errors_js_1.fail)('size_limit', `declared uncompressed size ${size} exceeds maxDecompressedSize (${cfg.maxDecompressedSize})`);
|
|
73
|
+
}
|
|
74
|
+
const alg = constants_js_1.ALGORITHM_NAMES[b[5]];
|
|
75
|
+
if (alg === undefined)
|
|
76
|
+
throw (0, errors_js_1.fail)('unsupported_compression', `unknown compression algorithm id ${b[5]}`);
|
|
77
|
+
return { alg, payload: b.subarray(constants_js_1.COMPRESSION_HEADER_SIZE), size };
|
|
78
|
+
}
|
|
79
|
+
function checkSize(out, size, alg) {
|
|
80
|
+
if (out.length !== size)
|
|
81
|
+
throw (0, errors_js_1.fail)('invalid_envelope', `${alg} payload decompressed size does not match the header`);
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
function unavailable(alg, op, asyncWorks) {
|
|
85
|
+
const fn = op === 'compression' ? 'encodeAsync()' : 'decodeAsync()';
|
|
86
|
+
return (0, errors_js_1.fail)('unsupported_compression', asyncWorks
|
|
87
|
+
? `${alg} ${op} is not available synchronously in this environment; use ${fn} or pass options.codecs.${alg}`
|
|
88
|
+
: `${alg} ${op} is not available in this environment; pass options.codecs.${alg}`);
|
|
89
|
+
}
|
|
90
|
+
function codecResult(r, alg, fn) {
|
|
91
|
+
if (!(0, values_js_1.isUint8Array)(r))
|
|
92
|
+
throw (0, errors_js_1.fail)('invalid_argument', `codecs.${alg}.${fn} must return a Uint8Array`);
|
|
93
|
+
return (0, decode_js_1.plainBytes)(r);
|
|
94
|
+
}
|
|
95
|
+
function codecError(e, alg, fn) {
|
|
96
|
+
if (e instanceof errors_js_1.BtoonError)
|
|
97
|
+
return e;
|
|
98
|
+
return (0, errors_js_1.fail)(fn === 'decompress' ? 'invalid_envelope' : 'invalid_argument', `codecs.${alg}.${fn} failed: ${messageOf(e)}`, e);
|
|
99
|
+
}
|
|
100
|
+
function callCodecSync(codecs, alg, fn, args) {
|
|
101
|
+
const codec = codecs?.[alg];
|
|
102
|
+
const f = codec?.[fn];
|
|
103
|
+
if (f === undefined)
|
|
104
|
+
return undefined;
|
|
105
|
+
let r;
|
|
106
|
+
try {
|
|
107
|
+
r = f.apply(codec, args);
|
|
108
|
+
}
|
|
109
|
+
catch (e) {
|
|
110
|
+
throw codecError(e, alg, fn);
|
|
111
|
+
}
|
|
112
|
+
if (isThenable(r)) {
|
|
113
|
+
Promise.resolve(r).catch(() => undefined);
|
|
114
|
+
throw (0, errors_js_1.fail)('invalid_argument', `codecs.${alg}.${fn} returned a Promise; use the async API (encodeAsync/decodeAsync)`);
|
|
115
|
+
}
|
|
116
|
+
return codecResult(r, alg, fn);
|
|
117
|
+
}
|
|
118
|
+
async function callCodecAsync(codecs, alg, fn, args) {
|
|
119
|
+
const codec = codecs?.[alg];
|
|
120
|
+
const f = codec?.[fn];
|
|
121
|
+
if (f === undefined)
|
|
122
|
+
return undefined;
|
|
123
|
+
let r;
|
|
124
|
+
try {
|
|
125
|
+
r = await f.apply(codec, args);
|
|
126
|
+
}
|
|
127
|
+
catch (e) {
|
|
128
|
+
throw codecError(e, alg, fn);
|
|
129
|
+
}
|
|
130
|
+
return codecResult(r, alg, fn);
|
|
131
|
+
}
|
|
132
|
+
function hasCodec(codecs, alg, fn) {
|
|
133
|
+
return typeof codecs?.[alg]?.[fn] === 'function';
|
|
134
|
+
}
|
|
135
|
+
function signedEnvelope(b) {
|
|
136
|
+
if (!hasMagic(b, SIGNED_MAGIC))
|
|
137
|
+
throw (0, errors_js_1.fail)('authentication_failed', 'input is not a signed envelope');
|
|
138
|
+
if (b.length < constants_js_1.SIGNED_HEADER_SIZE)
|
|
139
|
+
throw (0, errors_js_1.fail)('invalid_envelope', 'signed envelope is truncated');
|
|
140
|
+
if (b[4] !== 1 || b[5] !== 1 || b[6] !== 0 || b[7] !== 0) {
|
|
141
|
+
throw (0, errors_js_1.fail)('invalid_envelope', 'unsupported signed envelope version or algorithm');
|
|
142
|
+
}
|
|
143
|
+
return { header: b.subarray(0, 8), mac: b.subarray(8, constants_js_1.SIGNED_HEADER_SIZE), payload: b.subarray(constants_js_1.SIGNED_HEADER_SIZE) };
|
|
144
|
+
}
|
|
145
|
+
function rejectUnexpectedSignature(b) {
|
|
146
|
+
if (b.length >= constants_js_1.SIGNED_HEADER_SIZE && hasMagic(b, SIGNED_MAGIC)) {
|
|
147
|
+
throw (0, errors_js_1.fail)('authentication_failed', 'input is signed but no hmacKey was provided');
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function assembleSigned(mac, payload) {
|
|
151
|
+
const out = new Uint8Array(constants_js_1.SIGNED_HEADER_SIZE + payload.length);
|
|
152
|
+
out.set(SIGNED_HEADER, 0);
|
|
153
|
+
out.set(mac, 8);
|
|
154
|
+
out.set(payload, constants_js_1.SIGNED_HEADER_SIZE);
|
|
155
|
+
return out;
|
|
156
|
+
}
|
|
157
|
+
function concat2(a, b) {
|
|
158
|
+
const out = new Uint8Array(a.length + b.length);
|
|
159
|
+
out.set(a, 0);
|
|
160
|
+
out.set(b, a.length);
|
|
161
|
+
return out;
|
|
162
|
+
}
|
|
163
|
+
// --- synchronous API ------------------------------------------------------------------------
|
|
164
|
+
function encodeWith(value, options, platform) {
|
|
165
|
+
const cfg = (0, options_js_1.encodeConfig)(options);
|
|
166
|
+
let out = (0, encode_js_1.encodeDocument)(value, cfg.maxDepth, cfg.autoTabular);
|
|
167
|
+
if (cfg.compress) {
|
|
168
|
+
const alg = cfg.algorithm;
|
|
169
|
+
const builtin = platform.compress[alg];
|
|
170
|
+
// Fail fast, even for documents too small to compress, so a missing codec shows up in testing.
|
|
171
|
+
if (!hasCodec(cfg.codecs, alg, 'compress') && builtin === undefined) {
|
|
172
|
+
throw unavailable(alg, 'compression', (0, web_js_1.webFormat)(alg, 'CompressionStream') !== undefined);
|
|
173
|
+
}
|
|
174
|
+
if (out.length >= constants_js_1.MIN_COMPRESSION_SIZE) {
|
|
175
|
+
if (out.length > 0xffffffff)
|
|
176
|
+
throw (0, errors_js_1.fail)('size_limit', 'document too large to compress');
|
|
177
|
+
const payload = callCodecSync(cfg.codecs, alg, 'compress', [out]) ?? builtin(out);
|
|
178
|
+
out = envelopeIfSmaller(out, payload, alg);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (cfg.hmacKey !== undefined)
|
|
182
|
+
out = assembleSigned(platform.hmac(cfg.hmacKey, [SIGNED_HEADER, out]), out);
|
|
183
|
+
return out;
|
|
184
|
+
}
|
|
185
|
+
function decodeWith(data, options, platform) {
|
|
186
|
+
const cfg = (0, options_js_1.decodeConfig)(options);
|
|
187
|
+
let bytes = inputBytes(data);
|
|
188
|
+
if (cfg.hmacKey !== undefined) {
|
|
189
|
+
if (isShared(bytes))
|
|
190
|
+
bytes = bytes.slice(); // verify and parse the same bytes
|
|
191
|
+
const env = signedEnvelope(bytes);
|
|
192
|
+
const expected = platform.hmac(cfg.hmacKey, [env.header, env.payload]);
|
|
193
|
+
if (!platform.equal(expected, env.mac))
|
|
194
|
+
throw (0, errors_js_1.fail)('authentication_failed', 'HMAC verification failed');
|
|
195
|
+
bytes = env.payload;
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
rejectUnexpectedSignature(bytes);
|
|
199
|
+
}
|
|
200
|
+
const env = compressionEnvelope(bytes, cfg);
|
|
201
|
+
if (env !== null) {
|
|
202
|
+
const { alg, payload, size } = env;
|
|
203
|
+
let out = callCodecSync(cfg.codecs, alg, 'decompress', [payload, size]);
|
|
204
|
+
if (out === undefined) {
|
|
205
|
+
const builtin = platform.decompress[alg];
|
|
206
|
+
if (builtin === undefined)
|
|
207
|
+
throw unavailable(alg, 'decompression', (0, web_js_1.webFormat)(alg, 'DecompressionStream') !== undefined);
|
|
208
|
+
out = builtin(payload, size);
|
|
209
|
+
}
|
|
210
|
+
bytes = checkSize(out, size, alg);
|
|
211
|
+
}
|
|
212
|
+
return (0, decode_js_1.decodeDocument)(bytes, cfg);
|
|
213
|
+
}
|
|
214
|
+
// --- asynchronous API -------------------------------------------------------------------------
|
|
215
|
+
async function encodeAsyncWith(value, options, platform) {
|
|
216
|
+
const cfg = (0, options_js_1.encodeConfig)(options);
|
|
217
|
+
let out = (0, encode_js_1.encodeDocument)(value, cfg.maxDepth, cfg.autoTabular);
|
|
218
|
+
if (cfg.compress) {
|
|
219
|
+
const alg = cfg.algorithm;
|
|
220
|
+
const builtin = platform.compress[alg];
|
|
221
|
+
const format = (0, web_js_1.webFormat)(alg, 'CompressionStream');
|
|
222
|
+
if (!hasCodec(cfg.codecs, alg, 'compress') && format === undefined && builtin === undefined) {
|
|
223
|
+
throw unavailable(alg, 'compression', false);
|
|
224
|
+
}
|
|
225
|
+
if (out.length >= constants_js_1.MIN_COMPRESSION_SIZE) {
|
|
226
|
+
if (out.length > 0xffffffff)
|
|
227
|
+
throw (0, errors_js_1.fail)('size_limit', 'document too large to compress');
|
|
228
|
+
let payload = await callCodecAsync(cfg.codecs, alg, 'compress', [out]);
|
|
229
|
+
if (payload === undefined)
|
|
230
|
+
payload = format !== undefined ? await (0, web_js_1.webCompress)(format, out) : builtin(out);
|
|
231
|
+
out = envelopeIfSmaller(out, payload, alg);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (cfg.hmacKey !== undefined) {
|
|
235
|
+
const s = (0, web_js_1.subtle)();
|
|
236
|
+
const mac = s !== undefined ? await (0, web_js_1.webHmacSign)(s, cfg.hmacKey, concat2(SIGNED_HEADER, out)) : platform.hmac(cfg.hmacKey, [SIGNED_HEADER, out]);
|
|
237
|
+
out = assembleSigned(mac, out);
|
|
238
|
+
}
|
|
239
|
+
return out;
|
|
240
|
+
}
|
|
241
|
+
async function decodeAsyncWith(data, options, platform) {
|
|
242
|
+
const cfg = (0, options_js_1.decodeConfig)(options);
|
|
243
|
+
// A private copy: the caller may reuse its buffer while we wait.
|
|
244
|
+
let bytes = inputBytes(data).slice();
|
|
245
|
+
if (cfg.hmacKey !== undefined) {
|
|
246
|
+
const env = signedEnvelope(bytes);
|
|
247
|
+
const s = (0, web_js_1.subtle)();
|
|
248
|
+
const ok = s !== undefined
|
|
249
|
+
? await (0, web_js_1.webHmacVerify)(s, cfg.hmacKey, env.mac, concat2(env.header, env.payload))
|
|
250
|
+
: platform.equal(platform.hmac(cfg.hmacKey, [env.header, env.payload]), env.mac);
|
|
251
|
+
if (!ok)
|
|
252
|
+
throw (0, errors_js_1.fail)('authentication_failed', 'HMAC verification failed');
|
|
253
|
+
bytes = env.payload;
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
rejectUnexpectedSignature(bytes);
|
|
257
|
+
}
|
|
258
|
+
const env = compressionEnvelope(bytes, cfg);
|
|
259
|
+
if (env !== null) {
|
|
260
|
+
const { alg, payload, size } = env;
|
|
261
|
+
let out = await callCodecAsync(cfg.codecs, alg, 'decompress', [payload, size]);
|
|
262
|
+
if (out === undefined) {
|
|
263
|
+
const format = (0, web_js_1.webFormat)(alg, 'DecompressionStream');
|
|
264
|
+
const builtin = platform.decompress[alg];
|
|
265
|
+
if (format !== undefined)
|
|
266
|
+
out = await (0, web_js_1.webDecompress)(alg, format, payload, size);
|
|
267
|
+
else if (builtin !== undefined)
|
|
268
|
+
out = builtin(payload, size);
|
|
269
|
+
else
|
|
270
|
+
throw unavailable(alg, 'decompression', false);
|
|
271
|
+
}
|
|
272
|
+
bytes = checkSize(out, size, alg);
|
|
273
|
+
}
|
|
274
|
+
return (0, decode_js_1.decodeDocument)(bytes, cfg);
|
|
275
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { DecodeConfig } from './options.js';
|
|
2
|
+
/** A plain Uint8Array view of the same bytes (Buffer.prototype.slice does not copy). */
|
|
3
|
+
export declare function plainBytes(b: Uint8Array): Uint8Array;
|
|
4
|
+
/** Decodes one bare BTOON document (no envelopes). */
|
|
5
|
+
export declare function decodeDocument(bytes: Uint8Array, cfg: DecodeConfig): unknown;
|
|
6
|
+
/**
|
|
7
|
+
* Used by the encoder: an `Ext` that carries a BTOON-defined id must hold a payload a
|
|
8
|
+
* strict decoder accepts, so the encoder never emits a document decoders reject.
|
|
9
|
+
*/
|
|
10
|
+
export declare function validateBtoonExt(type: number, data: Uint8Array, depth: number, maxDepth?: number): void;
|