@datumesh/verify 0.1.1 → 0.2.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/README.md +7 -5
- package/dist/cjs/ed25519.js +2 -0
- package/dist/cjs/envelope.d.ts +38 -0
- package/dist/cjs/envelope.js +243 -0
- package/dist/cjs/errors.d.ts +2 -2
- package/dist/cjs/errors.js +5 -1
- package/dist/cjs/index.d.ts +13 -3
- package/dist/cjs/index.js +22 -3
- package/dist/cjs/json.d.ts +6 -7
- package/dist/cjs/json.js +49 -18
- package/dist/cjs/keys.d.ts +2 -0
- package/dist/cjs/keys.js +55 -2
- package/dist/cjs/lexical.d.ts +11 -1
- package/dist/cjs/lexical.js +28 -4
- package/dist/cjs/units-data.d.ts +4 -0
- package/dist/cjs/units-data.js +11 -0
- package/dist/cjs/units.d.ts +24 -0
- package/dist/cjs/units.js +131 -0
- package/dist/cjs/uri.d.ts +14 -2
- package/dist/cjs/uri.js +27 -14
- package/dist/cjs/validate.d.ts +8 -0
- package/dist/cjs/validate.js +63 -4
- package/dist/cjs/verify.d.ts +21 -2
- package/dist/cjs/verify.js +47 -7
- package/dist/esm/ed25519.js +2 -0
- package/dist/esm/envelope.d.ts +38 -0
- package/dist/esm/envelope.js +240 -0
- package/dist/esm/errors.d.ts +2 -2
- package/dist/esm/errors.js +5 -1
- package/dist/esm/index.d.ts +13 -3
- package/dist/esm/index.js +20 -4
- package/dist/esm/json.d.ts +6 -7
- package/dist/esm/json.js +49 -18
- package/dist/esm/keys.d.ts +2 -0
- package/dist/esm/keys.js +54 -1
- package/dist/esm/lexical.d.ts +11 -1
- package/dist/esm/lexical.js +24 -2
- package/dist/esm/units-data.d.ts +4 -0
- package/dist/esm/units-data.js +8 -0
- package/dist/esm/units.d.ts +24 -0
- package/dist/esm/units.js +123 -0
- package/dist/esm/uri.d.ts +14 -2
- package/dist/esm/uri.js +31 -18
- package/dist/esm/validate.d.ts +8 -0
- package/dist/esm/validate.js +61 -4
- package/dist/esm/verify.d.ts +21 -2
- package/dist/esm/verify.js +46 -8
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @datumesh/verify
|
|
2
2
|
|
|
3
|
-
Verify [Datumesh](https://datumesh.com) receipts in JavaScript: signed answers for addressable data cells, checked offline against strict RFC 8785 canonical JSON and Ed25519, following the [Datumesh specification](https://github.com/datumesh-com/datumesh-spec) revision `spec-0.
|
|
3
|
+
Verify [Datumesh](https://datumesh.com) receipts in JavaScript: signed answers for addressable data cells, checked offline against strict RFC 8785 canonical JSON and Ed25519, following the [Datumesh specification](https://github.com/datumesh-com/datumesh-spec) revision `spec-0.2.1` and passing all of its test vectors.
|
|
4
4
|
|
|
5
5
|
```sh
|
|
6
6
|
npm install @datumesh/verify
|
|
@@ -15,7 +15,7 @@ const vector = JSON.parse(readFileSync('stored-number.json', 'utf8'));
|
|
|
15
15
|
const keys = keysFromResponse('rates.example.org', 200, vector.keys['rates.example.org'].https.body);
|
|
16
16
|
const result = verify(vector.input, keys);
|
|
17
17
|
console.log(result.ok, result.code, result.level, result.hash);
|
|
18
|
-
// true OK signed
|
|
18
|
+
// true OK signed 19a5fe3a744c5792f747fb4c286ce396ab44b95922de4f07937502fe595b6f81
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
The same with CommonJS:
|
|
@@ -31,12 +31,14 @@ console.log(verify(vector.input, keys).code); // OK
|
|
|
31
31
|
|
|
32
32
|
## What it does
|
|
33
33
|
|
|
34
|
-
- `verify(receipt, keys, recompute?)` runs the verification procedure of SPEC.md 10.1 and returns `{ ok, code, level, hash, key, detail, path }`. It is synchronous, never touches the network and never throws for a bad receipt or bad key material; failures come back as the spec's error codes (`E_SIGNATURE`, `E_KEY_NOT_VALID`, ...). `level` is `"signed"`, or `"recomputed"` when you pass the descriptor, the input receipts and the value you obtained by evaluating the function (the library checks lineage and compares; it does not evaluate functions).
|
|
34
|
+
- `verify(receipt, keys, recompute?, blob?)` runs the verification procedure of SPEC.md 10.1 and returns `{ ok, code, level, hash, key, detail, path, blobChecked }`. It is synchronous, never touches the network and never throws for a bad receipt or bad key material; failures come back as the spec's error codes (`E_SIGNATURE`, `E_KEY_NOT_VALID`, ...). `level` is `"signed"`, or `"recomputed"` when you pass the descriptor, the input receipts and the value you obtained by evaluating the function (the library checks lineage and compares; it does not evaluate functions). For a receipt whose value is a `blob`, pass the content bytes as `blob` to check their length and SHA-256 (`E_BLOB_MISMATCH`; `blobChecked` is then true).
|
|
35
35
|
- `keys` is a key document, the result of `keysFromResponse` / `fetchKeys`, a `{ keyId: publicKeyHex }` map, or an array of these (one per host for lineage).
|
|
36
36
|
- `fetchKeys(host)` is the only function that uses the network: one `GET https://<host>/.well-known/datumesh.json`, no redirects, 10 s timeout, using the platform's `fetch` and TLS verification.
|
|
37
|
-
- `
|
|
37
|
+
- `checkEnvelope(status, body, keys, request)` checks a mesh's response envelope (SPEC.md 5.5) against its HTTP status, verifies the embedded receipt through the signature, and checks that it answers your request (`request` is `{ cell_id, asof? }`: the id of the cell you asked for and the `asof` you asked with, if any), returning `{ ok, code, outcome, trust, receiptHash, detail, path }`. The envelope itself is unsigned: `stale`, `superseded_by` and `verified` remain the mesh's claims.
|
|
38
|
+
- `resolvedUnit(descriptor, cell)` gives the unit of a cell's value from its function's declared units (`"USD/EUR"`, `"degC"`, or `null` for a non-number), using the closed list of `units.md` shipped with the package. The library never converts or checks values against units.
|
|
39
|
+
- `canonicalize(obj, kind)`, `cellHash`, `receiptHash`, `functionHash`, `parseUri(uri, signature)`, `formatUri(ref)` and `keyId` expose canonical forms, hashes and cell URIs. These and `resolvedUnit` throw `DatumeshError` (with `.code` and `.path`).
|
|
38
40
|
|
|
39
|
-
Strings are checked against Unicode 15.1 tables shipped with the package, so results do not depend on the JavaScript engine's Unicode version. Ed25519 verification is libsodium-strict, as the spec requires. ESM and CommonJS builds with TypeScript types, for Node 18+ and for browsers through a bundler (the core uses no Node built-ins). Where the specification left a question open, the choices made are listed in [DEFECTS.md](https://github.com/datumesh-com/datumesh-verify-js/blob/main/DEFECTS.md).
|
|
41
|
+
SHA-256 and Ed25519 come from the `@noble` libraries (pure JavaScript), so blob checks work the same in Node 18.0 and in browsers. Strings are checked against Unicode 15.1 tables shipped with the package, so results do not depend on the JavaScript engine's Unicode version. Ed25519 verification is libsodium-strict, as the spec requires. ESM and CommonJS builds with TypeScript types, for Node 18+ and for browsers through a bundler (the core uses no Node built-ins). Where the specification left a question open, the choices made are listed in [DEFECTS.md](https://github.com/datumesh-com/datumesh-verify-js/blob/main/DEFECTS.md).
|
|
40
42
|
|
|
41
43
|
A valid signature means only that the holder of the key named in `computed_by` stated the value. Whom to trust is your policy.
|
|
42
44
|
|
package/dist/cjs/ed25519.js
CHANGED
|
@@ -59,6 +59,8 @@ function verifyEd25519Strict(sig, msg, pub) {
|
|
|
59
59
|
const h = (0, sha2_1.sha512)(concat(rBytes, pub, msg));
|
|
60
60
|
const k = le(h) % L;
|
|
61
61
|
// R' = [S]B - [k]A (multiplyUnsafe accepts 0; inputs are public, so variable time is fine).
|
|
62
|
+
// subtract() adds the negation of the point [k]A, as SPEC 10.1 step 6 requires; it is not
|
|
63
|
+
// [L - k]A, which differs for a mixed-order A (receipt/ed25519-mixed-order-key).
|
|
62
64
|
const SB = s === 0n ? Point.ZERO : Point.BASE.multiplyUnsafe(s);
|
|
63
65
|
const kA = k === 0n ? Point.ZERO : A.multiplyUnsafe(k);
|
|
64
66
|
const rCheck = SB.subtract(kA).toBytes();
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type ErrorCode } from './errors.js';
|
|
2
|
+
export type Outcome = 'ok' | 'stale' | 'not_covered' | 'unlicensed' | 'payment_required' | 'rate_limited' | 'error' | 'bad_request';
|
|
3
|
+
/** What the client asked for (SPEC 5.5 step 5): the requested cell's id, and `asof` if the request had one. */
|
|
4
|
+
export interface EnvelopeRequest {
|
|
5
|
+
cell_id: string;
|
|
6
|
+
asof?: string;
|
|
7
|
+
}
|
|
8
|
+
export type Trust = 'primary' | 'verified' | 'unverified';
|
|
9
|
+
export interface EnvelopeResult {
|
|
10
|
+
/** True when the envelope is well formed, matches the status, and its receipt (if any) is signature-valid. */
|
|
11
|
+
ok: boolean;
|
|
12
|
+
/** "OK", E_ENVELOPE, a parse code, or the code of the embedded receipt's verification. */
|
|
13
|
+
code: 'OK' | ErrorCode;
|
|
14
|
+
/** The envelope's outcome when ok, else null. */
|
|
15
|
+
outcome: Outcome | null;
|
|
16
|
+
/** The trust label when ok and the envelope carries a receipt, else null. */
|
|
17
|
+
trust: Trust | null;
|
|
18
|
+
/** The embedded receipt's hash once it has been verified (SPEC 10.1 steps 1-6), else null. */
|
|
19
|
+
receiptHash: string | null;
|
|
20
|
+
/** Human-readable explanation. */
|
|
21
|
+
detail: string;
|
|
22
|
+
/** Where the failure was found (a JSON path such as `$.quote.amount`), or null. */
|
|
23
|
+
path: string | null;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Check a mesh response (SPEC 5.5): `status` is the HTTP status, `body` the response body as
|
|
27
|
+
* JSON text or bytes (a parsed object is accepted too, without duplicate-member detection), and
|
|
28
|
+
* `keys` any key source `verify` accepts, used to verify an embedded receipt through step 6.
|
|
29
|
+
* `request` is what the client asked for: `{cell_id, asof?}`, the id (SPEC 4.4) of the requested
|
|
30
|
+
* cell resolved to the fn_hash the client used, and the requested asof if there was one.
|
|
31
|
+
* Steps: strict parse; members (E_ENVELOPE); status against outcome (E_ENVELOPE); the receipt
|
|
32
|
+
* (its own codes); the receipt answers the request (E_ENVELOPE); then `trust` against the receipt
|
|
33
|
+
* and `superseded_by` against its hash (E_ENVELOPE).
|
|
34
|
+
*
|
|
35
|
+
* Never throws for an invalid body or bad key material; throws TypeError only when `status` is
|
|
36
|
+
* not an integer, `request` is malformed, or `keys` is not a key source at all.
|
|
37
|
+
*/
|
|
38
|
+
export declare function checkEnvelope(status: number, body: unknown, keys: unknown, request: EnvelopeRequest): EnvelopeResult;
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.checkEnvelope = checkEnvelope;
|
|
4
|
+
// Response envelopes (SPEC.md 5.5): the unsigned JSON body a mesh returns around a receipt.
|
|
5
|
+
// Synchronous, offline; the embedded receipt is verified through step 6 with the given keys.
|
|
6
|
+
const errors_js_1 = require("./errors.js");
|
|
7
|
+
const json_js_1 = require("./json.js");
|
|
8
|
+
const lexical_js_1 = require("./lexical.js");
|
|
9
|
+
const hash_js_1 = require("./hash.js");
|
|
10
|
+
const jcs_js_1 = require("./jcs.js");
|
|
11
|
+
const keys_js_1 = require("./keys.js");
|
|
12
|
+
const units_js_1 = require("./units.js");
|
|
13
|
+
const verify_js_1 = require("./verify.js");
|
|
14
|
+
/** SPEC 5.5: required and optional members per outcome (besides `outcome`). */
|
|
15
|
+
const MEMBERS = {
|
|
16
|
+
ok: { required: ['receipt', 'trust'], optional: ['superseded_by', 'reason'] },
|
|
17
|
+
stale: { required: ['receipt', 'trust', 'age_seconds'], optional: ['superseded_by', 'reason'] },
|
|
18
|
+
not_covered: { required: [], optional: [] },
|
|
19
|
+
unlicensed: { required: [], optional: [] },
|
|
20
|
+
payment_required: { required: ['quote'], optional: [] },
|
|
21
|
+
rate_limited: { required: ['retry_after_seconds'], optional: [] },
|
|
22
|
+
error: { required: ['error'], optional: [] },
|
|
23
|
+
bad_request: { required: ['error'], optional: [] },
|
|
24
|
+
};
|
|
25
|
+
const STATUS = {
|
|
26
|
+
ok: (s) => s === 200,
|
|
27
|
+
stale: (s) => s === 200,
|
|
28
|
+
not_covered: (s) => s === 404,
|
|
29
|
+
unlicensed: (s) => s === 403,
|
|
30
|
+
payment_required: (s) => s === 402,
|
|
31
|
+
rate_limited: (s) => s === 429,
|
|
32
|
+
error: (s) => s >= 500 && s <= 599,
|
|
33
|
+
bad_request: (s) => s === 400,
|
|
34
|
+
};
|
|
35
|
+
/** SPEC 5.5: the codes `parse` can raise (5.4), the only ones a bad_request carries. */
|
|
36
|
+
const PARSE_CODES = ['E_PARSE_URI', 'E_CANON_NAME', 'E_CANON_ARG_NAME', 'E_CANON_ARG_DUPLICATE', 'E_CANON_ARG_ORDER', 'E_CANON_PERCENT', 'E_CANON_TYPE', 'E_CANON_NUMBER', 'E_CANON_DATE', 'E_CANON_INSTANT', 'E_CANON_STRING', 'E_CANON_BLOB', 'E_CANON_LIMIT', 'E_ARG_UNKNOWN', 'E_ARG_MISSING'];
|
|
37
|
+
const TRUST = ['primary', 'verified', 'unverified'];
|
|
38
|
+
const MAX_SECONDS = 9007199254740991;
|
|
39
|
+
class Bad extends Error {
|
|
40
|
+
constructor(detail, path) {
|
|
41
|
+
super(detail);
|
|
42
|
+
this.detail = detail;
|
|
43
|
+
this.path = path;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function bad(detail, path) {
|
|
47
|
+
throw new Bad(detail, path);
|
|
48
|
+
}
|
|
49
|
+
/** A rule of 3.x checked inside an envelope: its own code becomes part of the E_ENVELOPE detail. */
|
|
50
|
+
function inner(f, path) {
|
|
51
|
+
try {
|
|
52
|
+
f();
|
|
53
|
+
}
|
|
54
|
+
catch (e) {
|
|
55
|
+
if (e instanceof errors_js_1.DatumeshError)
|
|
56
|
+
bad(`${e.code}: ${e.detail}`, path);
|
|
57
|
+
throw e;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* SPEC 5.5 integer members: written 0 or [1-9][0-9]*, at most 2^53 - 1. The source text decides
|
|
62
|
+
* (`30.0`, `3e1` and `-0` are rejected); for an already-parsed body, whose text is gone, the
|
|
63
|
+
* value must be a non-negative safe integer other than -0.
|
|
64
|
+
*/
|
|
65
|
+
function seconds(v, text, name, path) {
|
|
66
|
+
const why = `${name} is written 0 or [1-9][0-9]*, at most 2^53 - 1`;
|
|
67
|
+
if (typeof v !== 'number')
|
|
68
|
+
bad(why, path);
|
|
69
|
+
if (text !== undefined ? !/^(0|[1-9][0-9]*)$/.test(text) : !Number.isSafeInteger(v) || Object.is(v, -0))
|
|
70
|
+
bad(why, path);
|
|
71
|
+
if (v < 0 || v > MAX_SECONDS)
|
|
72
|
+
bad(why, path);
|
|
73
|
+
}
|
|
74
|
+
function quote(v, path) {
|
|
75
|
+
if (!(0, json_js_1.isObject)(v))
|
|
76
|
+
bad('quote is an object {amount, authorise, currency}', path);
|
|
77
|
+
for (const k of ['amount', 'authorise', 'currency'])
|
|
78
|
+
if (!(0, json_js_1.has)(v, k))
|
|
79
|
+
bad(`quote has no ${k}`, path);
|
|
80
|
+
for (const k of (0, json_js_1.sortedKeys)(v))
|
|
81
|
+
if (!['amount', 'authorise', 'currency'].includes(k))
|
|
82
|
+
bad(`quote has an unexpected member ${JSON.stringify(k)}`, path);
|
|
83
|
+
const amount = v.amount;
|
|
84
|
+
const ap = (0, json_js_1.memberPath)(path, 'amount');
|
|
85
|
+
if (typeof amount !== 'string')
|
|
86
|
+
bad('amount is a decimal string (SPEC 3.1)', ap);
|
|
87
|
+
inner(() => (0, lexical_js_1.checkNumber)(amount, ap), ap);
|
|
88
|
+
if (amount.startsWith('-') || amount === '0')
|
|
89
|
+
bad('amount is greater than zero', ap);
|
|
90
|
+
const url = v.authorise;
|
|
91
|
+
const up = (0, json_js_1.memberPath)(path, 'authorise');
|
|
92
|
+
if (typeof url !== 'string' || !lexical_js_1.URL_RE.test(url) || url.length > lexical_js_1.LIMITS.licence_url)
|
|
93
|
+
bad('authorise is an https:// URL in the form of a licence URL (SPEC 8.1)', up);
|
|
94
|
+
if (!(0, units_js_1.isCurrency)(v.currency))
|
|
95
|
+
bad('currency is a currency code from units.md', (0, json_js_1.memberPath)(path, 'currency'));
|
|
96
|
+
}
|
|
97
|
+
/** Step 2: members against the table and their rules. Returns the outcome. */
|
|
98
|
+
function members(o, numbers) {
|
|
99
|
+
if (!(0, json_js_1.isObject)(o) || typeof o.outcome !== 'string')
|
|
100
|
+
bad('an envelope is an object with a string outcome', '$');
|
|
101
|
+
const outcome = o.outcome;
|
|
102
|
+
if (!Object.prototype.hasOwnProperty.call(MEMBERS, outcome))
|
|
103
|
+
bad(`unknown outcome ${JSON.stringify(outcome)}`, '$.outcome');
|
|
104
|
+
const { required, optional } = MEMBERS[outcome];
|
|
105
|
+
for (const k of required)
|
|
106
|
+
if (!(0, json_js_1.has)(o, k))
|
|
107
|
+
bad(`outcome ${outcome} requires ${k}`, '$');
|
|
108
|
+
for (const k of (0, json_js_1.sortedKeys)(o))
|
|
109
|
+
if (k !== 'outcome' && !required.includes(k) && !optional.includes(k))
|
|
110
|
+
bad(`member ${JSON.stringify(k)} is not allowed with outcome ${outcome}`, (0, json_js_1.memberPath)('$', k));
|
|
111
|
+
if ((0, json_js_1.has)(o, 'reason') && !(0, json_js_1.has)(o, 'superseded_by'))
|
|
112
|
+
bad('reason is only present with superseded_by', '$.reason');
|
|
113
|
+
for (const k of (0, json_js_1.sortedKeys)(o)) {
|
|
114
|
+
const v = o[k];
|
|
115
|
+
const p = (0, json_js_1.memberPath)('$', k);
|
|
116
|
+
switch (k) {
|
|
117
|
+
case 'age_seconds':
|
|
118
|
+
case 'retry_after_seconds':
|
|
119
|
+
seconds(v, numbers.get(p), k, p);
|
|
120
|
+
break;
|
|
121
|
+
case 'error':
|
|
122
|
+
if (outcome === 'bad_request') {
|
|
123
|
+
if (typeof v !== 'string' || !PARSE_CODES.includes(v))
|
|
124
|
+
bad('a bad_request carries a code that parsing a cell URI raises (SPEC 5.4)', p);
|
|
125
|
+
}
|
|
126
|
+
else if (typeof v !== 'string' || v === 'E_ENVELOPE' || !errors_js_1.ERROR_CODES.includes(v)) {
|
|
127
|
+
bad('error is a code from SPEC.md 10.2 other than E_ENVELOPE', p);
|
|
128
|
+
}
|
|
129
|
+
break;
|
|
130
|
+
case 'quote':
|
|
131
|
+
quote(v, p);
|
|
132
|
+
break;
|
|
133
|
+
case 'reason':
|
|
134
|
+
if (typeof v !== 'string')
|
|
135
|
+
bad('reason is a string', p);
|
|
136
|
+
if (v === '')
|
|
137
|
+
bad('reason must not be empty', p);
|
|
138
|
+
inner(() => (0, lexical_js_1.checkString)(v, p, { limit: lexical_js_1.LIMITS.summary }), p);
|
|
139
|
+
break;
|
|
140
|
+
case 'receipt':
|
|
141
|
+
if (!(0, json_js_1.isObject)(v))
|
|
142
|
+
bad('receipt is a receipt object', p);
|
|
143
|
+
break;
|
|
144
|
+
case 'superseded_by':
|
|
145
|
+
if (typeof v !== 'string' || !(0, lexical_js_1.isHex)(v, 64))
|
|
146
|
+
bad('superseded_by is a receipt hash: 64 lowercase hex digits', p);
|
|
147
|
+
break;
|
|
148
|
+
case 'trust':
|
|
149
|
+
if (typeof v !== 'string' || !TRUST.includes(v))
|
|
150
|
+
bad('trust is one of primary, verified, unverified', p);
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return outcome;
|
|
155
|
+
}
|
|
156
|
+
function checkRequest(request) {
|
|
157
|
+
const ok = (0, json_js_1.isObject)(request) &&
|
|
158
|
+
typeof request.cell_id === 'string' &&
|
|
159
|
+
(0, lexical_js_1.isHex)(request.cell_id, 64) &&
|
|
160
|
+
Object.keys(request).every((k) => k === 'cell_id' || k === 'asof') &&
|
|
161
|
+
(!(0, json_js_1.has)(request, 'asof') || (typeof request.asof === 'string' && (0, lexical_js_1.isInstant)(request.asof)));
|
|
162
|
+
if (!ok)
|
|
163
|
+
throw new TypeError('request must be {cell_id: <64 lowercase hex>, asof?: <instant>}: the cell id of the requested cell (resolved to the fn_hash used) and its asof, if the request had one');
|
|
164
|
+
}
|
|
165
|
+
const hostOf = (s, sep) => {
|
|
166
|
+
const i = s.indexOf(sep);
|
|
167
|
+
return i < 0 ? s : s.slice(0, i);
|
|
168
|
+
};
|
|
169
|
+
/**
|
|
170
|
+
* Check a mesh response (SPEC 5.5): `status` is the HTTP status, `body` the response body as
|
|
171
|
+
* JSON text or bytes (a parsed object is accepted too, without duplicate-member detection), and
|
|
172
|
+
* `keys` any key source `verify` accepts, used to verify an embedded receipt through step 6.
|
|
173
|
+
* `request` is what the client asked for: `{cell_id, asof?}`, the id (SPEC 4.4) of the requested
|
|
174
|
+
* cell resolved to the fn_hash the client used, and the requested asof if there was one.
|
|
175
|
+
* Steps: strict parse; members (E_ENVELOPE); status against outcome (E_ENVELOPE); the receipt
|
|
176
|
+
* (its own codes); the receipt answers the request (E_ENVELOPE); then `trust` against the receipt
|
|
177
|
+
* and `superseded_by` against its hash (E_ENVELOPE).
|
|
178
|
+
*
|
|
179
|
+
* Never throws for an invalid body or bad key material; throws TypeError only when `status` is
|
|
180
|
+
* not an integer, `request` is malformed, or `keys` is not a key source at all.
|
|
181
|
+
*/
|
|
182
|
+
function checkEnvelope(status, body, keys, request) {
|
|
183
|
+
if (typeof status !== 'number' || !Number.isInteger(status))
|
|
184
|
+
throw new TypeError('status must be the HTTP status as an integer');
|
|
185
|
+
checkRequest(request);
|
|
186
|
+
const sources = (0, keys_js_1.collectSources)(keys);
|
|
187
|
+
const fail = (code, detail, path, receiptHash = null) => ({ ok: false, code, outcome: null, trust: null, receiptHash, detail, path });
|
|
188
|
+
let o;
|
|
189
|
+
const numbers = new Map();
|
|
190
|
+
try {
|
|
191
|
+
o = (0, json_js_1.toJson)(body, numbers); // step 1
|
|
192
|
+
}
|
|
193
|
+
catch (e) {
|
|
194
|
+
if (e instanceof errors_js_1.DatumeshError)
|
|
195
|
+
return fail(e.code, e.detail, e.path);
|
|
196
|
+
throw e;
|
|
197
|
+
}
|
|
198
|
+
let outcome;
|
|
199
|
+
try {
|
|
200
|
+
outcome = members(o, numbers); // step 2
|
|
201
|
+
if (!STATUS[outcome](status))
|
|
202
|
+
bad(`status ${status} does not match outcome ${outcome}`, '$.outcome'); // step 3
|
|
203
|
+
}
|
|
204
|
+
catch (e) {
|
|
205
|
+
if (e instanceof Bad)
|
|
206
|
+
return fail('E_ENVELOPE', e.detail, e.path);
|
|
207
|
+
throw e;
|
|
208
|
+
}
|
|
209
|
+
const env = o;
|
|
210
|
+
if (!(0, json_js_1.has)(env, 'receipt'))
|
|
211
|
+
return { ok: true, code: 'OK', outcome, trust: null, receiptHash: null, detail: `outcome ${outcome}`, path: null };
|
|
212
|
+
// Step 4.
|
|
213
|
+
let r;
|
|
214
|
+
try {
|
|
215
|
+
r = (0, verify_js_1.verifyEmbedded)(env.receipt, sources);
|
|
216
|
+
}
|
|
217
|
+
catch (e) {
|
|
218
|
+
if (e instanceof verify_js_1.Stop)
|
|
219
|
+
return fail(e.code, `receipt: ${e.detail}`, e.path === null ? '$.receipt' : e.path.replace(/^\$/, '$.receipt'));
|
|
220
|
+
throw e;
|
|
221
|
+
}
|
|
222
|
+
// Step 5: the receipt answers the request.
|
|
223
|
+
const cellId = (0, hash_js_1.sha256Hex)((0, jcs_js_1.jcsBytes)(r.receipt.cell));
|
|
224
|
+
if (cellId !== request.cell_id)
|
|
225
|
+
return fail('E_ENVELOPE', `the receipt is for cell ${cellId}, not the requested ${request.cell_id}`, '$.receipt.cell', r.hash);
|
|
226
|
+
// With a requested asof: equal for ok; at or before it for stale (D31). Instants are fixed
|
|
227
|
+
// width, so string order is time order (SPEC 3.3).
|
|
228
|
+
const asof = r.receipt.asof;
|
|
229
|
+
if (request.asof !== undefined && (outcome === 'stale' ? !(asof <= request.asof) : asof !== request.asof)) {
|
|
230
|
+
const want = outcome === 'stale' ? `at or before the requested ${request.asof}` : `the requested ${request.asof}`;
|
|
231
|
+
return fail('E_ENVELOPE', `the receipt answers asof ${asof}, not ${want}`, '$.receipt.asof', r.hash);
|
|
232
|
+
}
|
|
233
|
+
// Step 6.
|
|
234
|
+
const trust = env.trust;
|
|
235
|
+
const cell = r.receipt.cell;
|
|
236
|
+
const primary = hostOf(r.receipt.computed_by, '#') === hostOf(cell.ns, '/') && r.receipt.inputs.length === 0;
|
|
237
|
+
if ((trust === 'primary') !== primary) {
|
|
238
|
+
return fail('E_ENVELOPE', primary ? 'the receipt is primary (signed by its namespace host, no inputs): trust must be primary' : 'trust primary needs a receipt signed by the host of cell.ns with no inputs', '$.trust', r.hash);
|
|
239
|
+
}
|
|
240
|
+
if (env.superseded_by === r.hash)
|
|
241
|
+
return fail('E_ENVELOPE', 'superseded_by is the receipt itself', '$.superseded_by', r.hash);
|
|
242
|
+
return { ok: true, code: 'OK', outcome, trust, receiptHash: r.hash, detail: `outcome ${outcome}, trust ${trust}, receipt signed by ${r.receipt.computed_by}`, path: null };
|
|
243
|
+
}
|
package/dist/cjs/errors.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** The spec revision this library conforms to. */
|
|
2
|
-
export declare const SPEC_VERSION = "spec-0.
|
|
2
|
+
export declare const SPEC_VERSION = "spec-0.2.1";
|
|
3
3
|
/** Every error code defined by SPEC.md 10.2, in the order of that table. */
|
|
4
|
-
export declare const ERROR_CODES: readonly ["E_PARSE_JSON", "E_PARSE_DUPLICATE_KEY", "E_PARSE_URI", "E_SCHEMA", "E_CANON_NAME", "E_CANON_ARG_NAME", "E_CANON_ARG_DUPLICATE", "E_CANON_ARG_ORDER", "E_CANON_PERCENT", "E_CANON_TYPE", "E_CANON_NUMBER", "E_CANON_DATE", "E_CANON_INSTANT", "E_CANON_STRING", "E_CANON_HEX", "E_CANON_LICENCE", "E_CANON_SET", "E_CANON_LIMIT", "E_SLICE", "E_PUBLISHER_MISMATCH", "E_VALID_MISMATCH", "E_TIME_ORDER", "E_ARG_UNKNOWN", "E_ARG_MISSING", "E_KEY_UNAVAILABLE", "E_KEY_DOC", "E_KEY_UNKNOWN", "E_KEY_NOT_VALID", "E_SIGNATURE", "E_FN_UNAVAILABLE", "E_FN_HASH_MISMATCH", "E_ARG_TYPE", "E_RETURN_TYPE", "E_INPUT_UNAVAILABLE", "E_INPUT_INVALID", "E_INPUT_UNDECLARED", "E_VALUE_MISMATCH"];
|
|
4
|
+
export declare const ERROR_CODES: readonly ["E_PARSE_JSON", "E_PARSE_DUPLICATE_KEY", "E_PARSE_URI", "E_SCHEMA", "E_CANON_NAME", "E_CANON_ARG_NAME", "E_CANON_ARG_DUPLICATE", "E_CANON_ARG_ORDER", "E_CANON_PERCENT", "E_CANON_TYPE", "E_CANON_NUMBER", "E_CANON_UNIT", "E_CANON_DATE", "E_CANON_INSTANT", "E_CANON_STRING", "E_CANON_BLOB", "E_CANON_HEX", "E_CANON_LICENCE", "E_CANON_SET", "E_CANON_LIMIT", "E_SLICE", "E_PUBLISHER_MISMATCH", "E_VALID_MISMATCH", "E_TIME_ORDER", "E_ARG_UNKNOWN", "E_ARG_MISSING", "E_KEY_UNAVAILABLE", "E_KEY_DOC", "E_KEY_UNKNOWN", "E_KEY_NOT_VALID", "E_SIGNATURE", "E_FN_UNAVAILABLE", "E_FN_HASH_MISMATCH", "E_ARG_TYPE", "E_RETURN_TYPE", "E_INPUT_UNAVAILABLE", "E_INPUT_INVALID", "E_INPUT_UNDECLARED", "E_VALUE_MISMATCH", "E_BLOB_MISMATCH", "E_ENVELOPE"];
|
|
5
5
|
export type ErrorCode = (typeof ERROR_CODES)[number];
|
|
6
6
|
/** A Datumesh rule was broken. `code` is a SPEC.md 10.2 code; `path` locates the failure. */
|
|
7
7
|
export declare class DatumeshError extends Error {
|
package/dist/cjs/errors.js
CHANGED
|
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.DatumeshError = exports.ERROR_CODES = exports.SPEC_VERSION = void 0;
|
|
4
4
|
exports.fail = fail;
|
|
5
5
|
/** The spec revision this library conforms to. */
|
|
6
|
-
exports.SPEC_VERSION = 'spec-0.
|
|
6
|
+
exports.SPEC_VERSION = 'spec-0.2.1';
|
|
7
7
|
/** Every error code defined by SPEC.md 10.2, in the order of that table. */
|
|
8
8
|
exports.ERROR_CODES = [
|
|
9
9
|
'E_PARSE_JSON',
|
|
@@ -17,9 +17,11 @@ exports.ERROR_CODES = [
|
|
|
17
17
|
'E_CANON_PERCENT',
|
|
18
18
|
'E_CANON_TYPE',
|
|
19
19
|
'E_CANON_NUMBER',
|
|
20
|
+
'E_CANON_UNIT',
|
|
20
21
|
'E_CANON_DATE',
|
|
21
22
|
'E_CANON_INSTANT',
|
|
22
23
|
'E_CANON_STRING',
|
|
24
|
+
'E_CANON_BLOB',
|
|
23
25
|
'E_CANON_HEX',
|
|
24
26
|
'E_CANON_LICENCE',
|
|
25
27
|
'E_CANON_SET',
|
|
@@ -43,6 +45,8 @@ exports.ERROR_CODES = [
|
|
|
43
45
|
'E_INPUT_INVALID',
|
|
44
46
|
'E_INPUT_UNDECLARED',
|
|
45
47
|
'E_VALUE_MISMATCH',
|
|
48
|
+
'E_BLOB_MISMATCH',
|
|
49
|
+
'E_ENVELOPE',
|
|
46
50
|
];
|
|
47
51
|
/** A Datumesh rule was broken. `code` is a SPEC.md 10.2 code; `path` locates the failure. */
|
|
48
52
|
class DatumeshError extends Error {
|
package/dist/cjs/index.d.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { type Kind } from './validate.js';
|
|
2
2
|
export { DatumeshError, ERROR_CODES, SPEC_VERSION, type ErrorCode } from './errors.js';
|
|
3
3
|
export { verify, type VerifyResult, type Recompute } from './verify.js';
|
|
4
|
-
export {
|
|
5
|
-
export {
|
|
4
|
+
export { checkEnvelope, type EnvelopeResult, type EnvelopeRequest, type Outcome, type Trust } from './envelope.js';
|
|
5
|
+
export { parseUri, formatUri, httpsUrl, type CellRef, type TypedScalar, type Signature, type Unit, type UnitRef } from './uri.js';
|
|
6
|
+
export { keysFromResponse, keysFromDocument, fetchKeys, FETCH_TIMEOUT_MS, FETCH_MAX_BYTES, type KeysResult, type KeyEntry, type KeyMap, type KeySource } from './keys.js';
|
|
6
7
|
export type { Kind } from './validate.js';
|
|
7
8
|
export type { JsonValue } from './json.js';
|
|
8
9
|
/** Library version. */
|
|
9
|
-
export declare const VERSION = "0.
|
|
10
|
+
export declare const VERSION = "0.2.1";
|
|
10
11
|
/**
|
|
11
12
|
* Validate `input` as `kind` and return its RFC 8785 canonical bytes. `input` is JSON text,
|
|
12
13
|
* UTF-8 bytes, or an already-parsed value. `kind` is "scalar", "cell", "ref", "function" or
|
|
@@ -23,5 +24,14 @@ export declare function functionHash(descriptor: unknown): string;
|
|
|
23
24
|
export declare function receiptHash(receipt: unknown): string;
|
|
24
25
|
/** The signed payload of a receipt (SPEC 8.3): canonical bytes without `sig`. Throws DatumeshError. */
|
|
25
26
|
export declare function receiptPayload(receipt: unknown): Uint8Array;
|
|
27
|
+
/**
|
|
28
|
+
* The unit of a cell's value (SPEC 7.1, units.md section 3): the descriptor's return unit with
|
|
29
|
+
* the cell's argument values substituted, e.g. "USD/EUR"; null when the function does not
|
|
30
|
+
* return a number. `descriptor` must be the cell's function (E_FN_HASH_MISMATCH otherwise).
|
|
31
|
+
* Both are JSON text, bytes or parsed objects. Throws DatumeshError like canonicalize, and
|
|
32
|
+
* E_ARG_MISSING / E_SLICE / E_ARG_TYPE / E_CANON_UNIT when a referenced argument cannot give a
|
|
33
|
+
* unit.
|
|
34
|
+
*/
|
|
35
|
+
export declare function resolvedUnit(descriptor: unknown, cell: unknown): string | null;
|
|
26
36
|
/** Key id of an Ed25519 public key given as 64 lowercase hex digits (SPEC 9.1). Throws DatumeshError. */
|
|
27
37
|
export declare function keyId(publicKeyHex: string): string;
|
package/dist/cjs/index.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.VERSION = exports.FETCH_TIMEOUT_MS = exports.fetchKeys = exports.keysFromDocument = exports.keysFromResponse = exports.httpsUrl = exports.formatUri = exports.parseUri = exports.verify = exports.SPEC_VERSION = exports.ERROR_CODES = exports.DatumeshError = void 0;
|
|
3
|
+
exports.VERSION = exports.FETCH_MAX_BYTES = exports.FETCH_TIMEOUT_MS = exports.fetchKeys = exports.keysFromDocument = exports.keysFromResponse = exports.httpsUrl = exports.formatUri = exports.parseUri = exports.checkEnvelope = exports.verify = exports.SPEC_VERSION = exports.ERROR_CODES = exports.DatumeshError = void 0;
|
|
4
4
|
exports.canonicalize = canonicalize;
|
|
5
5
|
exports.cellHash = cellHash;
|
|
6
6
|
exports.functionHash = functionHash;
|
|
7
7
|
exports.receiptHash = receiptHash;
|
|
8
8
|
exports.receiptPayload = receiptPayload;
|
|
9
|
+
exports.resolvedUnit = resolvedUnit;
|
|
9
10
|
exports.keyId = keyId;
|
|
10
11
|
/**
|
|
11
|
-
* @datumesh/verify: verify Datumesh receipts (SPEC.md, revision spec-0.
|
|
12
|
+
* @datumesh/verify: verify Datumesh receipts (SPEC.md, revision spec-0.2.1).
|
|
12
13
|
*
|
|
13
14
|
* Everything here is synchronous and offline except fetchKeys, the only function that uses the
|
|
14
15
|
* network.
|
|
@@ -25,6 +26,8 @@ Object.defineProperty(exports, "ERROR_CODES", { enumerable: true, get: function
|
|
|
25
26
|
Object.defineProperty(exports, "SPEC_VERSION", { enumerable: true, get: function () { return errors_js_2.SPEC_VERSION; } });
|
|
26
27
|
var verify_js_1 = require("./verify.js");
|
|
27
28
|
Object.defineProperty(exports, "verify", { enumerable: true, get: function () { return verify_js_1.verify; } });
|
|
29
|
+
var envelope_js_1 = require("./envelope.js");
|
|
30
|
+
Object.defineProperty(exports, "checkEnvelope", { enumerable: true, get: function () { return envelope_js_1.checkEnvelope; } });
|
|
28
31
|
var uri_js_1 = require("./uri.js");
|
|
29
32
|
Object.defineProperty(exports, "parseUri", { enumerable: true, get: function () { return uri_js_1.parseUri; } });
|
|
30
33
|
Object.defineProperty(exports, "formatUri", { enumerable: true, get: function () { return uri_js_1.formatUri; } });
|
|
@@ -34,8 +37,9 @@ Object.defineProperty(exports, "keysFromResponse", { enumerable: true, get: func
|
|
|
34
37
|
Object.defineProperty(exports, "keysFromDocument", { enumerable: true, get: function () { return keys_js_1.keysFromDocument; } });
|
|
35
38
|
Object.defineProperty(exports, "fetchKeys", { enumerable: true, get: function () { return keys_js_1.fetchKeys; } });
|
|
36
39
|
Object.defineProperty(exports, "FETCH_TIMEOUT_MS", { enumerable: true, get: function () { return keys_js_1.FETCH_TIMEOUT_MS; } });
|
|
40
|
+
Object.defineProperty(exports, "FETCH_MAX_BYTES", { enumerable: true, get: function () { return keys_js_1.FETCH_MAX_BYTES; } });
|
|
37
41
|
/** Library version. */
|
|
38
|
-
exports.VERSION = '0.
|
|
42
|
+
exports.VERSION = '0.2.1';
|
|
39
43
|
function parseAndValidate(input, kind) {
|
|
40
44
|
if (kind !== undefined && kind !== null && !validate_js_1.KINDS.includes(kind))
|
|
41
45
|
throw new TypeError(`unknown kind ${JSON.stringify(kind)}; expected one of ${validate_js_1.KINDS.join(', ')}`);
|
|
@@ -75,6 +79,21 @@ function receiptPayload(receipt) {
|
|
|
75
79
|
o[k] = r[k];
|
|
76
80
|
return (0, jcs_js_1.jcsBytes)(o);
|
|
77
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* The unit of a cell's value (SPEC 7.1, units.md section 3): the descriptor's return unit with
|
|
84
|
+
* the cell's argument values substituted, e.g. "USD/EUR"; null when the function does not
|
|
85
|
+
* return a number. `descriptor` must be the cell's function (E_FN_HASH_MISMATCH otherwise).
|
|
86
|
+
* Both are JSON text, bytes or parsed objects. Throws DatumeshError like canonicalize, and
|
|
87
|
+
* E_ARG_MISSING / E_SLICE / E_ARG_TYPE / E_CANON_UNIT when a referenced argument cannot give a
|
|
88
|
+
* unit.
|
|
89
|
+
*/
|
|
90
|
+
function resolvedUnit(descriptor, cell) {
|
|
91
|
+
if (descriptor === undefined || cell === undefined)
|
|
92
|
+
throw new TypeError('resolvedUnit(descriptor, cell) needs both arguments');
|
|
93
|
+
const d = (0, json_js_1.toJson)(descriptor);
|
|
94
|
+
const c = (0, json_js_1.toJson)(cell);
|
|
95
|
+
return (0, validate_js_1.cellUnit)(d, c, (x) => (0, hash_js_1.sha256Hex)((0, jcs_js_1.jcsBytes)(x)));
|
|
96
|
+
}
|
|
78
97
|
/** Key id of an Ed25519 public key given as 64 lowercase hex digits (SPEC 9.1). Throws DatumeshError. */
|
|
79
98
|
function keyId(publicKeyHex) {
|
|
80
99
|
if (typeof publicKeyHex !== 'string' || !(0, lexical_js_1.isHex)(publicKeyHex, 64))
|
package/dist/cjs/json.d.ts
CHANGED
|
@@ -3,11 +3,10 @@ export interface JsonObject {
|
|
|
3
3
|
[key: string]: JsonValue;
|
|
4
4
|
}
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* canonicalisation from exhausting the call stack.
|
|
6
|
+
* SPEC 6.1: at most 64 levels of nesting ([ and { open at once, counted outside strings), checked
|
|
7
|
+
* before anything else in the text. Datumesh objects nest at most five levels.
|
|
9
8
|
*/
|
|
10
|
-
export declare const MAX_DEPTH =
|
|
9
|
+
export declare const MAX_DEPTH = 64;
|
|
11
10
|
export declare function isObject(v: unknown): v is JsonObject;
|
|
12
11
|
export declare function newObject(): JsonObject;
|
|
13
12
|
export declare function has(o: JsonObject, k: string): boolean;
|
|
@@ -15,9 +14,9 @@ export declare function has(o: JsonObject, k: string): boolean;
|
|
|
15
14
|
export declare function sortedKeys(o: JsonObject): string[];
|
|
16
15
|
export declare function memberPath(path: string, name: string): string;
|
|
17
16
|
/** Parse JSON text strictly. */
|
|
18
|
-
export declare function parseJsonText(text: string): JsonValue;
|
|
17
|
+
export declare function parseJsonText(text: string, numbers?: Map<string, string>): JsonValue;
|
|
19
18
|
/** Parse UTF-8 JSON bytes strictly: no BOM, no invalid UTF-8. */
|
|
20
|
-
export declare function parseJsonBytes(bytes: Uint8Array): JsonValue;
|
|
19
|
+
export declare function parseJsonBytes(bytes: Uint8Array, numbers?: Map<string, string>): JsonValue;
|
|
21
20
|
/**
|
|
22
21
|
* Check an already-parsed JavaScript value against the JSON data model and return a private
|
|
23
22
|
* deep copy (plain data with null-prototype objects), so later checks cannot be affected by
|
|
@@ -26,4 +25,4 @@ export declare function parseJsonBytes(bytes: Uint8Array): JsonValue;
|
|
|
26
25
|
*/
|
|
27
26
|
export declare function normalizeValue(v: unknown, path?: string, depth?: number): JsonValue;
|
|
28
27
|
/** JSON text (string), UTF-8 bytes (Uint8Array) or an already-parsed value. */
|
|
29
|
-
export declare function toJson(input: unknown): JsonValue;
|
|
28
|
+
export declare function toJson(input: unknown, numbers?: Map<string, string>): JsonValue;
|
package/dist/cjs/json.js
CHANGED
|
@@ -16,11 +16,33 @@ const errors_js_1 = require("./errors.js");
|
|
|
16
16
|
const unicode_js_1 = require("./unicode.js");
|
|
17
17
|
const utf8_js_1 = require("./utf8.js");
|
|
18
18
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* canonicalisation from exhausting the call stack.
|
|
19
|
+
* SPEC 6.1: at most 64 levels of nesting ([ and { open at once, counted outside strings), checked
|
|
20
|
+
* before anything else in the text. Datumesh objects nest at most five levels.
|
|
22
21
|
*/
|
|
23
|
-
exports.MAX_DEPTH =
|
|
22
|
+
exports.MAX_DEPTH = 64;
|
|
23
|
+
/** SPEC 6.1 order step 3: true when more than MAX_DEPTH brackets are open at once. */
|
|
24
|
+
function tooDeep(s) {
|
|
25
|
+
let depth = 0;
|
|
26
|
+
let inString = false;
|
|
27
|
+
for (let i = 0; i < s.length; i++) {
|
|
28
|
+
const c = s.charCodeAt(i);
|
|
29
|
+
if (inString) {
|
|
30
|
+
if (c === 0x5c)
|
|
31
|
+
i++; // skip the escaped character
|
|
32
|
+
else if (c === 0x22)
|
|
33
|
+
inString = false;
|
|
34
|
+
}
|
|
35
|
+
else if (c === 0x22)
|
|
36
|
+
inString = true;
|
|
37
|
+
else if (c === 0x5b || c === 0x7b) {
|
|
38
|
+
if (++depth > exports.MAX_DEPTH)
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
else if (c === 0x5d || c === 0x7d)
|
|
42
|
+
depth--;
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
24
46
|
function isObject(v) {
|
|
25
47
|
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
26
48
|
}
|
|
@@ -38,8 +60,11 @@ function memberPath(path, name) {
|
|
|
38
60
|
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(name) ? `${path}.${name}` : `${path}[${JSON.stringify(name)}]`;
|
|
39
61
|
}
|
|
40
62
|
class Parser {
|
|
41
|
-
constructor(s
|
|
63
|
+
constructor(s,
|
|
64
|
+
/** When given, receives the source text of every number, by JSON path. */
|
|
65
|
+
numbers) {
|
|
42
66
|
this.s = s;
|
|
67
|
+
this.numbers = numbers;
|
|
43
68
|
this.i = 0;
|
|
44
69
|
/** Path of the first string holding an escaped lone surrogate; reported after parsing. */
|
|
45
70
|
this.surrogatePath = null;
|
|
@@ -67,7 +92,7 @@ class Parser {
|
|
|
67
92
|
}
|
|
68
93
|
value(path, depth) {
|
|
69
94
|
if (depth > exports.MAX_DEPTH)
|
|
70
|
-
this.err(`nesting deeper than ${exports.MAX_DEPTH} levels
|
|
95
|
+
this.err(`nesting deeper than ${exports.MAX_DEPTH} levels`); // tooDeep found it first
|
|
71
96
|
const s = this.s;
|
|
72
97
|
const c = s[this.i];
|
|
73
98
|
if (c === '{')
|
|
@@ -89,7 +114,7 @@ class Parser {
|
|
|
89
114
|
return null;
|
|
90
115
|
}
|
|
91
116
|
if (c === '-' || (c >= '0' && c <= '9'))
|
|
92
|
-
return this.number();
|
|
117
|
+
return this.number(path);
|
|
93
118
|
if (c === undefined)
|
|
94
119
|
this.err('unexpected end of input');
|
|
95
120
|
if (s.startsWith('NaN', this.i) || s.startsWith('Infinity', this.i) || s.startsWith('-Infinity', this.i)) {
|
|
@@ -231,7 +256,7 @@ class Parser {
|
|
|
231
256
|
this.surrogatePath = path;
|
|
232
257
|
return out;
|
|
233
258
|
}
|
|
234
|
-
number() {
|
|
259
|
+
number(path) {
|
|
235
260
|
const s = this.s;
|
|
236
261
|
const m = /-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?/y;
|
|
237
262
|
m.lastIndex = this.i;
|
|
@@ -247,30 +272,35 @@ class Parser {
|
|
|
247
272
|
const n = Number(text);
|
|
248
273
|
if (!Number.isFinite(n))
|
|
249
274
|
(0, errors_js_1.fail)('E_PARSE_JSON', `number out of IEEE 754 range: ${text.slice(0, 40)}`, '$');
|
|
275
|
+
this.numbers?.set(path, text);
|
|
250
276
|
return n;
|
|
251
277
|
}
|
|
252
278
|
}
|
|
253
279
|
/** Parse JSON text strictly. */
|
|
254
|
-
function parseJsonText(text) {
|
|
280
|
+
function parseJsonText(text, numbers) {
|
|
281
|
+
// SPEC 6.1 order: BOM, (invalid UTF-8: parseJsonBytes), nesting, the first of syntax / NaN /
|
|
282
|
+
// overflow / duplicate in text order, then lone surrogates.
|
|
255
283
|
if (text.charCodeAt(0) === 0xfeff)
|
|
256
284
|
(0, errors_js_1.fail)('E_PARSE_JSON', 'byte order mark', '$');
|
|
285
|
+
if (tooDeep(text))
|
|
286
|
+
(0, errors_js_1.fail)('E_PARSE_JSON', `nesting deeper than ${exports.MAX_DEPTH} levels`, '$');
|
|
287
|
+
const p = new Parser(text, numbers);
|
|
288
|
+
const v = p.parseDocument();
|
|
257
289
|
if ((0, unicode_js_1.hasLoneSurrogate)(text))
|
|
258
290
|
(0, errors_js_1.fail)('E_PARSE_JSON', 'lone surrogate in the text', '$');
|
|
259
|
-
const p = new Parser(text);
|
|
260
|
-
const v = p.parseDocument();
|
|
261
291
|
if (p.surrogatePath !== null)
|
|
262
292
|
(0, errors_js_1.fail)('E_PARSE_JSON', 'lone surrogate', p.surrogatePath);
|
|
263
293
|
return v;
|
|
264
294
|
}
|
|
265
295
|
/** Parse UTF-8 JSON bytes strictly: no BOM, no invalid UTF-8. */
|
|
266
|
-
function parseJsonBytes(bytes) {
|
|
296
|
+
function parseJsonBytes(bytes, numbers) {
|
|
267
297
|
if (bytes.length >= 3 && bytes[0] === 0xef && bytes[1] === 0xbb && bytes[2] === 0xbf) {
|
|
268
298
|
(0, errors_js_1.fail)('E_PARSE_JSON', 'byte order mark', '$');
|
|
269
299
|
}
|
|
270
300
|
const text = (0, utf8_js_1.utf8Decode)(bytes);
|
|
271
301
|
if (text === null)
|
|
272
302
|
(0, errors_js_1.fail)('E_PARSE_JSON', 'invalid UTF-8', '$');
|
|
273
|
-
return parseJsonText(text);
|
|
303
|
+
return parseJsonText(text, numbers);
|
|
274
304
|
}
|
|
275
305
|
/**
|
|
276
306
|
* Check an already-parsed JavaScript value against the JSON data model and return a private
|
|
@@ -279,7 +309,8 @@ function parseJsonBytes(bytes) {
|
|
|
279
309
|
* or bytes for that.
|
|
280
310
|
*/
|
|
281
311
|
function normalizeValue(v, path = '$', depth = 0) {
|
|
282
|
-
|
|
312
|
+
const container = typeof v === 'object' && v !== null;
|
|
313
|
+
if (container && depth >= exports.MAX_DEPTH)
|
|
283
314
|
(0, errors_js_1.fail)('E_PARSE_JSON', `nesting deeper than ${exports.MAX_DEPTH} levels (or a cycle)`, path);
|
|
284
315
|
if (v === null || typeof v === 'boolean')
|
|
285
316
|
return v;
|
|
@@ -310,12 +341,12 @@ function normalizeValue(v, path = '$', depth = 0) {
|
|
|
310
341
|
(0, errors_js_1.fail)('E_PARSE_JSON', `${typeof v} is not a JSON value`, path);
|
|
311
342
|
}
|
|
312
343
|
/** JSON text (string), UTF-8 bytes (Uint8Array) or an already-parsed value. */
|
|
313
|
-
function toJson(input) {
|
|
344
|
+
function toJson(input, numbers) {
|
|
314
345
|
if (typeof input === 'string')
|
|
315
|
-
return parseJsonText(input);
|
|
346
|
+
return parseJsonText(input, numbers);
|
|
316
347
|
if (input instanceof Uint8Array)
|
|
317
|
-
return parseJsonBytes(input);
|
|
348
|
+
return parseJsonBytes(input, numbers);
|
|
318
349
|
if (input instanceof ArrayBuffer)
|
|
319
|
-
return parseJsonBytes(new Uint8Array(input));
|
|
350
|
+
return parseJsonBytes(new Uint8Array(input), numbers);
|
|
320
351
|
return normalizeValue(input);
|
|
321
352
|
}
|