@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/dist/cjs/verify.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { type ErrorCode } from './errors.js';
|
|
2
|
+
import { type JsonObject, type JsonValue } from './json.js';
|
|
3
|
+
import { type Sources } from './keys.js';
|
|
2
4
|
export interface VerifyResult {
|
|
3
5
|
/** True when the receipt is signature-valid (and, if recompute was given, fully verified). */
|
|
4
6
|
ok: boolean;
|
|
@@ -14,6 +16,8 @@ export interface VerifyResult {
|
|
|
14
16
|
detail: string;
|
|
15
17
|
/** Where the failure was found (a JSON path like `$.cell.args.base`), or null. */
|
|
16
18
|
path: string | null;
|
|
19
|
+
/** True only when blob content was supplied and step 7.7 ran and passed. Does not change `level`. */
|
|
20
|
+
blobChecked: boolean;
|
|
17
21
|
}
|
|
18
22
|
/** Input for SPEC 10.1 step 7. The library does not evaluate functions; `value` is the result. */
|
|
19
23
|
export interface Recompute {
|
|
@@ -27,12 +31,27 @@ export interface Recompute {
|
|
|
27
31
|
value: string | boolean;
|
|
28
32
|
};
|
|
29
33
|
}
|
|
34
|
+
declare class Stop extends Error {
|
|
35
|
+
readonly code: ErrorCode;
|
|
36
|
+
readonly detail: string;
|
|
37
|
+
readonly path: string | null;
|
|
38
|
+
constructor(code: ErrorCode, detail: string, path: string | null);
|
|
39
|
+
}
|
|
30
40
|
/**
|
|
31
41
|
* Verify a receipt (SPEC 10.1). `receipt` is JSON text, UTF-8 bytes or a parsed object (only
|
|
32
42
|
* text and bytes allow duplicate-member detection). `keys` is a key document, a KeysResult, a
|
|
33
|
-
* {keyId: publicKeyHex} map, or an array of these. With `recompute`, step 7 runs too.
|
|
43
|
+
* {keyId: publicKeyHex} map, or an array of these. With `recompute`, step 7 runs too. With
|
|
44
|
+
* `blob`, the content of a blob value, step 7.7 runs last (after step 7 when recomputing,
|
|
45
|
+
* otherwise right after step 6): E_SCHEMA if the value is not a blob, E_BLOB_MISMATCH if the
|
|
46
|
+
* content does not match. It does not change `level`; `blobChecked` reports it.
|
|
34
47
|
*
|
|
35
48
|
* Never throws for an invalid receipt or bad key material; throws TypeError only when an
|
|
36
49
|
* argument is of the wrong kind altogether.
|
|
37
50
|
*/
|
|
38
|
-
export declare function verify(receipt: unknown, keys: unknown, recomputeArg?: Recompute | null): VerifyResult;
|
|
51
|
+
export declare function verify(receipt: unknown, keys: unknown, recomputeArg?: Recompute | null, blob?: Uint8Array | ArrayBuffer | null): VerifyResult;
|
|
52
|
+
/** Steps 1-6 for an already-parsed receipt (used by checkEnvelope). Throws a Stop. */
|
|
53
|
+
export declare function verifyEmbedded(receipt: JsonValue, sources: Sources): {
|
|
54
|
+
receipt: JsonObject;
|
|
55
|
+
hash: string;
|
|
56
|
+
};
|
|
57
|
+
export { Stop };
|
package/dist/cjs/verify.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Stop = void 0;
|
|
3
4
|
exports.verify = verify;
|
|
5
|
+
exports.verifyEmbedded = verifyEmbedded;
|
|
4
6
|
// Receipt verification (SPEC.md 10.1). Synchronous, offline, no global state, no logging.
|
|
5
7
|
const errors_js_1 = require("./errors.js");
|
|
6
8
|
const ed25519_js_1 = require("./ed25519.js");
|
|
@@ -8,6 +10,7 @@ const hash_js_1 = require("./hash.js");
|
|
|
8
10
|
const json_js_1 = require("./json.js");
|
|
9
11
|
const jcs_js_1 = require("./jcs.js");
|
|
10
12
|
const keys_js_1 = require("./keys.js");
|
|
13
|
+
const lexical_js_1 = require("./lexical.js");
|
|
11
14
|
const validate_js_1 = require("./validate.js");
|
|
12
15
|
class Stop extends Error {
|
|
13
16
|
constructor(code, detail, path) {
|
|
@@ -17,6 +20,7 @@ class Stop extends Error {
|
|
|
17
20
|
this.path = path;
|
|
18
21
|
}
|
|
19
22
|
}
|
|
23
|
+
exports.Stop = Stop;
|
|
20
24
|
function stop(code, detail, path = '$') {
|
|
21
25
|
throw new Stop(code, detail, path);
|
|
22
26
|
}
|
|
@@ -106,9 +110,10 @@ function recompute(receipt, rc, sources) {
|
|
|
106
110
|
if (validNames.join('\u0000') !== declaredValid.join('\u0000')) {
|
|
107
111
|
stop('E_VALID_MISMATCH', 'valid must echo exactly the declared valid-time arguments', '$.valid');
|
|
108
112
|
}
|
|
109
|
-
// 7.4 Return type.
|
|
110
|
-
|
|
111
|
-
|
|
113
|
+
// 7.4 Return type: `returns`, or its `type` member when it is an object.
|
|
114
|
+
const rt = (0, validate_js_1.returnType)(sig);
|
|
115
|
+
if (receipt.type !== rt)
|
|
116
|
+
stop('E_RETURN_TYPE', `function returns ${rt}`, '$.type');
|
|
112
117
|
// 7.5 Inputs.
|
|
113
118
|
const declaredInputs = desc.inputs;
|
|
114
119
|
const supplied = rc.inputs ?? {};
|
|
@@ -144,6 +149,28 @@ function recompute(receipt, rc, sources) {
|
|
|
144
149
|
if (v.type !== receipt.type || v.value !== receipt.value)
|
|
145
150
|
stop('E_VALUE_MISMATCH', 'recomputed value differs from the receipt', '$.value');
|
|
146
151
|
}
|
|
152
|
+
/**
|
|
153
|
+
* Step 7.7: content is only for a blob value (E_SCHEMA otherwise); it has exactly `length`
|
|
154
|
+
* bytes, then a SHA-256 equal to the blob's hash (E_BLOB_MISMATCH).
|
|
155
|
+
*/
|
|
156
|
+
function checkContent(receipt, content) {
|
|
157
|
+
if (receipt.type !== 'blob')
|
|
158
|
+
stop('E_SCHEMA', `content was supplied, but the value is a ${String(receipt.type)}, not a blob`, '$.type');
|
|
159
|
+
const blob = (0, lexical_js_1.checkBlob)(receipt.value, '$.value'); // already checked in step 3
|
|
160
|
+
if (content.length !== blob.length)
|
|
161
|
+
stop('E_BLOB_MISMATCH', `content is ${content.length} bytes, the blob says ${blob.length}`, '$.value');
|
|
162
|
+
if ((0, hash_js_1.sha256Hex)(content) !== blob.sha256)
|
|
163
|
+
stop('E_BLOB_MISMATCH', "the content's SHA-256 is not the blob's hash", '$.value');
|
|
164
|
+
}
|
|
165
|
+
function checkBlobArg(b) {
|
|
166
|
+
if (b === undefined || b === null)
|
|
167
|
+
return null;
|
|
168
|
+
if (b instanceof Uint8Array)
|
|
169
|
+
return b;
|
|
170
|
+
if (b instanceof ArrayBuffer)
|
|
171
|
+
return new Uint8Array(b);
|
|
172
|
+
throw new TypeError('blob must be the content bytes (Uint8Array or ArrayBuffer)');
|
|
173
|
+
}
|
|
147
174
|
function checkRecomputeArg(rc) {
|
|
148
175
|
if (rc === undefined || rc === null)
|
|
149
176
|
return null;
|
|
@@ -161,32 +188,45 @@ function checkRecomputeArg(rc) {
|
|
|
161
188
|
/**
|
|
162
189
|
* Verify a receipt (SPEC 10.1). `receipt` is JSON text, UTF-8 bytes or a parsed object (only
|
|
163
190
|
* text and bytes allow duplicate-member detection). `keys` is a key document, a KeysResult, a
|
|
164
|
-
* {keyId: publicKeyHex} map, or an array of these. With `recompute`, step 7 runs too.
|
|
191
|
+
* {keyId: publicKeyHex} map, or an array of these. With `recompute`, step 7 runs too. With
|
|
192
|
+
* `blob`, the content of a blob value, step 7.7 runs last (after step 7 when recomputing,
|
|
193
|
+
* otherwise right after step 6): E_SCHEMA if the value is not a blob, E_BLOB_MISMATCH if the
|
|
194
|
+
* content does not match. It does not change `level`; `blobChecked` reports it.
|
|
165
195
|
*
|
|
166
196
|
* Never throws for an invalid receipt or bad key material; throws TypeError only when an
|
|
167
197
|
* argument is of the wrong kind altogether.
|
|
168
198
|
*/
|
|
169
|
-
function verify(receipt, keys, recomputeArg) {
|
|
199
|
+
function verify(receipt, keys, recomputeArg, blob) {
|
|
170
200
|
const sources = (0, keys_js_1.collectSources)(keys);
|
|
171
201
|
const rc = checkRecomputeArg(recomputeArg);
|
|
202
|
+
const content = checkBlobArg(blob);
|
|
172
203
|
let hash = null;
|
|
173
204
|
try {
|
|
174
205
|
const { receipt: r } = verifySigned(receipt, sources, (h) => (hash = h));
|
|
175
206
|
if (rc)
|
|
176
207
|
recompute(r, rc, sources);
|
|
208
|
+
const checked = content !== null;
|
|
209
|
+
if (checked)
|
|
210
|
+
checkContent(r, content);
|
|
211
|
+
const what = rc ? 'fully verified: signature valid and value recomputed' : `signature-valid: signed by ${r.computed_by}`;
|
|
177
212
|
return {
|
|
178
213
|
ok: true,
|
|
179
214
|
code: 'OK',
|
|
180
215
|
level: rc ? 'recomputed' : 'signed',
|
|
181
216
|
hash,
|
|
182
217
|
key: r.computed_by,
|
|
183
|
-
detail:
|
|
218
|
+
detail: checked ? `${what}; blob content matches` : what,
|
|
184
219
|
path: null,
|
|
220
|
+
blobChecked: checked,
|
|
185
221
|
};
|
|
186
222
|
}
|
|
187
223
|
catch (e) {
|
|
188
224
|
if (e instanceof Stop)
|
|
189
|
-
return { ok: false, code: e.code, level: null, hash, key: null, detail: e.detail, path: e.path };
|
|
225
|
+
return { ok: false, code: e.code, level: null, hash, key: null, detail: e.detail, path: e.path, blobChecked: false };
|
|
190
226
|
throw e; // a bug in this library, not a property of the receipt
|
|
191
227
|
}
|
|
192
228
|
}
|
|
229
|
+
/** Steps 1-6 for an already-parsed receipt (used by checkEnvelope). Throws a Stop. */
|
|
230
|
+
function verifyEmbedded(receipt, sources) {
|
|
231
|
+
return verifySigned(receipt, sources, () => undefined);
|
|
232
|
+
}
|
package/dist/esm/ed25519.js
CHANGED
|
@@ -56,6 +56,8 @@ export function verifyEd25519Strict(sig, msg, pub) {
|
|
|
56
56
|
const h = sha512(concat(rBytes, pub, msg));
|
|
57
57
|
const k = le(h) % L;
|
|
58
58
|
// R' = [S]B - [k]A (multiplyUnsafe accepts 0; inputs are public, so variable time is fine).
|
|
59
|
+
// subtract() adds the negation of the point [k]A, as SPEC 10.1 step 6 requires; it is not
|
|
60
|
+
// [L - k]A, which differs for a mixed-order A (receipt/ed25519-mixed-order-key).
|
|
59
61
|
const SB = s === 0n ? Point.ZERO : Point.BASE.multiplyUnsafe(s);
|
|
60
62
|
const kA = k === 0n ? Point.ZERO : A.multiplyUnsafe(k);
|
|
61
63
|
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,240 @@
|
|
|
1
|
+
// Response envelopes (SPEC.md 5.5): the unsigned JSON body a mesh returns around a receipt.
|
|
2
|
+
// Synchronous, offline; the embedded receipt is verified through step 6 with the given keys.
|
|
3
|
+
import { DatumeshError, ERROR_CODES } from './errors.js';
|
|
4
|
+
import { has, isObject, memberPath, sortedKeys, toJson } from './json.js';
|
|
5
|
+
import { LIMITS, URL_RE, checkNumber, checkString, isHex, isInstant } from './lexical.js';
|
|
6
|
+
import { sha256Hex } from './hash.js';
|
|
7
|
+
import { jcsBytes } from './jcs.js';
|
|
8
|
+
import { collectSources } from './keys.js';
|
|
9
|
+
import { isCurrency } from './units.js';
|
|
10
|
+
import { Stop, verifyEmbedded } from './verify.js';
|
|
11
|
+
/** SPEC 5.5: required and optional members per outcome (besides `outcome`). */
|
|
12
|
+
const MEMBERS = {
|
|
13
|
+
ok: { required: ['receipt', 'trust'], optional: ['superseded_by', 'reason'] },
|
|
14
|
+
stale: { required: ['receipt', 'trust', 'age_seconds'], optional: ['superseded_by', 'reason'] },
|
|
15
|
+
not_covered: { required: [], optional: [] },
|
|
16
|
+
unlicensed: { required: [], optional: [] },
|
|
17
|
+
payment_required: { required: ['quote'], optional: [] },
|
|
18
|
+
rate_limited: { required: ['retry_after_seconds'], optional: [] },
|
|
19
|
+
error: { required: ['error'], optional: [] },
|
|
20
|
+
bad_request: { required: ['error'], optional: [] },
|
|
21
|
+
};
|
|
22
|
+
const STATUS = {
|
|
23
|
+
ok: (s) => s === 200,
|
|
24
|
+
stale: (s) => s === 200,
|
|
25
|
+
not_covered: (s) => s === 404,
|
|
26
|
+
unlicensed: (s) => s === 403,
|
|
27
|
+
payment_required: (s) => s === 402,
|
|
28
|
+
rate_limited: (s) => s === 429,
|
|
29
|
+
error: (s) => s >= 500 && s <= 599,
|
|
30
|
+
bad_request: (s) => s === 400,
|
|
31
|
+
};
|
|
32
|
+
/** SPEC 5.5: the codes `parse` can raise (5.4), the only ones a bad_request carries. */
|
|
33
|
+
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'];
|
|
34
|
+
const TRUST = ['primary', 'verified', 'unverified'];
|
|
35
|
+
const MAX_SECONDS = 9007199254740991;
|
|
36
|
+
class Bad extends Error {
|
|
37
|
+
constructor(detail, path) {
|
|
38
|
+
super(detail);
|
|
39
|
+
this.detail = detail;
|
|
40
|
+
this.path = path;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function bad(detail, path) {
|
|
44
|
+
throw new Bad(detail, path);
|
|
45
|
+
}
|
|
46
|
+
/** A rule of 3.x checked inside an envelope: its own code becomes part of the E_ENVELOPE detail. */
|
|
47
|
+
function inner(f, path) {
|
|
48
|
+
try {
|
|
49
|
+
f();
|
|
50
|
+
}
|
|
51
|
+
catch (e) {
|
|
52
|
+
if (e instanceof DatumeshError)
|
|
53
|
+
bad(`${e.code}: ${e.detail}`, path);
|
|
54
|
+
throw e;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* SPEC 5.5 integer members: written 0 or [1-9][0-9]*, at most 2^53 - 1. The source text decides
|
|
59
|
+
* (`30.0`, `3e1` and `-0` are rejected); for an already-parsed body, whose text is gone, the
|
|
60
|
+
* value must be a non-negative safe integer other than -0.
|
|
61
|
+
*/
|
|
62
|
+
function seconds(v, text, name, path) {
|
|
63
|
+
const why = `${name} is written 0 or [1-9][0-9]*, at most 2^53 - 1`;
|
|
64
|
+
if (typeof v !== 'number')
|
|
65
|
+
bad(why, path);
|
|
66
|
+
if (text !== undefined ? !/^(0|[1-9][0-9]*)$/.test(text) : !Number.isSafeInteger(v) || Object.is(v, -0))
|
|
67
|
+
bad(why, path);
|
|
68
|
+
if (v < 0 || v > MAX_SECONDS)
|
|
69
|
+
bad(why, path);
|
|
70
|
+
}
|
|
71
|
+
function quote(v, path) {
|
|
72
|
+
if (!isObject(v))
|
|
73
|
+
bad('quote is an object {amount, authorise, currency}', path);
|
|
74
|
+
for (const k of ['amount', 'authorise', 'currency'])
|
|
75
|
+
if (!has(v, k))
|
|
76
|
+
bad(`quote has no ${k}`, path);
|
|
77
|
+
for (const k of sortedKeys(v))
|
|
78
|
+
if (!['amount', 'authorise', 'currency'].includes(k))
|
|
79
|
+
bad(`quote has an unexpected member ${JSON.stringify(k)}`, path);
|
|
80
|
+
const amount = v.amount;
|
|
81
|
+
const ap = memberPath(path, 'amount');
|
|
82
|
+
if (typeof amount !== 'string')
|
|
83
|
+
bad('amount is a decimal string (SPEC 3.1)', ap);
|
|
84
|
+
inner(() => checkNumber(amount, ap), ap);
|
|
85
|
+
if (amount.startsWith('-') || amount === '0')
|
|
86
|
+
bad('amount is greater than zero', ap);
|
|
87
|
+
const url = v.authorise;
|
|
88
|
+
const up = memberPath(path, 'authorise');
|
|
89
|
+
if (typeof url !== 'string' || !URL_RE.test(url) || url.length > LIMITS.licence_url)
|
|
90
|
+
bad('authorise is an https:// URL in the form of a licence URL (SPEC 8.1)', up);
|
|
91
|
+
if (!isCurrency(v.currency))
|
|
92
|
+
bad('currency is a currency code from units.md', memberPath(path, 'currency'));
|
|
93
|
+
}
|
|
94
|
+
/** Step 2: members against the table and their rules. Returns the outcome. */
|
|
95
|
+
function members(o, numbers) {
|
|
96
|
+
if (!isObject(o) || typeof o.outcome !== 'string')
|
|
97
|
+
bad('an envelope is an object with a string outcome', '$');
|
|
98
|
+
const outcome = o.outcome;
|
|
99
|
+
if (!Object.prototype.hasOwnProperty.call(MEMBERS, outcome))
|
|
100
|
+
bad(`unknown outcome ${JSON.stringify(outcome)}`, '$.outcome');
|
|
101
|
+
const { required, optional } = MEMBERS[outcome];
|
|
102
|
+
for (const k of required)
|
|
103
|
+
if (!has(o, k))
|
|
104
|
+
bad(`outcome ${outcome} requires ${k}`, '$');
|
|
105
|
+
for (const k of sortedKeys(o))
|
|
106
|
+
if (k !== 'outcome' && !required.includes(k) && !optional.includes(k))
|
|
107
|
+
bad(`member ${JSON.stringify(k)} is not allowed with outcome ${outcome}`, memberPath('$', k));
|
|
108
|
+
if (has(o, 'reason') && !has(o, 'superseded_by'))
|
|
109
|
+
bad('reason is only present with superseded_by', '$.reason');
|
|
110
|
+
for (const k of sortedKeys(o)) {
|
|
111
|
+
const v = o[k];
|
|
112
|
+
const p = memberPath('$', k);
|
|
113
|
+
switch (k) {
|
|
114
|
+
case 'age_seconds':
|
|
115
|
+
case 'retry_after_seconds':
|
|
116
|
+
seconds(v, numbers.get(p), k, p);
|
|
117
|
+
break;
|
|
118
|
+
case 'error':
|
|
119
|
+
if (outcome === 'bad_request') {
|
|
120
|
+
if (typeof v !== 'string' || !PARSE_CODES.includes(v))
|
|
121
|
+
bad('a bad_request carries a code that parsing a cell URI raises (SPEC 5.4)', p);
|
|
122
|
+
}
|
|
123
|
+
else if (typeof v !== 'string' || v === 'E_ENVELOPE' || !ERROR_CODES.includes(v)) {
|
|
124
|
+
bad('error is a code from SPEC.md 10.2 other than E_ENVELOPE', p);
|
|
125
|
+
}
|
|
126
|
+
break;
|
|
127
|
+
case 'quote':
|
|
128
|
+
quote(v, p);
|
|
129
|
+
break;
|
|
130
|
+
case 'reason':
|
|
131
|
+
if (typeof v !== 'string')
|
|
132
|
+
bad('reason is a string', p);
|
|
133
|
+
if (v === '')
|
|
134
|
+
bad('reason must not be empty', p);
|
|
135
|
+
inner(() => checkString(v, p, { limit: LIMITS.summary }), p);
|
|
136
|
+
break;
|
|
137
|
+
case 'receipt':
|
|
138
|
+
if (!isObject(v))
|
|
139
|
+
bad('receipt is a receipt object', p);
|
|
140
|
+
break;
|
|
141
|
+
case 'superseded_by':
|
|
142
|
+
if (typeof v !== 'string' || !isHex(v, 64))
|
|
143
|
+
bad('superseded_by is a receipt hash: 64 lowercase hex digits', p);
|
|
144
|
+
break;
|
|
145
|
+
case 'trust':
|
|
146
|
+
if (typeof v !== 'string' || !TRUST.includes(v))
|
|
147
|
+
bad('trust is one of primary, verified, unverified', p);
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return outcome;
|
|
152
|
+
}
|
|
153
|
+
function checkRequest(request) {
|
|
154
|
+
const ok = isObject(request) &&
|
|
155
|
+
typeof request.cell_id === 'string' &&
|
|
156
|
+
isHex(request.cell_id, 64) &&
|
|
157
|
+
Object.keys(request).every((k) => k === 'cell_id' || k === 'asof') &&
|
|
158
|
+
(!has(request, 'asof') || (typeof request.asof === 'string' && isInstant(request.asof)));
|
|
159
|
+
if (!ok)
|
|
160
|
+
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');
|
|
161
|
+
}
|
|
162
|
+
const hostOf = (s, sep) => {
|
|
163
|
+
const i = s.indexOf(sep);
|
|
164
|
+
return i < 0 ? s : s.slice(0, i);
|
|
165
|
+
};
|
|
166
|
+
/**
|
|
167
|
+
* Check a mesh response (SPEC 5.5): `status` is the HTTP status, `body` the response body as
|
|
168
|
+
* JSON text or bytes (a parsed object is accepted too, without duplicate-member detection), and
|
|
169
|
+
* `keys` any key source `verify` accepts, used to verify an embedded receipt through step 6.
|
|
170
|
+
* `request` is what the client asked for: `{cell_id, asof?}`, the id (SPEC 4.4) of the requested
|
|
171
|
+
* cell resolved to the fn_hash the client used, and the requested asof if there was one.
|
|
172
|
+
* Steps: strict parse; members (E_ENVELOPE); status against outcome (E_ENVELOPE); the receipt
|
|
173
|
+
* (its own codes); the receipt answers the request (E_ENVELOPE); then `trust` against the receipt
|
|
174
|
+
* and `superseded_by` against its hash (E_ENVELOPE).
|
|
175
|
+
*
|
|
176
|
+
* Never throws for an invalid body or bad key material; throws TypeError only when `status` is
|
|
177
|
+
* not an integer, `request` is malformed, or `keys` is not a key source at all.
|
|
178
|
+
*/
|
|
179
|
+
export function checkEnvelope(status, body, keys, request) {
|
|
180
|
+
if (typeof status !== 'number' || !Number.isInteger(status))
|
|
181
|
+
throw new TypeError('status must be the HTTP status as an integer');
|
|
182
|
+
checkRequest(request);
|
|
183
|
+
const sources = collectSources(keys);
|
|
184
|
+
const fail = (code, detail, path, receiptHash = null) => ({ ok: false, code, outcome: null, trust: null, receiptHash, detail, path });
|
|
185
|
+
let o;
|
|
186
|
+
const numbers = new Map();
|
|
187
|
+
try {
|
|
188
|
+
o = toJson(body, numbers); // step 1
|
|
189
|
+
}
|
|
190
|
+
catch (e) {
|
|
191
|
+
if (e instanceof DatumeshError)
|
|
192
|
+
return fail(e.code, e.detail, e.path);
|
|
193
|
+
throw e;
|
|
194
|
+
}
|
|
195
|
+
let outcome;
|
|
196
|
+
try {
|
|
197
|
+
outcome = members(o, numbers); // step 2
|
|
198
|
+
if (!STATUS[outcome](status))
|
|
199
|
+
bad(`status ${status} does not match outcome ${outcome}`, '$.outcome'); // step 3
|
|
200
|
+
}
|
|
201
|
+
catch (e) {
|
|
202
|
+
if (e instanceof Bad)
|
|
203
|
+
return fail('E_ENVELOPE', e.detail, e.path);
|
|
204
|
+
throw e;
|
|
205
|
+
}
|
|
206
|
+
const env = o;
|
|
207
|
+
if (!has(env, 'receipt'))
|
|
208
|
+
return { ok: true, code: 'OK', outcome, trust: null, receiptHash: null, detail: `outcome ${outcome}`, path: null };
|
|
209
|
+
// Step 4.
|
|
210
|
+
let r;
|
|
211
|
+
try {
|
|
212
|
+
r = verifyEmbedded(env.receipt, sources);
|
|
213
|
+
}
|
|
214
|
+
catch (e) {
|
|
215
|
+
if (e instanceof Stop)
|
|
216
|
+
return fail(e.code, `receipt: ${e.detail}`, e.path === null ? '$.receipt' : e.path.replace(/^\$/, '$.receipt'));
|
|
217
|
+
throw e;
|
|
218
|
+
}
|
|
219
|
+
// Step 5: the receipt answers the request.
|
|
220
|
+
const cellId = sha256Hex(jcsBytes(r.receipt.cell));
|
|
221
|
+
if (cellId !== request.cell_id)
|
|
222
|
+
return fail('E_ENVELOPE', `the receipt is for cell ${cellId}, not the requested ${request.cell_id}`, '$.receipt.cell', r.hash);
|
|
223
|
+
// With a requested asof: equal for ok; at or before it for stale (D31). Instants are fixed
|
|
224
|
+
// width, so string order is time order (SPEC 3.3).
|
|
225
|
+
const asof = r.receipt.asof;
|
|
226
|
+
if (request.asof !== undefined && (outcome === 'stale' ? !(asof <= request.asof) : asof !== request.asof)) {
|
|
227
|
+
const want = outcome === 'stale' ? `at or before the requested ${request.asof}` : `the requested ${request.asof}`;
|
|
228
|
+
return fail('E_ENVELOPE', `the receipt answers asof ${asof}, not ${want}`, '$.receipt.asof', r.hash);
|
|
229
|
+
}
|
|
230
|
+
// Step 6.
|
|
231
|
+
const trust = env.trust;
|
|
232
|
+
const cell = r.receipt.cell;
|
|
233
|
+
const primary = hostOf(r.receipt.computed_by, '#') === hostOf(cell.ns, '/') && r.receipt.inputs.length === 0;
|
|
234
|
+
if ((trust === 'primary') !== primary) {
|
|
235
|
+
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);
|
|
236
|
+
}
|
|
237
|
+
if (env.superseded_by === r.hash)
|
|
238
|
+
return fail('E_ENVELOPE', 'superseded_by is the receipt itself', '$.superseded_by', r.hash);
|
|
239
|
+
return { ok: true, code: 'OK', outcome, trust, receiptHash: r.hash, detail: `outcome ${outcome}, trust ${trust}, receipt signed by ${r.receipt.computed_by}`, path: null };
|
|
240
|
+
}
|
package/dist/esm/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/esm/errors.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** The spec revision this library conforms to. */
|
|
2
|
-
export const SPEC_VERSION = 'spec-0.
|
|
2
|
+
export 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
4
|
export const ERROR_CODES = [
|
|
5
5
|
'E_PARSE_JSON',
|
|
@@ -13,9 +13,11 @@ export const ERROR_CODES = [
|
|
|
13
13
|
'E_CANON_PERCENT',
|
|
14
14
|
'E_CANON_TYPE',
|
|
15
15
|
'E_CANON_NUMBER',
|
|
16
|
+
'E_CANON_UNIT',
|
|
16
17
|
'E_CANON_DATE',
|
|
17
18
|
'E_CANON_INSTANT',
|
|
18
19
|
'E_CANON_STRING',
|
|
20
|
+
'E_CANON_BLOB',
|
|
19
21
|
'E_CANON_HEX',
|
|
20
22
|
'E_CANON_LICENCE',
|
|
21
23
|
'E_CANON_SET',
|
|
@@ -39,6 +41,8 @@ export const ERROR_CODES = [
|
|
|
39
41
|
'E_INPUT_INVALID',
|
|
40
42
|
'E_INPUT_UNDECLARED',
|
|
41
43
|
'E_VALUE_MISMATCH',
|
|
44
|
+
'E_BLOB_MISMATCH',
|
|
45
|
+
'E_ENVELOPE',
|
|
42
46
|
];
|
|
43
47
|
/** A Datumesh rule was broken. `code` is a SPEC.md 10.2 code; `path` locates the failure. */
|
|
44
48
|
export class DatumeshError extends Error {
|
package/dist/esm/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/esm/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @datumesh/verify: verify Datumesh receipts (SPEC.md, revision spec-0.
|
|
2
|
+
* @datumesh/verify: verify Datumesh receipts (SPEC.md, revision spec-0.2.1).
|
|
3
3
|
*
|
|
4
4
|
* Everything here is synchronous and offline except fetchKeys, the only function that uses the
|
|
5
5
|
* network.
|
|
@@ -9,13 +9,14 @@ import { sha256Hex, keyIdOf, fromHex } from './hash.js';
|
|
|
9
9
|
import { newObject, toJson } from './json.js';
|
|
10
10
|
import { jcsBytes } from './jcs.js';
|
|
11
11
|
import { isHex } from './lexical.js';
|
|
12
|
-
import { KINDS, inferKind, validate } from './validate.js';
|
|
12
|
+
import { KINDS, cellUnit, inferKind, validate } from './validate.js';
|
|
13
13
|
export { DatumeshError, ERROR_CODES, SPEC_VERSION } from './errors.js';
|
|
14
14
|
export { verify } from './verify.js';
|
|
15
|
+
export { checkEnvelope } from './envelope.js';
|
|
15
16
|
export { parseUri, formatUri, httpsUrl } from './uri.js';
|
|
16
|
-
export { keysFromResponse, keysFromDocument, fetchKeys, FETCH_TIMEOUT_MS } from './keys.js';
|
|
17
|
+
export { keysFromResponse, keysFromDocument, fetchKeys, FETCH_TIMEOUT_MS, FETCH_MAX_BYTES } from './keys.js';
|
|
17
18
|
/** Library version. */
|
|
18
|
-
export const VERSION = '0.
|
|
19
|
+
export const VERSION = '0.2.1';
|
|
19
20
|
function parseAndValidate(input, kind) {
|
|
20
21
|
if (kind !== undefined && kind !== null && !KINDS.includes(kind))
|
|
21
22
|
throw new TypeError(`unknown kind ${JSON.stringify(kind)}; expected one of ${KINDS.join(', ')}`);
|
|
@@ -55,6 +56,21 @@ export function receiptPayload(receipt) {
|
|
|
55
56
|
o[k] = r[k];
|
|
56
57
|
return jcsBytes(o);
|
|
57
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* The unit of a cell's value (SPEC 7.1, units.md section 3): the descriptor's return unit with
|
|
61
|
+
* the cell's argument values substituted, e.g. "USD/EUR"; null when the function does not
|
|
62
|
+
* return a number. `descriptor` must be the cell's function (E_FN_HASH_MISMATCH otherwise).
|
|
63
|
+
* Both are JSON text, bytes or parsed objects. Throws DatumeshError like canonicalize, and
|
|
64
|
+
* E_ARG_MISSING / E_SLICE / E_ARG_TYPE / E_CANON_UNIT when a referenced argument cannot give a
|
|
65
|
+
* unit.
|
|
66
|
+
*/
|
|
67
|
+
export function resolvedUnit(descriptor, cell) {
|
|
68
|
+
if (descriptor === undefined || cell === undefined)
|
|
69
|
+
throw new TypeError('resolvedUnit(descriptor, cell) needs both arguments');
|
|
70
|
+
const d = toJson(descriptor);
|
|
71
|
+
const c = toJson(cell);
|
|
72
|
+
return cellUnit(d, c, (x) => sha256Hex(jcsBytes(x)));
|
|
73
|
+
}
|
|
58
74
|
/** Key id of an Ed25519 public key given as 64 lowercase hex digits (SPEC 9.1). Throws DatumeshError. */
|
|
59
75
|
export function keyId(publicKeyHex) {
|
|
60
76
|
if (typeof publicKeyHex !== 'string' || !isHex(publicKeyHex, 64))
|
package/dist/esm/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;
|