attenu-guard 0.1.0 → 0.2.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 (43) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +12 -4
  3. package/dist/cjs/audit.d.ts +1 -0
  4. package/dist/cjs/audit.d.ts.map +1 -1
  5. package/dist/cjs/audit.js +9 -5
  6. package/dist/cjs/audit.js.map +1 -1
  7. package/dist/cjs/canonical.d.ts +21 -69
  8. package/dist/cjs/canonical.d.ts.map +1 -1
  9. package/dist/cjs/canonical.js +198 -171
  10. package/dist/cjs/canonical.js.map +1 -1
  11. package/dist/cjs/evidence.d.ts +1 -0
  12. package/dist/cjs/evidence.d.ts.map +1 -1
  13. package/dist/cjs/evidence.js +8 -3
  14. package/dist/cjs/evidence.js.map +1 -1
  15. package/dist/cjs/index.d.ts +4 -4
  16. package/dist/cjs/index.d.ts.map +1 -1
  17. package/dist/cjs/index.js +14 -5
  18. package/dist/cjs/index.js.map +1 -1
  19. package/dist/cjs/wire.d.ts +136 -1
  20. package/dist/cjs/wire.d.ts.map +1 -1
  21. package/dist/cjs/wire.js +405 -2
  22. package/dist/cjs/wire.js.map +1 -1
  23. package/dist/esm/audit.d.ts +1 -0
  24. package/dist/esm/audit.d.ts.map +1 -1
  25. package/dist/esm/audit.js +10 -6
  26. package/dist/esm/audit.js.map +1 -1
  27. package/dist/esm/canonical.d.ts +21 -69
  28. package/dist/esm/canonical.d.ts.map +1 -1
  29. package/dist/esm/canonical.js +192 -168
  30. package/dist/esm/canonical.js.map +1 -1
  31. package/dist/esm/evidence.d.ts +1 -0
  32. package/dist/esm/evidence.d.ts.map +1 -1
  33. package/dist/esm/evidence.js +8 -3
  34. package/dist/esm/evidence.js.map +1 -1
  35. package/dist/esm/index.d.ts +4 -4
  36. package/dist/esm/index.d.ts.map +1 -1
  37. package/dist/esm/index.js +3 -3
  38. package/dist/esm/index.js.map +1 -1
  39. package/dist/esm/wire.d.ts +136 -1
  40. package/dist/esm/wire.d.ts.map +1 -1
  41. package/dist/esm/wire.js +400 -2
  42. package/dist/esm/wire.js.map +1 -1
  43. package/package.json +1 -1
@@ -1,30 +1,19 @@
1
- /**
2
- * canonical.ts — byte-exact interoperability with the Python implementation.
3
- *
4
- * Every hash, seal and signature in Attenu is taken over a canonical JSON
5
- * serialisation. The Python library produces those bytes with
6
- * `json.dumps(obj, sort_keys=True, separators=(",", ":"))`, whose defaults
7
- * include `ensure_ascii=True`. `JSON.stringify` differs from that in three
8
- * ways that would each silently break cross-language verification:
9
- *
10
- * 1. key order — `JSON.stringify` preserves insertion order, Python sorts;
11
- * 2. non-ASCII — Python escapes every code point above U+007E as `\uXXXX`
12
- * (astral characters as a surrogate pair), `JSON.stringify` emits them raw;
13
- * 3. numbers — Python distinguishes `int` from `float` (`100` vs `100.0`) and
14
- * switches to exponential notation at different magnitudes than JavaScript
15
- * (`1e-05` where `String()` gives `0.00001`).
16
- *
17
- * (1) and (2) are handled by `canonicalJson` below. (3) cannot be solved by a
18
- * serialiser alone, because a JavaScript `number` has forgotten whether the
19
- * document said `100` or `100.0`. So a document that is going to be re-hashed —
20
- * a ledger or a bundle produced elsewhere — is read with `parseJson`, which
21
- * keeps each number's original literal alongside its value as a `RawNumber`.
22
- * `canonicalJson` re-emits that literal verbatim, so a Python-written entry
23
- * hashes to the byte-identical digest here. Values created in this library are
24
- * plain numbers, and `pyNumber` formats them the way Python's `json` would, so
25
- * the reverse direction round-trips too.
26
- */
27
- /** A number as it was written in the source document, plus its parsed value. */
1
+ /** RFC 8785 JSON Canonicalization Scheme (JCS), with no runtime dependencies. */
2
+ export declare class CanonicalizationError extends Error {
3
+ constructor(message: string);
4
+ }
5
+ export declare class NonFiniteNumberError extends CanonicalizationError {
6
+ }
7
+ export declare class LoneSurrogateError extends CanonicalizationError {
8
+ }
9
+ export declare class UnsupportedTypeError extends CanonicalizationError {
10
+ }
11
+ /** Raised while parsing when an object repeats a member name. */
12
+ export declare class DuplicateMemberError extends SyntaxError {
13
+ readonly member: string;
14
+ constructor(member: string);
15
+ }
16
+ /** A parsed JSON number, retaining its source spelling for syntax-sensitive checks. */
28
17
  export declare class RawNumber {
29
18
  readonly raw: string;
30
19
  readonly value: number;
@@ -32,60 +21,23 @@ export declare class RawNumber {
32
21
  valueOf(): number;
33
22
  toJSON(): number;
34
23
  }
35
- /** A JSON value that may still carry original number literals. */
36
24
  export type CJson = null | boolean | number | string | RawNumber | CJson[] | {
37
25
  [key: string]: CJson;
38
26
  };
39
- /** A plain JSON value — what the rest of the library works with. */
40
27
  export type Json = null | boolean | number | string | Json[] | {
41
28
  [key: string]: Json;
42
29
  };
43
- /** Compare two strings by Unicode code point, as Python's `sorted` does. */
30
+ /** Compare by Unicode code point for semantic lists that mirror Python `sorted`. */
44
31
  export declare function compareCodePoints(a: string, b: string): number;
45
- /** Sort strings by code point (Python's `sorted(list_of_str)`). */
46
32
  export declare function sortedStrings(values: Iterable<string>): string[];
47
- /**
48
- * Format a number the way Python's `json.dumps` would.
49
- *
50
- * Integral values are emitted without a decimal point, matching a Python `int`.
51
- * A TypeScript number cannot record that it was meant to be the float `100.0`,
52
- * so such a value serialises as `100`; Python reads that back as an `int` and
53
- * re-emits `100`, which keeps the chain verifiable in both directions. Values
54
- * with a fractional part follow CPython's `repr`: shortest round-tripping
55
- * digits, exponential notation when the decimal exponent is below -4 or at
56
- * least 16, a two-digit signed exponent, and always a decimal point otherwise.
57
- */
33
+ /** Python-style numeric display used only in human-readable ceiling descriptions. */
58
34
  export declare function pyNumber(n: number): string;
59
- /** Escape a string the way Python's `json.dumps(ensure_ascii=True)` does. */
60
- export declare function pyString(s: string): string;
61
- /**
62
- * Serialise to the exact bytes Python's
63
- * `json.dumps(obj, sort_keys=True, separators=(",", ":"))` produces:
64
- * sorted keys, no incidental whitespace, ASCII-escaped strings.
65
- *
66
- * Object entries whose value is `undefined` are omitted, matching a Python dict
67
- * that simply never held the key. `null` is kept — it is a value.
68
- */
35
+ /** Return an RFC 8785 canonical JSON string. */
69
36
  export declare function canonicalJson(value: CJson | undefined): string;
70
- /**
71
- * Serialise with sorted keys and ASCII escaping, choosing the separators.
72
- *
73
- * `canonicalJson` uses the compact `(",", ":")` pair every hash is taken over.
74
- * The `.jsonl` ledger on disk is written with Python's DEFAULT separators
75
- * (`", "` and `": "`), so a line read back by either language re-serialises to
76
- * the same bytes its writer produced.
77
- */
78
- export declare function pyJsonDumps(value: CJson | undefined, itemSeparator?: string, keySeparator?: string): string;
79
- /** The canonical bytes — what every hash, seal and signature is taken over. */
37
+ /** The canonical UTF-8 bytes used by every hash, seal, and signature. */
80
38
  export declare function canonicalBytes(value: CJson | undefined): Buffer;
81
39
  /** Strip `RawNumber` wrappers, yielding ordinary JavaScript values. */
82
40
  export declare function toPlain<T = Json>(value: CJson | undefined): T;
83
- /**
84
- * Parse JSON, keeping every number's original literal.
85
- *
86
- * `JSON.parse` is not used: it discards the literal, and with it the ability to
87
- * reproduce the bytes the writer hashed. Objects preserve their key order for
88
- * readability; canonical serialisation sorts them anyway.
89
- */
41
+ /** Parse JSON without losing number spellings, while rejecting duplicates. */
90
42
  export declare function parseJson(text: string): CJson;
91
43
  //# sourceMappingURL=canonical.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"canonical.d.ts","sourceRoot":"","sources":["../../src/canonical.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,gFAAgF;AAChF,qBAAa,SAAS;IAElB,QAAQ,CAAC,GAAG,EAAE,MAAM;IACpB,QAAQ,CAAC,KAAK,EAAE,MAAM;gBADb,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,MAAM;IAExB,OAAO,IAAI,MAAM;IAGjB,MAAM,IAAI,MAAM;CAGjB;AAED,kEAAkE;AAClE,MAAM,MAAM,KAAK,GACb,IAAI,GACJ,OAAO,GACP,MAAM,GACN,MAAM,GACN,SAAS,GACT,KAAK,EAAE,GACP;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,KAAK,CAAA;CAAE,CAAC;AAE7B,oEAAoE;AACpE,MAAM,MAAM,IAAI,GAAG,IAAI,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,IAAI,EAAE,GAAG;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC;AAEvF,4EAA4E;AAC5E,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAU9D;AAED,mEAAmE;AACnE,wBAAgB,aAAa,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,MAAM,EAAE,CAEhE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,QAAQ,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAM1C;AA2BD,6EAA6E;AAC7E,wBAAgB,QAAQ,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAgB1C;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,KAAK,GAAG,SAAS,GAAG,MAAM,CAE9D;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CACzB,KAAK,EAAE,KAAK,GAAG,SAAS,EACxB,aAAa,SAAO,EACpB,YAAY,SAAO,GAClB,MAAM,CAwBR;AAED,+EAA+E;AAC/E,wBAAgB,cAAc,CAAC,KAAK,EAAE,KAAK,GAAG,SAAS,GAAG,MAAM,CAE/D;AAED,uEAAuE;AACvE,wBAAgB,OAAO,CAAC,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,GAAG,SAAS,GAAG,CAAC,CAS7D;AAED;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,KAAK,CAO7C"}
1
+ {"version":3,"file":"canonical.d.ts","sourceRoot":"","sources":["../../src/canonical.ts"],"names":[],"mappings":"AAAA,iFAAiF;AAEjF,qBAAa,qBAAsB,SAAQ,KAAK;gBAClC,OAAO,EAAE,MAAM;CAI5B;AAED,qBAAa,oBAAqB,SAAQ,qBAAqB;CAAG;AAClE,qBAAa,kBAAmB,SAAQ,qBAAqB;CAAG;AAChE,qBAAa,oBAAqB,SAAQ,qBAAqB;CAAG;AAElE,iEAAiE;AACjE,qBAAa,oBAAqB,SAAQ,WAAW;IACvC,QAAQ,CAAC,MAAM,EAAE,MAAM;gBAAd,MAAM,EAAE,MAAM;CAIpC;AAED,uFAAuF;AACvF,qBAAa,SAAS;IAElB,QAAQ,CAAC,GAAG,EAAE,MAAM;IACpB,QAAQ,CAAC,KAAK,EAAE,MAAM;gBADb,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,MAAM;IAExB,OAAO,IAAI,MAAM;IAGjB,MAAM,IAAI,MAAM;CAGjB;AAED,MAAM,MAAM,KAAK,GACb,IAAI,GACJ,OAAO,GACP,MAAM,GACN,MAAM,GACN,SAAS,GACT,KAAK,EAAE,GACP;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,KAAK,CAAA;CAAE,CAAC;AAE7B,MAAM,MAAM,IAAI,GAAG,IAAI,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,IAAI,EAAE,GAAG;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC;AAEvF,oFAAoF;AACpF,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAU9D;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,MAAM,EAAE,CAEhE;AAED,qFAAqF;AACrF,wBAAgB,QAAQ,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAsB1C;AAuGD,gDAAgD;AAChD,wBAAgB,aAAa,CAAC,KAAK,EAAE,KAAK,GAAG,SAAS,GAAG,MAAM,CAE9D;AAED,yEAAyE;AACzE,wBAAgB,cAAc,CAAC,KAAK,EAAE,KAAK,GAAG,SAAS,GAAG,MAAM,CAE/D;AAED,uEAAuE;AACvE,wBAAgB,OAAO,CAAC,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,GAAG,SAAS,GAAG,CAAC,CAgB7D;AAED,8EAA8E;AAC9E,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,KAAK,CAO7C"}
@@ -1,30 +1,26 @@
1
- /**
2
- * canonical.ts — byte-exact interoperability with the Python implementation.
3
- *
4
- * Every hash, seal and signature in Attenu is taken over a canonical JSON
5
- * serialisation. The Python library produces those bytes with
6
- * `json.dumps(obj, sort_keys=True, separators=(",", ":"))`, whose defaults
7
- * include `ensure_ascii=True`. `JSON.stringify` differs from that in three
8
- * ways that would each silently break cross-language verification:
9
- *
10
- * 1. key order — `JSON.stringify` preserves insertion order, Python sorts;
11
- * 2. non-ASCII — Python escapes every code point above U+007E as `\uXXXX`
12
- * (astral characters as a surrogate pair), `JSON.stringify` emits them raw;
13
- * 3. numbers — Python distinguishes `int` from `float` (`100` vs `100.0`) and
14
- * switches to exponential notation at different magnitudes than JavaScript
15
- * (`1e-05` where `String()` gives `0.00001`).
16
- *
17
- * (1) and (2) are handled by `canonicalJson` below. (3) cannot be solved by a
18
- * serialiser alone, because a JavaScript `number` has forgotten whether the
19
- * document said `100` or `100.0`. So a document that is going to be re-hashed —
20
- * a ledger or a bundle produced elsewhere — is read with `parseJson`, which
21
- * keeps each number's original literal alongside its value as a `RawNumber`.
22
- * `canonicalJson` re-emits that literal verbatim, so a Python-written entry
23
- * hashes to the byte-identical digest here. Values created in this library are
24
- * plain numbers, and `pyNumber` formats them the way Python's `json` would, so
25
- * the reverse direction round-trips too.
26
- */
27
- /** A number as it was written in the source document, plus its parsed value. */
1
+ /** RFC 8785 JSON Canonicalization Scheme (JCS), with no runtime dependencies. */
2
+ export class CanonicalizationError extends Error {
3
+ constructor(message) {
4
+ super(message);
5
+ this.name = new.target.name;
6
+ }
7
+ }
8
+ export class NonFiniteNumberError extends CanonicalizationError {
9
+ }
10
+ export class LoneSurrogateError extends CanonicalizationError {
11
+ }
12
+ export class UnsupportedTypeError extends CanonicalizationError {
13
+ }
14
+ /** Raised while parsing when an object repeats a member name. */
15
+ export class DuplicateMemberError extends SyntaxError {
16
+ member;
17
+ constructor(member) {
18
+ super(`duplicate JSON object member ${JSON.stringify(member)}`);
19
+ this.member = member;
20
+ this.name = "DuplicateMemberError";
21
+ }
22
+ }
23
+ /** A parsed JSON number, retaining its source spelling for syntax-sensitive checks. */
28
24
  export class RawNumber {
29
25
  raw;
30
26
  value;
@@ -39,7 +35,7 @@ export class RawNumber {
39
35
  return this.value;
40
36
  }
41
37
  }
42
- /** Compare two strings by Unicode code point, as Python's `sorted` does. */
38
+ /** Compare by Unicode code point for semantic lists that mirror Python `sorted`. */
43
39
  export function compareCodePoints(a, b) {
44
40
  const ca = Array.from(a);
45
41
  const cb = Array.from(b);
@@ -52,21 +48,10 @@ export function compareCodePoints(a, b) {
52
48
  }
53
49
  return ca.length - cb.length;
54
50
  }
55
- /** Sort strings by code point (Python's `sorted(list_of_str)`). */
56
51
  export function sortedStrings(values) {
57
52
  return Array.from(values).sort(compareCodePoints);
58
53
  }
59
- /**
60
- * Format a number the way Python's `json.dumps` would.
61
- *
62
- * Integral values are emitted without a decimal point, matching a Python `int`.
63
- * A TypeScript number cannot record that it was meant to be the float `100.0`,
64
- * so such a value serialises as `100`; Python reads that back as an `int` and
65
- * re-emits `100`, which keeps the chain verifiable in both directions. Values
66
- * with a fractional part follow CPython's `repr`: shortest round-tripping
67
- * digits, exponential notation when the decimal exponent is below -4 or at
68
- * least 16, a two-digit signed exponent, and always a decimal point otherwise.
69
- */
54
+ /** Python-style numeric display used only in human-readable ceiling descriptions. */
70
55
  export function pyNumber(n) {
71
56
  if (Number.isNaN(n))
72
57
  return "NaN";
@@ -76,22 +61,15 @@ export function pyNumber(n) {
76
61
  return "-0.0";
77
62
  if (Number.isInteger(n) && Math.abs(n) < 1e21)
78
63
  return n.toFixed(0);
79
- return pyFloatRepr(n);
80
- }
81
- function pyFloatRepr(n) {
82
- // `toExponential()` with no argument yields the shortest digit string that
83
- // uniquely identifies the value — the same "shortest round-trip" digits
84
- // CPython's repr produces. Only the layout around them differs.
85
64
  const parsed = /^(-?)(\d)(?:\.(\d+))?e([+-]\d+)$/.exec(n.toExponential());
86
65
  if (!parsed)
87
- return String(n); // unreachable for a finite double
66
+ return String(n);
88
67
  const sign = parsed[1];
89
68
  const digits = parsed[2] + (parsed[3] ?? "");
90
69
  const exp = parseInt(parsed[4], 10);
91
70
  if (exp < -4 || exp >= 16) {
92
71
  const mantissa = digits.length > 1 ? `${digits[0]}.${digits.slice(1)}` : digits;
93
- const expSign = exp < 0 ? "-" : "+";
94
- return `${sign}${mantissa}e${expSign}${String(Math.abs(exp)).padStart(2, "0")}`;
72
+ return `${sign}${mantissa}e${exp < 0 ? "-" : "+"}${String(Math.abs(exp)).padStart(2, "0")}`;
95
73
  }
96
74
  if (exp >= 0) {
97
75
  const body = digits.length > exp + 1
@@ -101,76 +79,112 @@ function pyFloatRepr(n) {
101
79
  }
102
80
  return `${sign}0.${"0".repeat(-exp - 1)}${digits}`;
103
81
  }
104
- /** Escape a string the way Python's `json.dumps(ensure_ascii=True)` does. */
105
- export function pyString(s) {
106
- let out = '"';
107
- for (let i = 0; i < s.length; i++) {
108
- const ch = s[i];
109
- const code = s.charCodeAt(i);
110
- if (ch === '"')
111
- out += '\\"';
112
- else if (ch === "\\")
113
- out += "\\\\";
114
- else if (code === 0x08)
115
- out += "\\b";
116
- else if (code === 0x09)
117
- out += "\\t";
118
- else if (code === 0x0a)
119
- out += "\\n";
120
- else if (code === 0x0c)
121
- out += "\\f";
122
- else if (code === 0x0d)
123
- out += "\\r";
124
- else if (code < 0x20 || code > 0x7e)
125
- out += "\\u" + code.toString(16).padStart(4, "0");
126
- else
127
- out += ch;
82
+ function validateUnicode(value) {
83
+ for (let i = 0; i < value.length; i++) {
84
+ const unit = value.charCodeAt(i);
85
+ if (unit >= 0xd800 && unit <= 0xdbff) {
86
+ const next = value.charCodeAt(i + 1);
87
+ if (!(next >= 0xdc00 && next <= 0xdfff)) {
88
+ throw new LoneSurrogateError("lone UTF-16 surrogates are not permitted");
89
+ }
90
+ i += 1;
91
+ }
92
+ else if (unit >= 0xdc00 && unit <= 0xdfff) {
93
+ throw new LoneSurrogateError("lone UTF-16 surrogates are not permitted");
94
+ }
128
95
  }
129
- return out + '"';
130
96
  }
131
- /**
132
- * Serialise to the exact bytes Python's
133
- * `json.dumps(obj, sort_keys=True, separators=(",", ":"))` produces:
134
- * sorted keys, no incidental whitespace, ASCII-escaped strings.
135
- *
136
- * Object entries whose value is `undefined` are omitted, matching a Python dict
137
- * that simply never held the key. `null` is kept — it is a value.
138
- */
139
- export function canonicalJson(value) {
140
- return pyJsonDumps(value, ",", ":");
97
+ function jcsString(value) {
98
+ validateUnicode(value);
99
+ return JSON.stringify(value);
100
+ }
101
+ function jcsNumber(value) {
102
+ if (!Number.isFinite(value)) {
103
+ throw new NonFiniteNumberError("non-finite numbers are not permitted");
104
+ }
105
+ return JSON.stringify(value);
106
+ }
107
+ function unsupported(value) {
108
+ const kind = value === null ? "null" : typeof value;
109
+ throw new UnsupportedTypeError(`not a supported JSON value: ${kind}`);
141
110
  }
142
- /**
143
- * Serialise with sorted keys and ASCII escaping, choosing the separators.
144
- *
145
- * `canonicalJson` uses the compact `(",", ":")` pair every hash is taken over.
146
- * The `.jsonl` ledger on disk is written with Python's DEFAULT separators
147
- * (`", "` and `": "`), so a line read back by either language re-serialises to
148
- * the same bytes its writer produced.
149
- */
150
- export function pyJsonDumps(value, itemSeparator = ", ", keySeparator = ": ") {
151
- if (value === null || value === undefined)
111
+ function serialize(value, active) {
112
+ if (value === null)
152
113
  return "null";
153
114
  if (value instanceof RawNumber)
154
- return value.raw;
115
+ return jcsNumber(value.value);
155
116
  switch (typeof value) {
156
117
  case "boolean":
157
118
  return value ? "true" : "false";
158
119
  case "number":
159
- return pyNumber(value);
120
+ return jcsNumber(value);
160
121
  case "string":
161
- return pyString(value);
122
+ return jcsString(value);
123
+ case "undefined":
124
+ case "bigint":
125
+ case "symbol":
126
+ case "function":
127
+ return unsupported(value);
162
128
  }
163
- if (Array.isArray(value)) {
164
- return ("[" + value.map((v) => pyJsonDumps(v, itemSeparator, keySeparator)).join(itemSeparator) + "]");
129
+ if (active.has(value))
130
+ throw new UnsupportedTypeError("cyclic JSON values are not permitted");
131
+ active.add(value);
132
+ try {
133
+ if (Array.isArray(value)) {
134
+ const own = Reflect.ownKeys(value);
135
+ for (const key of own) {
136
+ if (key === "length")
137
+ continue;
138
+ if (typeof key !== "string" || !/^(?:0|[1-9]\d*)$/.test(key) || Number(key) >= value.length) {
139
+ throw new UnsupportedTypeError("JSON arrays may not carry extra properties");
140
+ }
141
+ }
142
+ const items = [];
143
+ for (let i = 0; i < value.length; i++) {
144
+ if (!Object.prototype.hasOwnProperty.call(value, i)) {
145
+ throw new UnsupportedTypeError("sparse JSON arrays are not permitted");
146
+ }
147
+ const descriptor = Object.getOwnPropertyDescriptor(value, String(i));
148
+ if (!descriptor.enumerable || !("value" in descriptor)) {
149
+ throw new UnsupportedTypeError("JSON arrays may contain only enumerable data elements");
150
+ }
151
+ items.push(serialize(descriptor.value, active));
152
+ }
153
+ return `[${items.join(",")}]`;
154
+ }
155
+ const prototype = Object.getPrototypeOf(value);
156
+ if (prototype !== Object.prototype && prototype !== null) {
157
+ throw new UnsupportedTypeError("only plain JSON objects are permitted");
158
+ }
159
+ const symbols = Object.getOwnPropertySymbols(value);
160
+ if (symbols.length > 0)
161
+ throw new UnsupportedTypeError("JSON object member names must be strings");
162
+ const keys = Object.getOwnPropertyNames(value);
163
+ const members = new Map();
164
+ for (const key of keys) {
165
+ validateUnicode(key);
166
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
167
+ if (!descriptor.enumerable || !("value" in descriptor)) {
168
+ throw new UnsupportedTypeError("JSON objects may contain only enumerable data properties");
169
+ }
170
+ members.set(key, descriptor.value);
171
+ }
172
+ // RFC 8785 sorts property names by their raw UTF-16 code units. JavaScript's
173
+ // default string ordering is exactly that ordering.
174
+ keys.sort();
175
+ return `{${keys
176
+ .map((key) => `${jcsString(key)}:${serialize(members.get(key), active)}`)
177
+ .join(",")}}`;
165
178
  }
166
- const keys = sortedStrings(Object.keys(value).filter((k) => value[k] !== undefined));
167
- return ("{" +
168
- keys
169
- .map((k) => `${pyString(k)}${keySeparator}${pyJsonDumps(value[k], itemSeparator, keySeparator)}`)
170
- .join(itemSeparator) +
171
- "}");
179
+ finally {
180
+ active.delete(value);
181
+ }
182
+ }
183
+ /** Return an RFC 8785 canonical JSON string. */
184
+ export function canonicalJson(value) {
185
+ return serialize(value, new WeakSet());
172
186
  }
173
- /** The canonical bytes — what every hash, seal and signature is taken over. */
187
+ /** The canonical UTF-8 bytes used by every hash, seal, and signature. */
174
188
  export function canonicalBytes(value) {
175
189
  return Buffer.from(canonicalJson(value), "utf8");
176
190
  }
@@ -179,137 +193,144 @@ export function toPlain(value) {
179
193
  if (value instanceof RawNumber)
180
194
  return value.value;
181
195
  if (Array.isArray(value))
182
- return value.map((v) => toPlain(v));
196
+ return value.map((item) => toPlain(item));
183
197
  if (value !== null && typeof value === "object") {
184
198
  const out = {};
185
- for (const [k, v] of Object.entries(value))
186
- out[k] = toPlain(v);
199
+ for (const [key, item] of Object.entries(value)) {
200
+ Object.defineProperty(out, key, {
201
+ value: toPlain(item),
202
+ enumerable: true,
203
+ configurable: true,
204
+ writable: true,
205
+ });
206
+ }
187
207
  return out;
188
208
  }
189
209
  return value;
190
210
  }
191
- /**
192
- * Parse JSON, keeping every number's original literal.
193
- *
194
- * `JSON.parse` is not used: it discards the literal, and with it the ability to
195
- * reproduce the bytes the writer hashed. Objects preserve their key order for
196
- * readability; canonical serialisation sorts them anyway.
197
- */
211
+ /** Parse JSON without losing number spellings, while rejecting duplicates. */
198
212
  export function parseJson(text) {
199
- const p = new Parser(text);
200
- p.skipWhitespace();
201
- const value = p.parseValue();
202
- p.skipWhitespace();
203
- if (!p.atEnd())
204
- p.fail("unexpected trailing data");
213
+ const parser = new Parser(text);
214
+ parser.skipWhitespace();
215
+ const value = parser.parseValue();
216
+ parser.skipWhitespace();
217
+ if (!parser.atEnd())
218
+ parser.fail("unexpected trailing data");
205
219
  return value;
206
220
  }
207
221
  const NUMBER_RE = /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/;
208
222
  class Parser {
209
- s;
223
+ source;
210
224
  i = 0;
211
- constructor(s) {
212
- this.s = s;
225
+ constructor(source) {
226
+ this.source = source;
213
227
  }
214
228
  atEnd() {
215
- return this.i >= this.s.length;
229
+ return this.i >= this.source.length;
216
230
  }
217
231
  fail(message) {
218
232
  throw new SyntaxError(`${message} at position ${this.i}`);
219
233
  }
220
234
  skipWhitespace() {
221
- while (this.i < this.s.length) {
222
- const c = this.s[this.i];
223
- if (c === " " || c === "\t" || c === "\n" || c === "\r")
224
- this.i++;
225
- else
226
- break;
227
- }
235
+ while (this.i < this.source.length && /[\t\n\r ]/.test(this.source[this.i]))
236
+ this.i++;
228
237
  }
229
238
  parseValue() {
230
239
  if (this.atEnd())
231
240
  this.fail("unexpected end of input");
232
- const c = this.s[this.i];
233
- if (c === "{")
241
+ const char = this.source[this.i];
242
+ if (char === "{")
234
243
  return this.parseObject();
235
- if (c === "[")
244
+ if (char === "[")
236
245
  return this.parseArray();
237
- if (c === '"')
246
+ if (char === '"')
238
247
  return this.parseString();
239
- if (this.s.startsWith("true", this.i))
248
+ if (this.source.startsWith("true", this.i))
240
249
  return (this.i += 4), true;
241
- if (this.s.startsWith("false", this.i))
250
+ if (this.source.startsWith("false", this.i))
242
251
  return (this.i += 5), false;
243
- if (this.s.startsWith("null", this.i))
252
+ if (this.source.startsWith("null", this.i))
244
253
  return (this.i += 4), null;
254
+ if (this.source.startsWith("NaN", this.i) ||
255
+ this.source.startsWith("Infinity", this.i) ||
256
+ this.source.startsWith("-Infinity", this.i)) {
257
+ throw new NonFiniteNumberError("non-finite numbers are not permitted");
258
+ }
245
259
  return this.parseNumber();
246
260
  }
247
261
  parseObject() {
248
- this.i++; // {
249
- const out = {};
262
+ this.i++;
263
+ const out = Object.create(null);
250
264
  this.skipWhitespace();
251
- if (this.s[this.i] === "}")
265
+ if (this.source[this.i] === "}")
252
266
  return this.i++, out;
253
267
  for (;;) {
254
268
  this.skipWhitespace();
255
- if (this.s[this.i] !== '"')
269
+ if (this.source[this.i] !== '"')
256
270
  this.fail("expected an object key");
257
271
  const key = this.parseString();
272
+ if (Object.prototype.hasOwnProperty.call(out, key))
273
+ throw new DuplicateMemberError(key);
258
274
  this.skipWhitespace();
259
- if (this.s[this.i] !== ":")
275
+ if (this.source[this.i] !== ":")
260
276
  this.fail("expected ':'");
261
277
  this.i++;
262
278
  this.skipWhitespace();
263
279
  out[key] = this.parseValue();
264
280
  this.skipWhitespace();
265
- const c = this.s[this.i];
266
- if (c === ",") {
281
+ const char = this.source[this.i];
282
+ if (char === ",") {
267
283
  this.i++;
268
284
  continue;
269
285
  }
270
- if (c === "}")
286
+ if (char === "}")
271
287
  return this.i++, out;
272
288
  this.fail("expected ',' or '}'");
273
289
  }
274
290
  }
275
291
  parseArray() {
276
- this.i++; // [
292
+ this.i++;
277
293
  const out = [];
278
294
  this.skipWhitespace();
279
- if (this.s[this.i] === "]")
295
+ if (this.source[this.i] === "]")
280
296
  return this.i++, out;
281
297
  for (;;) {
282
298
  this.skipWhitespace();
283
299
  out.push(this.parseValue());
284
300
  this.skipWhitespace();
285
- const c = this.s[this.i];
286
- if (c === ",") {
301
+ const char = this.source[this.i];
302
+ if (char === ",") {
287
303
  this.i++;
288
304
  continue;
289
305
  }
290
- if (c === "]")
306
+ if (char === "]")
291
307
  return this.i++, out;
292
308
  this.fail("expected ',' or ']'");
293
309
  }
294
310
  }
295
311
  parseString() {
296
- this.i++; // opening quote
312
+ this.i++;
297
313
  let out = "";
298
314
  for (;;) {
299
315
  if (this.atEnd())
300
316
  this.fail("unterminated string");
301
- const c = this.s[this.i];
302
- if (c === '"')
303
- return this.i++, out;
304
- if (c !== "\\") {
305
- out += c;
317
+ const char = this.source[this.i];
318
+ if (char === '"') {
319
+ this.i++;
320
+ validateUnicode(out);
321
+ return out;
322
+ }
323
+ if (char.charCodeAt(0) < 0x20)
324
+ this.fail("unescaped control character");
325
+ if (char !== "\\") {
326
+ out += char;
306
327
  this.i++;
307
328
  continue;
308
329
  }
309
330
  this.i++;
310
- const esc = this.s[this.i];
331
+ const escape = this.source[this.i];
311
332
  this.i++;
312
- switch (esc) {
333
+ switch (escape) {
313
334
  case '"':
314
335
  out += '"';
315
336
  break;
@@ -335,25 +356,28 @@ class Parser {
335
356
  out += "\t";
336
357
  break;
337
358
  case "u": {
338
- const hex = this.s.slice(this.i, this.i + 4);
359
+ const hex = this.source.slice(this.i, this.i + 4);
339
360
  if (!/^[0-9a-fA-F]{4}$/.test(hex))
340
361
  this.fail("bad \\u escape");
341
362
  out += String.fromCharCode(parseInt(hex, 16));
342
363
  this.i += 4;
343
364
  break;
344
365
  }
345
- default:
346
- this.fail("bad escape");
366
+ default: this.fail("bad escape");
347
367
  }
348
368
  }
349
369
  }
350
370
  parseNumber() {
351
- const m = NUMBER_RE.exec(this.s.slice(this.i));
352
- if (!m)
371
+ const match = NUMBER_RE.exec(this.source.slice(this.i));
372
+ if (!match)
353
373
  this.fail("expected a value");
354
- const raw = m[0];
374
+ const raw = match[0];
355
375
  this.i += raw.length;
356
- return new RawNumber(raw, Number(raw));
376
+ const value = Number(raw);
377
+ if (!Number.isFinite(value)) {
378
+ throw new NonFiniteNumberError("non-finite numbers are not permitted");
379
+ }
380
+ return new RawNumber(raw, value);
357
381
  }
358
382
  }
359
383
  //# sourceMappingURL=canonical.js.map