@btoon/js 0.0.0-stage → 0.1.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.
Files changed (64) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +328 -2
  3. package/dist/cjs/constants.d.ts +34 -0
  4. package/dist/cjs/constants.js +32 -0
  5. package/dist/cjs/core.d.ts +15 -0
  6. package/dist/cjs/core.js +275 -0
  7. package/dist/cjs/decode.d.ts +10 -0
  8. package/dist/cjs/decode.js +554 -0
  9. package/dist/cjs/encode.d.ts +2 -0
  10. package/dist/cjs/encode.js +946 -0
  11. package/dist/cjs/errors.d.ts +53 -0
  12. package/dist/cjs/errors.js +45 -0
  13. package/dist/cjs/index.d.ts +68 -0
  14. package/dist/cjs/index.js +86 -0
  15. package/dist/cjs/inflate.d.ts +9 -0
  16. package/dist/cjs/inflate.js +353 -0
  17. package/dist/cjs/node.d.ts +64 -0
  18. package/dist/cjs/node.js +82 -0
  19. package/dist/cjs/options.d.ts +95 -0
  20. package/dist/cjs/options.js +128 -0
  21. package/dist/cjs/package.json +3 -0
  22. package/dist/cjs/platform-node.d.ts +3 -0
  23. package/dist/cjs/platform-node.js +132 -0
  24. package/dist/cjs/platform-portable.d.ts +7 -0
  25. package/dist/cjs/platform-portable.js +16 -0
  26. package/dist/cjs/sha256.d.ts +5 -0
  27. package/dist/cjs/sha256.js +141 -0
  28. package/dist/cjs/utf8.d.ts +19 -0
  29. package/dist/cjs/utf8.js +107 -0
  30. package/dist/cjs/values.d.ts +58 -0
  31. package/dist/cjs/values.js +129 -0
  32. package/dist/cjs/web.d.ts +25 -0
  33. package/dist/cjs/web.js +151 -0
  34. package/dist/esm/constants.d.ts +34 -0
  35. package/dist/esm/constants.js +29 -0
  36. package/dist/esm/core.d.ts +15 -0
  37. package/dist/esm/core.js +269 -0
  38. package/dist/esm/decode.d.ts +10 -0
  39. package/dist/esm/decode.js +549 -0
  40. package/dist/esm/encode.d.ts +2 -0
  41. package/dist/esm/encode.js +943 -0
  42. package/dist/esm/errors.d.ts +53 -0
  43. package/dist/esm/errors.js +39 -0
  44. package/dist/esm/index.d.ts +68 -0
  45. package/dist/esm/index.js +74 -0
  46. package/dist/esm/inflate.d.ts +9 -0
  47. package/dist/esm/inflate.js +349 -0
  48. package/dist/esm/node.d.ts +64 -0
  49. package/dist/esm/node.js +70 -0
  50. package/dist/esm/options.d.ts +95 -0
  51. package/dist/esm/options.js +124 -0
  52. package/dist/esm/platform-node.d.ts +3 -0
  53. package/dist/esm/platform-node.js +96 -0
  54. package/dist/esm/platform-portable.d.ts +7 -0
  55. package/dist/esm/platform-portable.js +13 -0
  56. package/dist/esm/sha256.d.ts +5 -0
  57. package/dist/esm/sha256.js +136 -0
  58. package/dist/esm/utf8.d.ts +19 -0
  59. package/dist/esm/utf8.js +99 -0
  60. package/dist/esm/values.d.ts +58 -0
  61. package/dist/esm/values.js +120 -0
  62. package/dist/esm/web.d.ts +25 -0
  63. package/dist/esm/web.js +143 -0
  64. package/package.json +85 -4
@@ -0,0 +1,70 @@
1
+ /**
2
+ * BTOON for JavaScript and TypeScript — Node.js entry point (also used by Bun). Same API
3
+ * as the portable entry; synchronous compression and signing use `node:zlib` (zlib,
4
+ * Brotli, and Zstandard where Node.js has it) and `node:crypto`.
5
+ *
6
+ * @packageDocumentation
7
+ */
8
+ import { decodeAsyncWith, decodeWith, encodeAsyncWith, encodeWith } from './core.js';
9
+ import { nodePlatform } from './platform-node.js';
10
+ export { SPEC_VERSION, VERSION } from './constants.js';
11
+ export { BtoonError } from './errors.js';
12
+ export { Ext, Timestamp } from './values.js';
13
+ /**
14
+ * Encodes a value as canonical BTOON (SPEC §4): identical values produce identical bytes
15
+ * in every conforming implementation.
16
+ *
17
+ * Accepted values: `null`/`undefined` (nil), booleans, numbers (safe integers as integers,
18
+ * everything else — including `-0`, `NaN` and integral values beyond ±2^53 — as float64),
19
+ * `bigint`, strings (must not contain lone surrogates), `Uint8Array`/`ArrayBuffer`/other
20
+ * typed arrays and `DataView` (binary), `Float32Array`/`Float64Array` (float vectors),
21
+ * arrays, plain objects and `Map`s with string keys (maps, keys sorted by UTF-8 bytes),
22
+ * `Date` and {@link Timestamp} (timestamps), and {@link Ext}. Any other object is encoded
23
+ * the way `JSON.stringify` sees it: the result of its `toJSON()` method if it has one
24
+ * (called once), otherwise a map of its own enumerable string-keyed properties; `Number`,
25
+ * `String`, `Boolean` and `BigInt` wrapper objects are unwrapped. Functions and symbols are
26
+ * rejected. Arrays of uniform objects use the tabular encoding when it is smaller.
27
+ *
28
+ * With `compress` or `hmacKey`, the document is wrapped in a compression and/or signed
29
+ * envelope, layered as sign(compress(document)).
30
+ *
31
+ * @throws {BtoonError} `invalid_argument` for functions, symbols or invalid options, `max_depth`
32
+ * for values nested deeper than `maxDepth` (including cycles), `invalid_utf8` for strings
33
+ * with lone surrogates, `unsupported_compression` if the algorithm has no synchronous
34
+ * implementation here (see `encodeAsync` and `options.codecs`).
35
+ */
36
+ export function encode(value, options) {
37
+ return encodeWith(value, options, nodePlatform);
38
+ }
39
+ /**
40
+ * Decodes one BTOON document, enforcing SPEC §7: bounds-checked reads, a nesting limit,
41
+ * no allocation beyond what the input justifies, strict UTF-8, unique map keys and no
42
+ * trailing bytes. Compression envelopes are decompressed (with the output capped at the
43
+ * declared size and `maxDecompressedSize`); with `hmacKey` the signature is verified before
44
+ * anything is parsed.
45
+ *
46
+ * Maps decode to plain objects. A `"__proto__"` key becomes an own property (as with
47
+ * `JSON.parse`); it never changes an object's prototype.
48
+ *
49
+ * @throws {BtoonError} with the SPEC §7 error code describing the problem.
50
+ */
51
+ export function decode(data, options) {
52
+ return decodeWith(data, options, nodePlatform);
53
+ }
54
+ /**
55
+ * Like {@link encode}, but compresses with `CompressionStream` (when the runtime supports
56
+ * the algorithm) and signs with WebCrypto, falling back to the synchronous implementations.
57
+ * Rejects with a {@link BtoonError}.
58
+ */
59
+ export function encodeAsync(value, options) {
60
+ return encodeAsyncWith(value, options, nodePlatform);
61
+ }
62
+ /**
63
+ * Like {@link decode}, but decompresses with `DecompressionStream` (when the runtime
64
+ * supports the algorithm) and verifies signatures with `crypto.subtle.verify`, falling back
65
+ * to the synchronous implementations. The input is copied first, so the caller may reuse
66
+ * its buffer immediately. Rejects with a {@link BtoonError}.
67
+ */
68
+ export function decodeAsync(data, options) {
69
+ return decodeAsyncWith(data, options, nodePlatform);
70
+ }
@@ -0,0 +1,95 @@
1
+ /** Compression algorithms of the envelope (SPEC §6). Only `zlib` is mandatory. */
2
+ export type CompressionAlgorithm = 'zlib' | 'lz4' | 'zstd' | 'brotli' | 'snappy';
3
+ /**
4
+ * A user-supplied compression codec (see {@link CodecRegistry}). Either function may be
5
+ * synchronous or return a Promise; synchronous `encode`/`decode` accept only synchronous
6
+ * results.
7
+ */
8
+ export interface Codec {
9
+ /**
10
+ * Compress a whole document. The output must be the payload format SPEC §6 requires for
11
+ * the algorithm: an RFC 1950 zlib stream, an LZ4 *block* (no frame), a Zstandard frame,
12
+ * a Brotli stream, or raw Snappy.
13
+ */
14
+ compress?(data: Uint8Array): Uint8Array | Promise<Uint8Array>;
15
+ /**
16
+ * Decompress a payload. `uncompressedSize` is the size declared by the envelope, already
17
+ * checked against `maxDecompressedSize`; implementations must not produce more than that
18
+ * (LZ4 block decoders need it as the output size). The result must be exactly that long.
19
+ */
20
+ decompress?(data: Uint8Array, uncompressedSize: number): Uint8Array | Promise<Uint8Array>;
21
+ }
22
+ /**
23
+ * Compression codecs supplied by the caller, keyed by algorithm. A codec here takes
24
+ * precedence over the built-in implementations, so this is how to add LZ4 or Snappy, or
25
+ * synchronous zlib compression in browsers (for example with `fflate`).
26
+ */
27
+ export type CodecRegistry = {
28
+ readonly [A in CompressionAlgorithm]?: Codec;
29
+ };
30
+ /** Options of `encode` / `encodeAsync`. */
31
+ export interface EncodeOptions {
32
+ /** Wrap the document in a compression envelope (SPEC §6). Default `false`. */
33
+ compress?: boolean;
34
+ /** Algorithm used when `compress` is true. Default `'zlib'`. Has no effect unless `compress` is true. */
35
+ compression?: CompressionAlgorithm;
36
+ /** Use the tabular (columnar) encoding where SPEC §5.6.2's size rule says so. Default `true`. */
37
+ autoTabular?: boolean;
38
+ /** Maximum container nesting; deeper or cyclic values throw `max_depth`. Default `128`. */
39
+ maxDepth?: number;
40
+ /** Sign the output with HMAC-SHA256 (SPEC §8). A string is used as its UTF-8 bytes. Must not be empty. */
41
+ hmacKey?: Uint8Array | string;
42
+ /** Caller-supplied compression codecs. */
43
+ codecs?: CodecRegistry;
44
+ }
45
+ /** Options of `decode` / `decodeAsync`. */
46
+ export interface DecodeOptions {
47
+ /**
48
+ * SPEC §7 strict mode. Default `true`. Non-strict mode allows trailing bytes and
49
+ * duplicate keys (last wins) and replaces invalid UTF-8 with U+FFFD; bounds, depth and
50
+ * allocation checks are never relaxed.
51
+ */
52
+ strict?: boolean;
53
+ /** Maximum container nesting; deeper input throws `max_depth`. Default `128`. */
54
+ maxDepth?: number;
55
+ /** Largest accepted `uncompressed_size` of a compression envelope, in bytes. Default 256 MiB. */
56
+ maxDecompressedSize?: number;
57
+ /** Accept compression envelopes. Default `true`; when `false` they throw `invalid_envelope`. */
58
+ autoDecompress?: boolean;
59
+ /** Require a signed envelope and verify it (SPEC §8) before parsing. Must not be empty. */
60
+ hmacKey?: Uint8Array | string;
61
+ /**
62
+ * How timestamp (-1), date (-2) and datetime (-3) values decode. `'date'` (default):
63
+ * `Date` (millisecond precision, offset dropped; instants outside the range of `Date`
64
+ * become `Timestamp`). `'object'`: always {@link Timestamp}, with full precision.
65
+ */
66
+ timestamps?: 'date' | 'object';
67
+ /**
68
+ * `'auto'` (default): integers decode to `number` when they are safe integers and to
69
+ * `bigint` otherwise. `'always'`: integers written with a 64-bit marker (uint64/int64)
70
+ * or as a bigint extension always decode to `bigint`.
71
+ */
72
+ bigint?: 'auto' | 'always';
73
+ /** Caller-supplied compression codecs. */
74
+ codecs?: CodecRegistry;
75
+ }
76
+ export interface EncodeConfig {
77
+ compress: boolean;
78
+ algorithm: CompressionAlgorithm;
79
+ autoTabular: boolean;
80
+ maxDepth: number;
81
+ hmacKey: Uint8Array | undefined;
82
+ codecs: CodecRegistry | undefined;
83
+ }
84
+ export interface DecodeConfig {
85
+ strict: boolean;
86
+ maxDepth: number;
87
+ maxDecompressedSize: number;
88
+ autoDecompress: boolean;
89
+ hmacKey: Uint8Array | undefined;
90
+ timestampObjects: boolean;
91
+ bigintAlways: boolean;
92
+ codecs: CodecRegistry | undefined;
93
+ }
94
+ export declare function encodeConfig(options: EncodeOptions | undefined): EncodeConfig;
95
+ export declare function decodeConfig(options: DecodeOptions | undefined): DecodeConfig;
@@ -0,0 +1,124 @@
1
+ import { ALGORITHM_NAMES, DEFAULT_MAX_DECOMPRESSED_SIZE, DEFAULT_MAX_DEPTH } from './constants.js';
2
+ import { fail } from './errors.js';
3
+ import { isWellFormed } from './utf8.js';
4
+ import { isUint8Array } from './values.js';
5
+ const DEFAULT_ENCODE = Object.freeze({
6
+ compress: false,
7
+ algorithm: 'zlib',
8
+ autoTabular: true,
9
+ maxDepth: DEFAULT_MAX_DEPTH,
10
+ hmacKey: undefined,
11
+ codecs: undefined,
12
+ });
13
+ const DEFAULT_DECODE = Object.freeze({
14
+ strict: true,
15
+ maxDepth: DEFAULT_MAX_DEPTH,
16
+ maxDecompressedSize: DEFAULT_MAX_DECOMPRESSED_SIZE,
17
+ autoDecompress: true,
18
+ hmacKey: undefined,
19
+ timestampObjects: false,
20
+ bigintAlways: false,
21
+ codecs: undefined,
22
+ });
23
+ function optionsObject(options, what) {
24
+ if (options === undefined || options === null)
25
+ return undefined;
26
+ if (typeof options !== 'object')
27
+ throw fail('invalid_argument', `${what} options must be an object`);
28
+ return options;
29
+ }
30
+ function bool(o, name, dflt) {
31
+ const v = o[name];
32
+ if (v === undefined)
33
+ return dflt;
34
+ if (typeof v !== 'boolean')
35
+ throw fail('invalid_argument', `option ${name} must be a boolean`);
36
+ return v;
37
+ }
38
+ function count(o, name, dflt) {
39
+ const v = o[name];
40
+ if (v === undefined)
41
+ return dflt;
42
+ if (typeof v !== 'number' || !Number.isSafeInteger(v) || v < 0) {
43
+ throw fail('invalid_argument', `option ${name} must be a non-negative integer`);
44
+ }
45
+ return v;
46
+ }
47
+ function oneOf(o, name, allowed, dflt) {
48
+ const v = o[name];
49
+ if (v === undefined)
50
+ return dflt;
51
+ if (typeof v !== 'string' || !allowed.includes(v)) {
52
+ throw fail('invalid_argument', `option ${name} must be one of ${allowed.map((a) => `'${a}'`).join(', ')}`);
53
+ }
54
+ return v;
55
+ }
56
+ function hmacKey(o) {
57
+ const v = o.hmacKey;
58
+ if (v === undefined)
59
+ return undefined;
60
+ let key;
61
+ if (typeof v === 'string') {
62
+ if (!isWellFormed(v))
63
+ throw fail('invalid_argument', 'hmacKey string contains a lone surrogate');
64
+ key = new TextEncoder().encode(v);
65
+ }
66
+ else if (isUint8Array(v)) {
67
+ key = Uint8Array.prototype.slice.call(v); // private copy: immune to later mutation
68
+ }
69
+ else {
70
+ throw fail('invalid_argument', 'option hmacKey must be a Uint8Array or a string');
71
+ }
72
+ if (key.length === 0)
73
+ throw fail('invalid_argument', 'option hmacKey must not be empty');
74
+ return key;
75
+ }
76
+ function codecs(o) {
77
+ const v = o.codecs;
78
+ if (v === undefined)
79
+ return undefined;
80
+ if (typeof v !== 'object' || v === null)
81
+ throw fail('invalid_argument', 'option codecs must be an object');
82
+ for (const name of ALGORITHM_NAMES) {
83
+ const c = v[name];
84
+ if (c === undefined)
85
+ continue;
86
+ if (typeof c !== 'object' || c === null)
87
+ throw fail('invalid_argument', `codecs.${name} must be an object`);
88
+ for (const fn of ['compress', 'decompress']) {
89
+ const f = c[fn];
90
+ if (f !== undefined && typeof f !== 'function') {
91
+ throw fail('invalid_argument', `codecs.${name}.${fn} must be a function`);
92
+ }
93
+ }
94
+ }
95
+ return v;
96
+ }
97
+ export function encodeConfig(options) {
98
+ const o = optionsObject(options, 'encode');
99
+ if (o === undefined)
100
+ return DEFAULT_ENCODE;
101
+ return {
102
+ compress: bool(o, 'compress', false),
103
+ algorithm: oneOf(o, 'compression', ALGORITHM_NAMES, 'zlib'),
104
+ autoTabular: bool(o, 'autoTabular', true),
105
+ maxDepth: count(o, 'maxDepth', DEFAULT_MAX_DEPTH),
106
+ hmacKey: hmacKey(o),
107
+ codecs: codecs(o),
108
+ };
109
+ }
110
+ export function decodeConfig(options) {
111
+ const o = optionsObject(options, 'decode');
112
+ if (o === undefined)
113
+ return DEFAULT_DECODE;
114
+ return {
115
+ strict: bool(o, 'strict', true),
116
+ maxDepth: count(o, 'maxDepth', DEFAULT_MAX_DEPTH),
117
+ maxDecompressedSize: count(o, 'maxDecompressedSize', DEFAULT_MAX_DECOMPRESSED_SIZE),
118
+ autoDecompress: bool(o, 'autoDecompress', true),
119
+ hmacKey: hmacKey(o),
120
+ timestampObjects: oneOf(o, 'timestamps', ['date', 'object'], 'date') === 'object',
121
+ bigintAlways: oneOf(o, 'bigint', ['auto', 'always'], 'auto') === 'always',
122
+ codecs: codecs(o),
123
+ };
124
+ }
@@ -0,0 +1,3 @@
1
+ import type { Platform } from './core.js';
2
+ /** Node.js (and Bun): node:zlib for zlib, Brotli and Zstandard; node:crypto for HMAC. */
3
+ export declare const nodePlatform: Platform;
@@ -0,0 +1,96 @@
1
+ import { createHmac, timingSafeEqual } from 'node:crypto';
2
+ import * as zlib from 'node:zlib';
3
+ import { fail } from './errors.js';
4
+ import { adler32 } from './inflate.js';
5
+ function nodeFn(name) {
6
+ const f = zlib[name];
7
+ return typeof f === 'function' ? f : undefined;
8
+ }
9
+ function plain(b) {
10
+ return new Uint8Array(b.buffer, b.byteOffset, b.byteLength);
11
+ }
12
+ /**
13
+ * Wraps a node:zlib `*Sync` decompressor: output is capped at the declared size
14
+ * (`maxOutputLength`), and input left over after the end of the stream is rejected.
15
+ */
16
+ function decompressor(alg, fn, extra) {
17
+ if (fn === undefined)
18
+ return undefined;
19
+ return (data, size) => {
20
+ let r;
21
+ try {
22
+ r = fn(data, { ...extra?.(size), info: true, maxOutputLength: Math.max(1, size) });
23
+ }
24
+ catch (e) {
25
+ if (e.code === 'ERR_BUFFER_TOO_LARGE') {
26
+ throw fail('invalid_envelope', `${alg} payload decompresses to more than the declared size`, e);
27
+ }
28
+ throw fail('invalid_envelope', `${alg} payload is corrupt: ${e.message}`, e);
29
+ }
30
+ let out;
31
+ let consumed;
32
+ if (r instanceof Uint8Array) {
33
+ out = r;
34
+ }
35
+ else {
36
+ const info = r;
37
+ out = info.buffer;
38
+ consumed = info.engine?.bytesWritten;
39
+ }
40
+ if (typeof consumed === 'number') {
41
+ if (consumed !== data.length)
42
+ throw fail('invalid_envelope', `${alg} payload has trailing bytes`);
43
+ }
44
+ else if (alg === 'zlib') {
45
+ // A runtime that doesn't report consumed input: the stream must end in its Adler-32.
46
+ const n = data.length;
47
+ const trailer = n >= 4 ? ((data[n - 4] << 24) | (data[n - 3] << 16) | (data[n - 2] << 8) | data[n - 1]) >>> 0 : -1;
48
+ if (trailer !== adler32(out))
49
+ throw fail('invalid_envelope', 'zlib payload has trailing bytes');
50
+ }
51
+ return plain(out);
52
+ };
53
+ }
54
+ function compressor(fn, options) {
55
+ if (fn === undefined)
56
+ return undefined;
57
+ return (data) => plain(fn(data, options));
58
+ }
59
+ const decompress = {};
60
+ const compress = {};
61
+ decompress.zlib = decompressor('zlib', nodeFn('inflateSync'));
62
+ compress.zlib = compressor(nodeFn('deflateSync'), { level: 6 });
63
+ const brotliQuality = zlib.constants?.BROTLI_PARAM_QUALITY;
64
+ if (brotliQuality !== undefined) {
65
+ decompress.brotli = decompressor('brotli', nodeFn('brotliDecompressSync'));
66
+ compress.brotli = compressor(nodeFn('brotliCompressSync'), { params: { [brotliQuality]: 6 } });
67
+ }
68
+ // node:zlib has Zstandard from Node.js 22.15 / 23.8. SPEC §6: bound the decoder's memory
69
+ // by rejecting frames whose window exceeds max(8 MiB, min(uncompressed_size, 128 MiB)).
70
+ const zstdWindowLogMax = zlib.constants?.ZSTD_d_windowLogMax;
71
+ decompress.zstd = decompressor('zstd', nodeFn('zstdDecompressSync'), (size) => zstdWindowLogMax === undefined
72
+ ? {}
73
+ : { params: { [zstdWindowLogMax]: Math.ceil(Math.log2(Math.max(8 << 20, Math.min(size, 128 << 20)))) } });
74
+ compress.zstd = compressor(nodeFn('zstdCompressSync'), {});
75
+ for (const alg of Object.keys(decompress)) {
76
+ if (decompress[alg] === undefined)
77
+ delete decompress[alg];
78
+ }
79
+ for (const alg of Object.keys(compress)) {
80
+ if (compress[alg] === undefined)
81
+ delete compress[alg];
82
+ }
83
+ /** Node.js (and Bun): node:zlib for zlib, Brotli and Zstandard; node:crypto for HMAC. */
84
+ export const nodePlatform = {
85
+ decompress,
86
+ compress,
87
+ hmac(key, parts) {
88
+ const h = createHmac('sha256', key);
89
+ for (const p of parts)
90
+ h.update(p);
91
+ return plain(h.digest());
92
+ },
93
+ equal(a, b) {
94
+ return a.length === b.length && timingSafeEqual(a, b);
95
+ },
96
+ };
@@ -0,0 +1,7 @@
1
+ import type { Platform } from './core.js';
2
+ /**
3
+ * Environments without Node.js built-ins (browsers, Deno, workers): synchronous zlib
4
+ * decompression and HMAC-SHA256 in pure JavaScript. Synchronous compression needs a
5
+ * caller-supplied codec; encodeAsync uses CompressionStream instead.
6
+ */
7
+ export declare const portablePlatform: Platform;
@@ -0,0 +1,13 @@
1
+ import { inflateZlib } from './inflate.js';
2
+ import { hmacSha256, timingSafeEqual } from './sha256.js';
3
+ /**
4
+ * Environments without Node.js built-ins (browsers, Deno, workers): synchronous zlib
5
+ * decompression and HMAC-SHA256 in pure JavaScript. Synchronous compression needs a
6
+ * caller-supplied codec; encodeAsync uses CompressionStream instead.
7
+ */
8
+ export const portablePlatform = {
9
+ decompress: { zlib: inflateZlib },
10
+ compress: {},
11
+ hmac: hmacSha256,
12
+ equal: timingSafeEqual,
13
+ };
@@ -0,0 +1,5 @@
1
+ export declare function sha256(data: Uint8Array): Uint8Array;
2
+ /** HMAC-SHA256 over the concatenation of `parts` (no copy of the parts is made). */
3
+ export declare function hmacSha256(key: Uint8Array, parts: readonly Uint8Array[]): Uint8Array;
4
+ /** Compares two byte strings in time that depends only on their length. */
5
+ export declare function timingSafeEqual(a: Uint8Array, b: Uint8Array): boolean;
@@ -0,0 +1,136 @@
1
+ // SHA-256 (FIPS 180-4) and HMAC-SHA256 (RFC 2104) for runtimes without a synchronous
2
+ // native implementation. Used only for the signed envelope (SPEC §8).
3
+ const K = new Int32Array([
4
+ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98,
5
+ 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786,
6
+ 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8,
7
+ 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
8
+ 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819,
9
+ 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a,
10
+ 0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7,
11
+ 0xc67178f2,
12
+ ]);
13
+ const IV = [0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19];
14
+ class Sha256 {
15
+ constructor() {
16
+ this.h = new Int32Array(IV);
17
+ this.w = new Int32Array(64);
18
+ this.block = new Uint8Array(64);
19
+ this.blockLen = 0;
20
+ this.total = 0;
21
+ }
22
+ update(data) {
23
+ let i = 0;
24
+ const n = data.length;
25
+ this.total += n;
26
+ if (this.blockLen > 0) {
27
+ const take = Math.min(64 - this.blockLen, n);
28
+ this.block.set(data.subarray(0, take), this.blockLen);
29
+ this.blockLen += take;
30
+ i = take;
31
+ if (this.blockLen < 64)
32
+ return this;
33
+ this.compress(this.block, 0);
34
+ this.blockLen = 0;
35
+ }
36
+ for (; i + 64 <= n; i += 64)
37
+ this.compress(data, i);
38
+ if (i < n) {
39
+ this.block.set(data.subarray(i), 0);
40
+ this.blockLen = n - i;
41
+ }
42
+ return this;
43
+ }
44
+ digest() {
45
+ const bits = this.total * 8;
46
+ const block = this.block;
47
+ block[this.blockLen++] = 0x80;
48
+ if (this.blockLen > 56) {
49
+ block.fill(0, this.blockLen);
50
+ this.compress(block, 0);
51
+ this.blockLen = 0;
52
+ }
53
+ block.fill(0, this.blockLen, 56);
54
+ const view = new DataView(block.buffer);
55
+ view.setUint32(56, Math.floor(bits / 4294967296));
56
+ view.setUint32(60, bits >>> 0);
57
+ this.compress(block, 0);
58
+ const out = new Uint8Array(32);
59
+ const ov = new DataView(out.buffer);
60
+ for (let j = 0; j < 8; j++)
61
+ ov.setInt32(j * 4, this.h[j]);
62
+ return out;
63
+ }
64
+ compress(d, p) {
65
+ const w = this.w;
66
+ for (let j = 0; j < 16; j++, p += 4)
67
+ w[j] = (d[p] << 24) | (d[p + 1] << 16) | (d[p + 2] << 8) | d[p + 3];
68
+ for (let j = 16; j < 64; j++) {
69
+ const a = w[j - 15];
70
+ const b = w[j - 2];
71
+ const s0 = ((a >>> 7) | (a << 25)) ^ ((a >>> 18) | (a << 14)) ^ (a >>> 3);
72
+ const s1 = ((b >>> 17) | (b << 15)) ^ ((b >>> 19) | (b << 13)) ^ (b >>> 10);
73
+ w[j] = (w[j - 16] + s0 + w[j - 7] + s1) | 0;
74
+ }
75
+ const h = this.h;
76
+ let a = h[0];
77
+ let b = h[1];
78
+ let c = h[2];
79
+ let e = h[4];
80
+ let f = h[5];
81
+ let g = h[6];
82
+ let d0 = h[3];
83
+ let hh = h[7];
84
+ for (let j = 0; j < 64; j++) {
85
+ const S1 = ((e >>> 6) | (e << 26)) ^ ((e >>> 11) | (e << 21)) ^ ((e >>> 25) | (e << 7));
86
+ const ch = (e & f) ^ (~e & g);
87
+ const t1 = (hh + S1 + ch + K[j] + w[j]) | 0;
88
+ const S0 = ((a >>> 2) | (a << 30)) ^ ((a >>> 13) | (a << 19)) ^ ((a >>> 22) | (a << 10));
89
+ const maj = (a & b) ^ (a & c) ^ (b & c);
90
+ const t2 = (S0 + maj) | 0;
91
+ hh = g;
92
+ g = f;
93
+ f = e;
94
+ e = (d0 + t1) | 0;
95
+ d0 = c;
96
+ c = b;
97
+ b = a;
98
+ a = (t1 + t2) | 0;
99
+ }
100
+ h[0] = (h[0] + a) | 0;
101
+ h[1] = (h[1] + b) | 0;
102
+ h[2] = (h[2] + c) | 0;
103
+ h[3] = (h[3] + d0) | 0;
104
+ h[4] = (h[4] + e) | 0;
105
+ h[5] = (h[5] + f) | 0;
106
+ h[6] = (h[6] + g) | 0;
107
+ h[7] = (h[7] + hh) | 0;
108
+ }
109
+ }
110
+ export function sha256(data) {
111
+ return new Sha256().update(data).digest();
112
+ }
113
+ /** HMAC-SHA256 over the concatenation of `parts` (no copy of the parts is made). */
114
+ export function hmacSha256(key, parts) {
115
+ const k = key.length > 64 ? sha256(key) : key;
116
+ const ipad = new Uint8Array(64);
117
+ const opad = new Uint8Array(64);
118
+ for (let i = 0; i < 64; i++) {
119
+ const b = i < k.length ? k[i] : 0;
120
+ ipad[i] = b ^ 0x36;
121
+ opad[i] = b ^ 0x5c;
122
+ }
123
+ const inner = new Sha256().update(ipad);
124
+ for (const p of parts)
125
+ inner.update(p);
126
+ return new Sha256().update(opad).update(inner.digest()).digest();
127
+ }
128
+ /** Compares two byte strings in time that depends only on their length. */
129
+ export function timingSafeEqual(a, b) {
130
+ if (a.length !== b.length)
131
+ return false;
132
+ let diff = 0;
133
+ for (let i = 0; i < a.length; i++)
134
+ diff |= a[i] ^ b[i];
135
+ return diff === 0;
136
+ }
@@ -0,0 +1,19 @@
1
+ export declare const textEncoder: TextEncoder;
2
+ /** True if `s` has no lone surrogates, i.e. it can be encoded as UTF-8 losslessly. */
3
+ export declare function isWellFormed(s: string): boolean;
4
+ export declare function loneSurrogateError(): Error;
5
+ /** Exact UTF-8 byte length of `s`; throws `invalid_utf8` for a lone surrogate. */
6
+ export declare function utf8Length(s: string): number;
7
+ /**
8
+ * Orders strings by their UTF-8 bytes (SPEC §4.4), which equals code point order. Plain
9
+ * `<` compares UTF-16 code units, which puts astral characters (surrogate pairs,
10
+ * U+D800–U+DFFF) before U+E000–U+FFFF; this corrects exactly that case.
11
+ */
12
+ export declare function compareUtf8(a: string, b: string): number;
13
+ /**
14
+ * Decodes `length` bytes at `start`. Strict mode rejects ill-formed UTF-8 (RFC 3629:
15
+ * overlongs, surrogates, > U+10FFFF, truncated sequences) via TextDecoder's fatal mode;
16
+ * non-strict mode substitutes U+FFFD. Returns `undefined` for invalid input so the caller
17
+ * can report the position.
18
+ */
19
+ export declare function decodeUtf8(bytes: Uint8Array, start: number, length: number, strict: boolean): string | undefined;
@@ -0,0 +1,99 @@
1
+ import { fail } from './errors.js';
2
+ export const textEncoder = new TextEncoder();
3
+ // ignoreBOM: true keeps a leading U+FEFF instead of silently stripping it.
4
+ const fatalDecoder = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true });
5
+ const lenientDecoder = new TextDecoder('utf-8', { ignoreBOM: true });
6
+ const LONE_SURROGATE = /(?:^|[^\uD800-\uDBFF])[\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])/;
7
+ const HAS_SURROGATE = /[\uD800-\uDFFF]/;
8
+ const nativeIsWellFormed = typeof String.prototype.isWellFormed === 'function';
9
+ /** True if `s` has no lone surrogates, i.e. it can be encoded as UTF-8 losslessly. */
10
+ export function isWellFormed(s) {
11
+ if (nativeIsWellFormed)
12
+ return s.isWellFormed();
13
+ return !HAS_SURROGATE.test(s) || !LONE_SURROGATE.test(s);
14
+ }
15
+ export function loneSurrogateError() {
16
+ return fail('invalid_utf8', 'string contains a lone surrogate and cannot be encoded as UTF-8');
17
+ }
18
+ /** Exact UTF-8 byte length of `s`; throws `invalid_utf8` for a lone surrogate. */
19
+ export function utf8Length(s) {
20
+ const n = s.length;
21
+ let bytes = n;
22
+ for (let i = 0; i < n; i++) {
23
+ const c = s.charCodeAt(i);
24
+ if (c < 0x80)
25
+ continue;
26
+ if (c < 0x800) {
27
+ bytes += 1;
28
+ }
29
+ else if (c < 0xd800 || c > 0xdfff) {
30
+ bytes += 2;
31
+ }
32
+ else {
33
+ const d = c <= 0xdbff && i + 1 < n ? s.charCodeAt(i + 1) : 0;
34
+ if (d < 0xdc00 || d > 0xdfff)
35
+ throw loneSurrogateError();
36
+ i++;
37
+ bytes += 2; // 4 bytes for 2 code units
38
+ }
39
+ }
40
+ return bytes;
41
+ }
42
+ /**
43
+ * Orders strings by their UTF-8 bytes (SPEC §4.4), which equals code point order. Plain
44
+ * `<` compares UTF-16 code units, which puts astral characters (surrogate pairs,
45
+ * U+D800–U+DFFF) before U+E000–U+FFFF; this corrects exactly that case.
46
+ */
47
+ export function compareUtf8(a, b) {
48
+ if (a === b)
49
+ return 0;
50
+ const n = a.length < b.length ? a.length : b.length;
51
+ for (let i = 0; i < n; i++) {
52
+ const x = a.charCodeAt(i);
53
+ const y = b.charCodeAt(i);
54
+ if (x !== y) {
55
+ if (x >= 0xd800 && y >= 0xd800) {
56
+ const xs = x <= 0xdfff;
57
+ const ys = y <= 0xdfff;
58
+ if (xs !== ys)
59
+ return xs ? 1 : -1;
60
+ }
61
+ return x - y;
62
+ }
63
+ }
64
+ return a.length - b.length;
65
+ }
66
+ const fromCharCode = String.fromCharCode;
67
+ /**
68
+ * Decodes `length` bytes at `start`. Strict mode rejects ill-formed UTF-8 (RFC 3629:
69
+ * overlongs, surrogates, > U+10FFFF, truncated sequences) via TextDecoder's fatal mode;
70
+ * non-strict mode substitutes U+FFFD. Returns `undefined` for invalid input so the caller
71
+ * can report the position.
72
+ */
73
+ export function decodeUtf8(bytes, start, length, strict) {
74
+ if (length <= 32) {
75
+ // Short ASCII: one pass, no TextDecoder call.
76
+ let s = '';
77
+ let i = start;
78
+ const end = start + length;
79
+ for (; i < end; i++) {
80
+ const b = bytes[i];
81
+ if (b >= 0x80)
82
+ break;
83
+ s += fromCharCode(b);
84
+ }
85
+ if (i === end)
86
+ return s;
87
+ }
88
+ const view = bytes.subarray(start, start + length);
89
+ try {
90
+ return (strict ? fatalDecoder : lenientDecoder).decode(view);
91
+ }
92
+ catch (e) {
93
+ // The fatal decoder reports ill-formed input as a TypeError; anything else is the
94
+ // engine refusing to build a string that large.
95
+ if (strict && e instanceof TypeError)
96
+ return undefined;
97
+ throw fail('size_limit', 'string is too long for this JavaScript engine', e);
98
+ }
99
+ }