@datumesh/verify 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +56 -2
  3. package/dist/cjs/ed25519.d.ts +2 -0
  4. package/dist/cjs/ed25519.js +82 -0
  5. package/dist/cjs/envelope.d.ts +38 -0
  6. package/dist/cjs/envelope.js +243 -0
  7. package/dist/cjs/errors.d.ts +14 -0
  8. package/dist/cjs/errors.js +65 -0
  9. package/dist/cjs/hash.d.ts +6 -0
  10. package/dist/cjs/hash.js +27 -0
  11. package/dist/cjs/index.d.ts +37 -0
  12. package/dist/cjs/index.js +101 -0
  13. package/dist/cjs/jcs.d.ts +5 -0
  14. package/dist/cjs/jcs.js +25 -0
  15. package/dist/cjs/json.d.ts +29 -0
  16. package/dist/cjs/json.js +325 -0
  17. package/dist/cjs/keys.d.ts +73 -0
  18. package/dist/cjs/keys.js +257 -0
  19. package/dist/cjs/lexical.d.ts +69 -0
  20. package/dist/cjs/lexical.js +231 -0
  21. package/dist/cjs/package.json +3 -0
  22. package/dist/cjs/unicode-data.d.ts +8 -0
  23. package/dist/cjs/unicode-data.js +14 -0
  24. package/dist/cjs/unicode.d.ts +12 -0
  25. package/dist/cjs/unicode.js +192 -0
  26. package/dist/cjs/units-data.d.ts +4 -0
  27. package/dist/cjs/units-data.js +11 -0
  28. package/dist/cjs/units.d.ts +24 -0
  29. package/dist/cjs/units.js +131 -0
  30. package/dist/cjs/uri.d.ts +43 -0
  31. package/dist/cjs/uri.js +209 -0
  32. package/dist/cjs/utf8.d.ts +10 -0
  33. package/dist/cjs/utf8.js +101 -0
  34. package/dist/cjs/validate.d.ts +25 -0
  35. package/dist/cjs/validate.js +408 -0
  36. package/dist/cjs/verify.d.ts +57 -0
  37. package/dist/cjs/verify.js +232 -0
  38. package/dist/esm/ed25519.d.ts +2 -0
  39. package/dist/esm/ed25519.js +79 -0
  40. package/dist/esm/envelope.d.ts +38 -0
  41. package/dist/esm/envelope.js +240 -0
  42. package/dist/esm/errors.d.ts +14 -0
  43. package/dist/esm/errors.js +60 -0
  44. package/dist/esm/hash.d.ts +6 -0
  45. package/dist/esm/hash.js +21 -0
  46. package/dist/esm/index.d.ts +37 -0
  47. package/dist/esm/index.js +79 -0
  48. package/dist/esm/jcs.d.ts +5 -0
  49. package/dist/esm/jcs.js +18 -0
  50. package/dist/esm/json.d.ts +29 -0
  51. package/dist/esm/json.js +313 -0
  52. package/dist/esm/keys.d.ts +73 -0
  53. package/dist/esm/keys.js +249 -0
  54. package/dist/esm/lexical.d.ts +69 -0
  55. package/dist/esm/lexical.js +209 -0
  56. package/dist/esm/package.json +3 -0
  57. package/dist/esm/unicode-data.d.ts +8 -0
  58. package/dist/esm/unicode-data.js +11 -0
  59. package/dist/esm/unicode.d.ts +12 -0
  60. package/dist/esm/unicode.js +184 -0
  61. package/dist/esm/units-data.d.ts +4 -0
  62. package/dist/esm/units-data.js +8 -0
  63. package/dist/esm/units.d.ts +24 -0
  64. package/dist/esm/units.js +123 -0
  65. package/dist/esm/uri.d.ts +43 -0
  66. package/dist/esm/uri.js +204 -0
  67. package/dist/esm/utf8.d.ts +10 -0
  68. package/dist/esm/utf8.js +96 -0
  69. package/dist/esm/validate.d.ts +25 -0
  70. package/dist/esm/validate.js +397 -0
  71. package/dist/esm/verify.d.ts +57 -0
  72. package/dist/esm/verify.js +227 -0
  73. package/package.json +54 -4
@@ -0,0 +1,232 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Stop = void 0;
4
+ exports.verify = verify;
5
+ exports.verifyEmbedded = verifyEmbedded;
6
+ // Receipt verification (SPEC.md 10.1). Synchronous, offline, no global state, no logging.
7
+ const errors_js_1 = require("./errors.js");
8
+ const ed25519_js_1 = require("./ed25519.js");
9
+ const hash_js_1 = require("./hash.js");
10
+ const json_js_1 = require("./json.js");
11
+ const jcs_js_1 = require("./jcs.js");
12
+ const keys_js_1 = require("./keys.js");
13
+ const lexical_js_1 = require("./lexical.js");
14
+ const validate_js_1 = require("./validate.js");
15
+ class Stop extends Error {
16
+ constructor(code, detail, path) {
17
+ super(detail);
18
+ this.code = code;
19
+ this.detail = detail;
20
+ this.path = path;
21
+ }
22
+ }
23
+ exports.Stop = Stop;
24
+ function stop(code, detail, path = '$') {
25
+ throw new Stop(code, detail, path);
26
+ }
27
+ function withoutSig(r) {
28
+ const o = (0, json_js_1.newObject)();
29
+ for (const k of Object.keys(r))
30
+ if (k !== 'sig')
31
+ o[k] = r[k];
32
+ return o;
33
+ }
34
+ /** Steps 1-6. Throws Stop; returns the parsed receipt and its hash. */
35
+ function verifySigned(input, sources, setHash) {
36
+ let r;
37
+ try {
38
+ r = (0, json_js_1.toJson)(input); // step 1
39
+ (0, validate_js_1.receiptStructure)(r); // step 2
40
+ }
41
+ catch (e) {
42
+ if (e instanceof errors_js_1.DatumeshError)
43
+ stop(e.code, e.detail, e.path);
44
+ throw e;
45
+ }
46
+ const receipt = r;
47
+ const payload = (0, jcs_js_1.jcsBytes)(withoutSig(receipt));
48
+ const hash = (0, hash_js_1.sha256Hex)(payload);
49
+ setHash(hash);
50
+ try {
51
+ (0, validate_js_1.receiptCanonicalAndConsistent)(receipt); // steps 3-4
52
+ }
53
+ catch (e) {
54
+ if (e instanceof errors_js_1.DatumeshError)
55
+ stop(e.code, e.detail, e.path);
56
+ throw e;
57
+ }
58
+ // Step 5.
59
+ const ref = receipt.computed_by;
60
+ const hashAt = ref.indexOf('#');
61
+ const host = ref.slice(0, hashAt);
62
+ const id = ref.slice(hashAt + 1);
63
+ const found = (0, keys_js_1.lookupKey)(sources, host, id, receipt.issued);
64
+ if (!found.ok)
65
+ stop(found.code, found.detail, found.code === 'E_KEY_NOT_VALID' ? '$.issued' : '$.computed_by');
66
+ // Step 6.
67
+ if (!(0, ed25519_js_1.verifyEd25519Strict)((0, hash_js_1.fromHex)(receipt.sig), payload, (0, hash_js_1.fromHex)(found.key.public))) {
68
+ stop('E_SIGNATURE', 'Ed25519 verification failed', '$.sig');
69
+ }
70
+ return { receipt, hash };
71
+ }
72
+ function recompute(receipt, rc, sources) {
73
+ const cell = receipt.cell;
74
+ const cellArgs = cell.args;
75
+ // 7.1 Descriptor.
76
+ if (rc.descriptor === undefined || rc.descriptor === null)
77
+ stop('E_FN_UNAVAILABLE', `no descriptor for ${cell.fn_hash}`, '$.cell.fn_hash');
78
+ let desc;
79
+ try {
80
+ const d = (0, json_js_1.toJson)(rc.descriptor);
81
+ if ((0, hash_js_1.sha256Hex)((0, jcs_js_1.jcsBytes)(d)) !== cell.fn_hash)
82
+ stop('E_FN_HASH_MISMATCH', 'descriptor does not hash to cell.fn_hash', '$.cell.fn_hash');
83
+ (0, validate_js_1.validate)(d, 'function', 'descriptor');
84
+ desc = d;
85
+ }
86
+ catch (e) {
87
+ if (e instanceof errors_js_1.DatumeshError)
88
+ stop(e.code, `descriptor: ${e.detail}`, e.path);
89
+ throw e;
90
+ }
91
+ const sig = desc.signature;
92
+ const declared = sig.args;
93
+ // 7.2 Arguments: unknown, then missing, then types.
94
+ for (const k of (0, json_js_1.sortedKeys)(cellArgs)) {
95
+ if (!(0, json_js_1.has)(declared, k))
96
+ stop('E_ARG_UNKNOWN', `function declares no argument ${JSON.stringify(k)}`, `$.cell.args.${k}`);
97
+ }
98
+ for (const k of (0, json_js_1.sortedKeys)(declared)) {
99
+ if (!(0, json_js_1.has)(cellArgs, k))
100
+ stop('E_ARG_MISSING', `argument ${JSON.stringify(k)} is not bound`, '$.cell.args');
101
+ }
102
+ for (const k of (0, json_js_1.sortedKeys)(cellArgs)) {
103
+ const want = declared[k].type;
104
+ if (cellArgs[k].type !== want)
105
+ stop('E_ARG_TYPE', `${k} is declared ${String(want)}`, `$.cell.args.${k}`);
106
+ }
107
+ // 7.3 valid holds exactly the declared valid-time arguments.
108
+ const validNames = (0, json_js_1.sortedKeys)(receipt.valid);
109
+ const declaredValid = (0, json_js_1.sortedKeys)(declared).filter((k) => declared[k].valid === true);
110
+ if (validNames.join('\u0000') !== declaredValid.join('\u0000')) {
111
+ stop('E_VALID_MISMATCH', 'valid must echo exactly the declared valid-time arguments', '$.valid');
112
+ }
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');
117
+ // 7.5 Inputs.
118
+ const declaredInputs = desc.inputs;
119
+ const supplied = rc.inputs ?? {};
120
+ let latestTx = null;
121
+ for (const h of receipt.inputs) {
122
+ if (!Object.prototype.hasOwnProperty.call(supplied, h))
123
+ stop('E_INPUT_UNAVAILABLE', `input receipt ${h} not available`, '$.inputs');
124
+ let inner;
125
+ try {
126
+ inner = verifySigned(supplied[h], sources, () => undefined);
127
+ }
128
+ catch (e) {
129
+ if (e instanceof Stop)
130
+ stop('E_INPUT_INVALID', `input ${h}: ${e.code}: ${e.detail}`, '$.inputs');
131
+ throw e;
132
+ }
133
+ if (inner.hash !== h)
134
+ stop('E_INPUT_UNAVAILABLE', `supplied receipt does not hash to ${h}`, '$.inputs');
135
+ const ic = inner.receipt.cell;
136
+ if (!declaredInputs.includes(`${ic.ns}/${ic.fn}`)) {
137
+ stop('E_INPUT_UNDECLARED', `input ${h} is a cell of an undeclared function`, '$.inputs');
138
+ }
139
+ if (inner.receipt.asof !== receipt.asof)
140
+ stop('E_TIME_ORDER', "input receipts must share the receipt's asof", '$.inputs');
141
+ const itx = inner.receipt.tx;
142
+ if (latestTx === null || itx > latestTx)
143
+ latestTx = itx;
144
+ }
145
+ if (latestTx !== null && receipt.tx !== latestTx)
146
+ stop('E_TIME_ORDER', 'tx must be the latest input tx', '$.tx');
147
+ // 7.6 Value.
148
+ const v = rc.value;
149
+ if (v.type !== receipt.type || v.value !== receipt.value)
150
+ stop('E_VALUE_MISMATCH', 'recomputed value differs from the receipt', '$.value');
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
+ }
174
+ function checkRecomputeArg(rc) {
175
+ if (rc === undefined || rc === null)
176
+ return null;
177
+ if (typeof rc !== 'object' || Array.isArray(rc))
178
+ throw new TypeError('recompute must be an object {descriptor, inputs, value}');
179
+ const r = rc;
180
+ if (!r.value || typeof r.value !== 'object' || typeof r.value.type !== 'string' || (typeof r.value.value !== 'string' && typeof r.value.value !== 'boolean')) {
181
+ throw new TypeError('recompute.value must be the evaluated typed scalar {type, value}');
182
+ }
183
+ if (r.inputs !== undefined && (typeof r.inputs !== 'object' || r.inputs === null || Array.isArray(r.inputs))) {
184
+ throw new TypeError('recompute.inputs must be an object mapping receipt hash to receipt JSON');
185
+ }
186
+ return r;
187
+ }
188
+ /**
189
+ * Verify a receipt (SPEC 10.1). `receipt` is JSON text, UTF-8 bytes or a parsed object (only
190
+ * text and bytes allow duplicate-member detection). `keys` is a key document, a KeysResult, a
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.
195
+ *
196
+ * Never throws for an invalid receipt or bad key material; throws TypeError only when an
197
+ * argument is of the wrong kind altogether.
198
+ */
199
+ function verify(receipt, keys, recomputeArg, blob) {
200
+ const sources = (0, keys_js_1.collectSources)(keys);
201
+ const rc = checkRecomputeArg(recomputeArg);
202
+ const content = checkBlobArg(blob);
203
+ let hash = null;
204
+ try {
205
+ const { receipt: r } = verifySigned(receipt, sources, (h) => (hash = h));
206
+ if (rc)
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}`;
212
+ return {
213
+ ok: true,
214
+ code: 'OK',
215
+ level: rc ? 'recomputed' : 'signed',
216
+ hash,
217
+ key: r.computed_by,
218
+ detail: checked ? `${what}; blob content matches` : what,
219
+ path: null,
220
+ blobChecked: checked,
221
+ };
222
+ }
223
+ catch (e) {
224
+ if (e instanceof Stop)
225
+ return { ok: false, code: e.code, level: null, hash, key: null, detail: e.detail, path: e.path, blobChecked: false };
226
+ throw e; // a bug in this library, not a property of the receipt
227
+ }
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
+ }
@@ -0,0 +1,2 @@
1
+ /** Strict Ed25519 verification of `sig` (64 bytes) over `msg` with public key `pub` (32 bytes). */
2
+ export declare function verifyEd25519Strict(sig: Uint8Array, msg: Uint8Array, pub: Uint8Array): boolean;
@@ -0,0 +1,79 @@
1
+ // Ed25519 verification as SPEC.md 10.1 step 6 requires (libsodium-strict, RFC 8032 cofactorless):
2
+ // - S < L;
3
+ // - A and R canonically encoded (y < p; x = 0 with the sign bit set is rejected);
4
+ // - A and R not of small order;
5
+ // - [S]B - [k]A, encoded, equals R byte for byte, with k = SHA-512(R || A || M) mod L.
6
+ //
7
+ // @noble/curves supplies the group arithmetic. Its own verify() with zip215:false enforces
8
+ // S < L, canonical A and R, and rejects small-order A, but it accepts small-order R and checks
9
+ // the cofactored equation [8]([S]B - R - [k]A) = 0, which accepts some signatures the
10
+ // cofactorless rule rejects. So the checks are done here, and noble's verify() is additionally
11
+ // required to succeed (it is implied by the cofactorless check; it is a second opinion only).
12
+ import { ed25519 } from '@noble/curves/ed25519';
13
+ import { sha512 } from '@noble/hashes/sha2';
14
+ const Point = ed25519.Point;
15
+ const L = 2n ** 252n + 27742317777372353535851937790883648493n;
16
+ const P = 2n ** 255n - 19n;
17
+ function le(b) {
18
+ let n = 0n;
19
+ for (let i = b.length - 1; i >= 0; i--)
20
+ n = (n << 8n) | BigInt(b[i]);
21
+ return n;
22
+ }
23
+ function decodeStrict(b) {
24
+ const y = le(b) & ((1n << 255n) - 1n);
25
+ if (y >= P)
26
+ return null; // non-canonical encoding
27
+ try {
28
+ return Point.fromBytes(b, false); // also rejects x = 0 with the sign bit set
29
+ }
30
+ catch {
31
+ return null;
32
+ }
33
+ }
34
+ function equalBytes(a, b) {
35
+ if (a.length !== b.length)
36
+ return false;
37
+ let d = 0;
38
+ for (let i = 0; i < a.length; i++)
39
+ d |= a[i] ^ b[i];
40
+ return d === 0;
41
+ }
42
+ /** Strict Ed25519 verification of `sig` (64 bytes) over `msg` with public key `pub` (32 bytes). */
43
+ export function verifyEd25519Strict(sig, msg, pub) {
44
+ if (sig.length !== 64 || pub.length !== 32)
45
+ return false;
46
+ const rBytes = sig.subarray(0, 32);
47
+ const s = le(sig.subarray(32, 64));
48
+ if (s >= L)
49
+ return false;
50
+ const A = decodeStrict(pub);
51
+ const R = decodeStrict(rBytes);
52
+ if (!A || !R)
53
+ return false;
54
+ if (A.isSmallOrder() || R.isSmallOrder())
55
+ return false;
56
+ const h = sha512(concat(rBytes, pub, msg));
57
+ const k = le(h) % L;
58
+ // R' = [S]B - [k]A (multiplyUnsafe accepts 0; inputs are public, so variable time is fine).
59
+ const SB = s === 0n ? Point.ZERO : Point.BASE.multiplyUnsafe(s);
60
+ const kA = k === 0n ? Point.ZERO : A.multiplyUnsafe(k);
61
+ const rCheck = SB.subtract(kA).toBytes();
62
+ if (!equalBytes(rCheck, rBytes))
63
+ return false;
64
+ try {
65
+ return ed25519.verify(sig, msg, pub, { zip215: false });
66
+ }
67
+ catch {
68
+ return false;
69
+ }
70
+ }
71
+ function concat(...parts) {
72
+ const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
73
+ let o = 0;
74
+ for (const p of parts) {
75
+ out.set(p, o);
76
+ o += p.length;
77
+ }
78
+ return out;
79
+ }
@@ -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
+ }
@@ -0,0 +1,14 @@
1
+ /** The spec revision this library conforms to. */
2
+ export declare const SPEC_VERSION = "spec-0.2.0";
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_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
+ export type ErrorCode = (typeof ERROR_CODES)[number];
6
+ /** A Datumesh rule was broken. `code` is a SPEC.md 10.2 code; `path` locates the failure. */
7
+ export declare class DatumeshError extends Error {
8
+ readonly code: ErrorCode;
9
+ readonly path: string | null;
10
+ readonly detail: string;
11
+ constructor(code: ErrorCode, detail: string, path?: string | null);
12
+ }
13
+ /** Shorthand used throughout: throw a DatumeshError. */
14
+ export declare function fail(code: ErrorCode, detail: string, path?: string | null): never;
@@ -0,0 +1,60 @@
1
+ /** The spec revision this library conforms to. */
2
+ export const SPEC_VERSION = 'spec-0.2.0';
3
+ /** Every error code defined by SPEC.md 10.2, in the order of that table. */
4
+ export const ERROR_CODES = [
5
+ 'E_PARSE_JSON',
6
+ 'E_PARSE_DUPLICATE_KEY',
7
+ 'E_PARSE_URI',
8
+ 'E_SCHEMA',
9
+ 'E_CANON_NAME',
10
+ 'E_CANON_ARG_NAME',
11
+ 'E_CANON_ARG_DUPLICATE',
12
+ 'E_CANON_ARG_ORDER',
13
+ 'E_CANON_PERCENT',
14
+ 'E_CANON_TYPE',
15
+ 'E_CANON_NUMBER',
16
+ 'E_CANON_UNIT',
17
+ 'E_CANON_DATE',
18
+ 'E_CANON_INSTANT',
19
+ 'E_CANON_STRING',
20
+ 'E_CANON_BLOB',
21
+ 'E_CANON_HEX',
22
+ 'E_CANON_LICENCE',
23
+ 'E_CANON_SET',
24
+ 'E_CANON_LIMIT',
25
+ 'E_SLICE',
26
+ 'E_PUBLISHER_MISMATCH',
27
+ 'E_VALID_MISMATCH',
28
+ 'E_TIME_ORDER',
29
+ 'E_ARG_UNKNOWN',
30
+ 'E_ARG_MISSING',
31
+ 'E_KEY_UNAVAILABLE',
32
+ 'E_KEY_DOC',
33
+ 'E_KEY_UNKNOWN',
34
+ 'E_KEY_NOT_VALID',
35
+ 'E_SIGNATURE',
36
+ 'E_FN_UNAVAILABLE',
37
+ 'E_FN_HASH_MISMATCH',
38
+ 'E_ARG_TYPE',
39
+ 'E_RETURN_TYPE',
40
+ 'E_INPUT_UNAVAILABLE',
41
+ 'E_INPUT_INVALID',
42
+ 'E_INPUT_UNDECLARED',
43
+ 'E_VALUE_MISMATCH',
44
+ 'E_BLOB_MISMATCH',
45
+ 'E_ENVELOPE',
46
+ ];
47
+ /** A Datumesh rule was broken. `code` is a SPEC.md 10.2 code; `path` locates the failure. */
48
+ export class DatumeshError extends Error {
49
+ constructor(code, detail, path = null) {
50
+ super(`${code}${path ? ` at ${path}` : ''}: ${detail}`);
51
+ this.name = 'DatumeshError';
52
+ this.code = code;
53
+ this.path = path;
54
+ this.detail = detail;
55
+ }
56
+ }
57
+ /** Shorthand used throughout: throw a DatumeshError. */
58
+ export function fail(code, detail, path = null) {
59
+ throw new DatumeshError(code, detail, path);
60
+ }