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.
- package/CHANGELOG.md +34 -0
- package/README.md +12 -4
- package/dist/cjs/audit.d.ts +1 -0
- package/dist/cjs/audit.d.ts.map +1 -1
- package/dist/cjs/audit.js +9 -5
- package/dist/cjs/audit.js.map +1 -1
- package/dist/cjs/canonical.d.ts +21 -69
- package/dist/cjs/canonical.d.ts.map +1 -1
- package/dist/cjs/canonical.js +198 -171
- package/dist/cjs/canonical.js.map +1 -1
- package/dist/cjs/evidence.d.ts +1 -0
- package/dist/cjs/evidence.d.ts.map +1 -1
- package/dist/cjs/evidence.js +8 -3
- package/dist/cjs/evidence.js.map +1 -1
- package/dist/cjs/index.d.ts +4 -4
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +14 -5
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/wire.d.ts +136 -1
- package/dist/cjs/wire.d.ts.map +1 -1
- package/dist/cjs/wire.js +405 -2
- package/dist/cjs/wire.js.map +1 -1
- package/dist/esm/audit.d.ts +1 -0
- package/dist/esm/audit.d.ts.map +1 -1
- package/dist/esm/audit.js +10 -6
- package/dist/esm/audit.js.map +1 -1
- package/dist/esm/canonical.d.ts +21 -69
- package/dist/esm/canonical.d.ts.map +1 -1
- package/dist/esm/canonical.js +192 -168
- package/dist/esm/canonical.js.map +1 -1
- package/dist/esm/evidence.d.ts +1 -0
- package/dist/esm/evidence.d.ts.map +1 -1
- package/dist/esm/evidence.js +8 -3
- package/dist/esm/evidence.js.map +1 -1
- package/dist/esm/index.d.ts +4 -4
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +3 -3
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/wire.d.ts +136 -1
- package/dist/esm/wire.d.ts.map +1 -1
- package/dist/esm/wire.js +400 -2
- package/dist/esm/wire.js.map +1 -1
- package/package.json +1 -1
package/dist/esm/canonical.d.ts
CHANGED
|
@@ -1,30 +1,19 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
|
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
|
-
/**
|
|
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
|
|
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"}
|
package/dist/esm/canonical.js
CHANGED
|
@@ -1,30 +1,26 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
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);
|
|
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
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
else if (
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
120
|
+
return jcsNumber(value);
|
|
160
121
|
case "string":
|
|
161
|
-
return
|
|
122
|
+
return jcsString(value);
|
|
123
|
+
case "undefined":
|
|
124
|
+
case "bigint":
|
|
125
|
+
case "symbol":
|
|
126
|
+
case "function":
|
|
127
|
+
return unsupported(value);
|
|
162
128
|
}
|
|
163
|
-
if (
|
|
164
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
|
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((
|
|
196
|
+
return value.map((item) => toPlain(item));
|
|
183
197
|
if (value !== null && typeof value === "object") {
|
|
184
198
|
const out = {};
|
|
185
|
-
for (const [
|
|
186
|
-
out
|
|
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
|
|
200
|
-
|
|
201
|
-
const value =
|
|
202
|
-
|
|
203
|
-
if (!
|
|
204
|
-
|
|
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
|
-
|
|
223
|
+
source;
|
|
210
224
|
i = 0;
|
|
211
|
-
constructor(
|
|
212
|
-
this.
|
|
225
|
+
constructor(source) {
|
|
226
|
+
this.source = source;
|
|
213
227
|
}
|
|
214
228
|
atEnd() {
|
|
215
|
-
return this.i >= this.
|
|
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.
|
|
222
|
-
|
|
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
|
|
233
|
-
if (
|
|
241
|
+
const char = this.source[this.i];
|
|
242
|
+
if (char === "{")
|
|
234
243
|
return this.parseObject();
|
|
235
|
-
if (
|
|
244
|
+
if (char === "[")
|
|
236
245
|
return this.parseArray();
|
|
237
|
-
if (
|
|
246
|
+
if (char === '"')
|
|
238
247
|
return this.parseString();
|
|
239
|
-
if (this.
|
|
248
|
+
if (this.source.startsWith("true", this.i))
|
|
240
249
|
return (this.i += 4), true;
|
|
241
|
-
if (this.
|
|
250
|
+
if (this.source.startsWith("false", this.i))
|
|
242
251
|
return (this.i += 5), false;
|
|
243
|
-
if (this.
|
|
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.
|
|
265
|
+
if (this.source[this.i] === "}")
|
|
252
266
|
return this.i++, out;
|
|
253
267
|
for (;;) {
|
|
254
268
|
this.skipWhitespace();
|
|
255
|
-
if (this.
|
|
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.
|
|
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
|
|
266
|
-
if (
|
|
281
|
+
const char = this.source[this.i];
|
|
282
|
+
if (char === ",") {
|
|
267
283
|
this.i++;
|
|
268
284
|
continue;
|
|
269
285
|
}
|
|
270
|
-
if (
|
|
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.
|
|
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
|
|
286
|
-
if (
|
|
301
|
+
const char = this.source[this.i];
|
|
302
|
+
if (char === ",") {
|
|
287
303
|
this.i++;
|
|
288
304
|
continue;
|
|
289
305
|
}
|
|
290
|
-
if (
|
|
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++;
|
|
312
|
+
this.i++;
|
|
297
313
|
let out = "";
|
|
298
314
|
for (;;) {
|
|
299
315
|
if (this.atEnd())
|
|
300
316
|
this.fail("unterminated string");
|
|
301
|
-
const
|
|
302
|
-
if (
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
out
|
|
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
|
|
331
|
+
const escape = this.source[this.i];
|
|
311
332
|
this.i++;
|
|
312
|
-
switch (
|
|
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.
|
|
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
|
|
352
|
-
if (!
|
|
371
|
+
const match = NUMBER_RE.exec(this.source.slice(this.i));
|
|
372
|
+
if (!match)
|
|
353
373
|
this.fail("expected a value");
|
|
354
|
-
const raw =
|
|
374
|
+
const raw = match[0];
|
|
355
375
|
this.i += raw.length;
|
|
356
|
-
|
|
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
|