@datumesh/verify 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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.2.1` and passing all of its test vectors.
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.0` and passing all of its test vectors.
4
4
 
5
5
  ```sh
6
6
  npm install @datumesh/verify
@@ -31,10 +31,10 @@ console.log(verify(vector.input, keys).code); // OK
31
31
 
32
32
  ## What it does
33
33
 
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).
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). If you could not evaluate it, leave out the value: when the descriptor, arguments and inputs check out, the result is `ok` at level `"signed"` with `code` `"E_EVAL_UNAVAILABLE"`, the one code reported with success. 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
- - `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.
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 receipt's `asof` must be at or before the requested one, for `ok` as for `stale`. The envelope itself is unsigned: that an `ok` receipt is the latest version, `stale`, `superseded_by` and `verified` remain the mesh's claims.
38
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
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`).
40
40
 
@@ -223,12 +223,11 @@ function checkEnvelope(status, body, keys, request) {
223
223
  const cellId = (0, hash_js_1.sha256Hex)((0, jcs_js_1.jcsBytes)(r.receipt.cell));
224
224
  if (cellId !== request.cell_id)
225
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
226
+ // With a requested asof: at or before it, for ok and stale alike (D31, revised in spec-0.3.0). Instants are fixed
227
227
  // width, so string order is time order (SPEC 3.3).
228
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);
229
+ if (request.asof !== undefined && !(asof <= request.asof)) {
230
+ return fail('E_ENVELOPE', `the receipt answers asof ${asof}, later than the requested ${request.asof}`, '$.receipt.asof', r.hash);
232
231
  }
233
232
  // Step 6.
234
233
  const trust = env.trust;
@@ -1,7 +1,10 @@
1
1
  /** The spec revision this library conforms to. */
2
- export declare const SPEC_VERSION = "spec-0.2.1";
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"];
2
+ export declare const SPEC_VERSION = "spec-0.3.0";
3
+ /**
4
+ * Every error code defined by SPEC.md 10.2, in the order of that table. `E_EVAL_UNAVAILABLE` is
5
+ * the one code that is not a failure: `verify` reports it with success (10.1 step 7.6).
6
+ */
7
+ 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_EVAL_UNAVAILABLE", "E_BLOB_MISMATCH", "E_ENVELOPE"];
5
8
  export type ErrorCode = (typeof ERROR_CODES)[number];
6
9
  /** A Datumesh rule was broken. `code` is a SPEC.md 10.2 code; `path` locates the failure. */
7
10
  export declare class DatumeshError extends Error {
@@ -3,8 +3,11 @@ 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.2.1';
7
- /** Every error code defined by SPEC.md 10.2, in the order of that table. */
6
+ exports.SPEC_VERSION = 'spec-0.3.0';
7
+ /**
8
+ * Every error code defined by SPEC.md 10.2, in the order of that table. `E_EVAL_UNAVAILABLE` is
9
+ * the one code that is not a failure: `verify` reports it with success (10.1 step 7.6).
10
+ */
8
11
  exports.ERROR_CODES = [
9
12
  'E_PARSE_JSON',
10
13
  'E_PARSE_DUPLICATE_KEY',
@@ -45,6 +48,7 @@ exports.ERROR_CODES = [
45
48
  'E_INPUT_INVALID',
46
49
  'E_INPUT_UNDECLARED',
47
50
  'E_VALUE_MISMATCH',
51
+ 'E_EVAL_UNAVAILABLE',
48
52
  'E_BLOB_MISMATCH',
49
53
  'E_ENVELOPE',
50
54
  ];
@@ -7,7 +7,7 @@ export { keysFromResponse, keysFromDocument, fetchKeys, FETCH_TIMEOUT_MS, FETCH_
7
7
  export type { Kind } from './validate.js';
8
8
  export type { JsonValue } from './json.js';
9
9
  /** Library version. */
10
- export declare const VERSION = "0.2.1";
10
+ export declare const VERSION = "0.3.0";
11
11
  /**
12
12
  * Validate `input` as `kind` and return its RFC 8785 canonical bytes. `input` is JSON text,
13
13
  * UTF-8 bytes, or an already-parsed value. `kind` is "scalar", "cell", "ref", "function" or
package/dist/cjs/index.js CHANGED
@@ -9,7 +9,7 @@ exports.receiptPayload = receiptPayload;
9
9
  exports.resolvedUnit = resolvedUnit;
10
10
  exports.keyId = keyId;
11
11
  /**
12
- * @datumesh/verify: verify Datumesh receipts (SPEC.md, revision spec-0.2.1).
12
+ * @datumesh/verify: verify Datumesh receipts (SPEC.md, revision spec-0.3.0).
13
13
  *
14
14
  * Everything here is synchronous and offline except fetchKeys, the only function that uses the
15
15
  * network.
@@ -39,7 +39,7 @@ Object.defineProperty(exports, "fetchKeys", { enumerable: true, get: function ()
39
39
  Object.defineProperty(exports, "FETCH_TIMEOUT_MS", { enumerable: true, get: function () { return keys_js_1.FETCH_TIMEOUT_MS; } });
40
40
  Object.defineProperty(exports, "FETCH_MAX_BYTES", { enumerable: true, get: function () { return keys_js_1.FETCH_MAX_BYTES; } });
41
41
  /** Library version. */
42
- exports.VERSION = '0.2.1';
42
+ exports.VERSION = '0.3.0';
43
43
  function parseAndValidate(input, kind) {
44
44
  if (kind !== undefined && kind !== null && !validate_js_1.KINDS.includes(kind))
45
45
  throw new TypeError(`unknown kind ${JSON.stringify(kind)}; expected one of ${validate_js_1.KINDS.join(', ')}`);
@@ -23,10 +23,11 @@ export type KeyMap = Record<string, string>;
23
23
  export type KeySource = KeysResult | KeyMap | Record<string, unknown> | KeySource[];
24
24
  /**
25
25
  * Interpret the HTTPS response for https://<host>/.well-known/datumesh.json (SPEC 9.3).
26
- * `status` is the HTTP status, or null for a network, DNS, TLS or timeout failure. `body` is the
27
- * response body as text or bytes. Never throws.
26
+ * `status` is the HTTP status, or null for a network, DNS, TLS or timeout failure. Only the
27
+ * integer 200 is a retrieved document: any other value (a string "200" included, or undefined)
28
+ * is E_KEY_UNAVAILABLE. `body` is the response body as text or bytes. Never throws.
28
29
  */
29
- export declare function keysFromResponse(host: string, status: number | null, body?: string | Uint8Array | null): KeysResult;
30
+ export declare function keysFromResponse(host: string, status: unknown, body?: string | Uint8Array | null): KeysResult;
30
31
  /**
31
32
  * Validate an already-parsed key document (its `host` member names the host it covers).
32
33
  * Never throws.
package/dist/cjs/keys.js CHANGED
@@ -110,15 +110,21 @@ function docResult(host, parse) {
110
110
  }
111
111
  /**
112
112
  * Interpret the HTTPS response for https://<host>/.well-known/datumesh.json (SPEC 9.3).
113
- * `status` is the HTTP status, or null for a network, DNS, TLS or timeout failure. `body` is the
114
- * response body as text or bytes. Never throws.
113
+ * `status` is the HTTP status, or null for a network, DNS, TLS or timeout failure. Only the
114
+ * integer 200 is a retrieved document: any other value (a string "200" included, or undefined)
115
+ * is E_KEY_UNAVAILABLE. `body` is the response body as text or bytes. Never throws.
115
116
  */
116
117
  function keysFromResponse(host, status, body) {
117
118
  const h = typeof host === 'string' ? host : String(host);
118
119
  try {
119
- if (status === null || status === undefined) {
120
+ if (status === null) {
120
121
  return { ok: false, code: 'E_KEY_UNAVAILABLE', host: h, detail: 'network, DNS, TLS or timeout failure' };
121
122
  }
123
+ if (status === undefined)
124
+ return { ok: false, code: 'E_KEY_UNAVAILABLE', host: h, detail: 'no HTTP status' };
125
+ if (typeof status !== 'number') {
126
+ return { ok: false, code: 'E_KEY_UNAVAILABLE', host: h, detail: `HTTP status ${JSON.stringify(String(status))} is not an integer (only the integer 200 is a retrieved document)` };
127
+ }
122
128
  if (status !== 200) {
123
129
  return { ok: false, code: 'E_KEY_UNAVAILABLE', host: h, detail: `HTTP status ${String(status)} (only 200 is accepted; redirects are not followed)` };
124
130
  }
@@ -2,9 +2,17 @@ import { type ErrorCode } from './errors.js';
2
2
  import { type JsonObject, type JsonValue } from './json.js';
3
3
  import { type Sources } from './keys.js';
4
4
  export interface VerifyResult {
5
- /** True when the receipt is signature-valid (and, if recompute was given, fully verified). */
5
+ /**
6
+ * True when the receipt is signature-valid (and, if recompute was given with a value, fully
7
+ * verified).
8
+ */
6
9
  ok: boolean;
7
- /** "OK" or a SPEC 10.2 error code. */
10
+ /**
11
+ * "OK" or a SPEC 10.2 error code. With `ok` true it is "OK", or `E_EVAL_UNAVAILABLE` when
12
+ * recompute was given without a value: steps 7.1-7.5 passed but the function was not evaluated
13
+ * (SPEC 10.1 step 7.6), so `level` is "signed" and `path` is "$.value". That is the one code
14
+ * reported with success.
15
+ */
8
16
  code: 'OK' | ErrorCode;
9
17
  /** "signed" (steps 1-6) or "recomputed" (steps 1-7) when ok, else null. */
10
18
  level: 'signed' | 'recomputed' | null;
@@ -21,12 +29,21 @@ export interface VerifyResult {
21
29
  }
22
30
  /** Input for SPEC 10.1 step 7. The library does not evaluate functions; `value` is the result. */
23
31
  export interface Recompute {
24
- /** The function descriptor for cell.fn_hash (object or JSON text); absent: E_FN_UNAVAILABLE. */
32
+ /**
33
+ * The function descriptor for cell.fn_hash: a parsed JSON value, or JSON text / UTF-8 bytes,
34
+ * parsed strictly first. Absent (undefined): E_FN_UNAVAILABLE. Anything else obtained, `null`
35
+ * included, is a descriptor offered: if it is not the descriptor it fails the hash check
36
+ * (E_FN_HASH_MISMATCH; SPEC 10.1 step 7.1). A JS string is always JSON text.
37
+ */
25
38
  descriptor?: unknown;
26
39
  /** Input receipts by receipt hash, as JSON text (or bytes, or parsed objects). */
27
40
  inputs?: Record<string, unknown>;
28
- /** What evaluating the function produced: a typed scalar. */
29
- value: {
41
+ /**
42
+ * What evaluating the function produced: a typed scalar. Absent (undefined) when the function
43
+ * could not be evaluated: once 7.1-7.5 pass, the result is success at level "signed" with code
44
+ * E_EVAL_UNAVAILABLE (SPEC 10.1 step 7.6).
45
+ */
46
+ value?: {
30
47
  type: string;
31
48
  value: string | boolean;
32
49
  };
@@ -40,7 +57,9 @@ declare class Stop extends Error {
40
57
  /**
41
58
  * Verify a receipt (SPEC 10.1). `receipt` is JSON text, UTF-8 bytes or a parsed object (only
42
59
  * text and bytes allow duplicate-member detection). `keys` is a key document, a KeysResult, a
43
- * {keyId: publicKeyHex} map, or an array of these. With `recompute`, step 7 runs too. With
60
+ * {keyId: publicKeyHex} map, or an array of these. With `recompute`, step 7 runs too; without
61
+ * its `value`, steps 7.1-7.5 run and the result is success at level "signed" with code
62
+ * E_EVAL_UNAVAILABLE (path "$.value") unless they fail. With
44
63
  * `blob`, the content of a blob value, step 7.7 runs last (after step 7 when recomputing,
45
64
  * otherwise right after step 6): E_SCHEMA if the value is not a blob, E_BLOB_MISMATCH if the
46
65
  * content does not match. It does not change `level`; `blobChecked` reports it.
@@ -69,11 +69,12 @@ function verifySigned(input, sources, setHash) {
69
69
  }
70
70
  return { receipt, hash };
71
71
  }
72
+ /** Step 7. Throws Stop; returns false when 7.6 could not run (no value: E_EVAL_UNAVAILABLE). */
72
73
  function recompute(receipt, rc, sources) {
73
74
  const cell = receipt.cell;
74
75
  const cellArgs = cell.args;
75
- // 7.1 Descriptor.
76
- if (rc.descriptor === undefined || rc.descriptor === null)
76
+ // 7.1 Descriptor. Only obtaining nothing is unavailability; null is a descriptor offered (D35).
77
+ if (rc.descriptor === undefined)
77
78
  stop('E_FN_UNAVAILABLE', `no descriptor for ${cell.fn_hash}`, '$.cell.fn_hash');
78
79
  let desc;
79
80
  try {
@@ -136,18 +137,23 @@ function recompute(receipt, rc, sources) {
136
137
  if (!declaredInputs.includes(`${ic.ns}/${ic.fn}`)) {
137
138
  stop('E_INPUT_UNDECLARED', `input ${h} is a cell of an undeclared function`, '$.inputs');
138
139
  }
139
- if (inner.receipt.asof !== receipt.asof)
140
- stop('E_TIME_ORDER', "input receipts must share the receipt's asof", '$.inputs');
140
+ // At or before the receipt's asof (D17, revised in spec-0.3.0); instants are fixed width, so
141
+ // string order is time order (SPEC 3.3).
142
+ if (!(inner.receipt.asof <= receipt.asof))
143
+ stop('E_TIME_ORDER', `input ${h} has asof ${inner.receipt.asof}, after the receipt's asof`, '$.inputs');
141
144
  const itx = inner.receipt.tx;
142
145
  if (latestTx === null || itx > latestTx)
143
146
  latestTx = itx;
144
147
  }
145
148
  if (latestTx !== null && receipt.tx !== latestTx)
146
149
  stop('E_TIME_ORDER', 'tx must be the latest input tx', '$.tx');
147
- // 7.6 Value.
150
+ // 7.6 Value. No value: the function could not be evaluated (E_EVAL_UNAVAILABLE, not a failure).
148
151
  const v = rc.value;
152
+ if (v === undefined)
153
+ return false;
149
154
  if (v.type !== receipt.type || v.value !== receipt.value)
150
155
  stop('E_VALUE_MISMATCH', 'recomputed value differs from the receipt', '$.value');
156
+ return true;
151
157
  }
152
158
  /**
153
159
  * Step 7.7: content is only for a blob value (E_SCHEMA otherwise); it has exactly `length`
@@ -177,8 +183,8 @@ function checkRecomputeArg(rc) {
177
183
  if (typeof rc !== 'object' || Array.isArray(rc))
178
184
  throw new TypeError('recompute must be an object {descriptor, inputs, value}');
179
185
  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}');
186
+ if (r.value !== undefined && (!r.value || typeof r.value !== 'object' || typeof r.value.type !== 'string' || (typeof r.value.value !== 'string' && typeof r.value.value !== 'boolean'))) {
187
+ throw new TypeError('recompute.value must be the evaluated typed scalar {type, value}, or absent when the function could not be evaluated');
182
188
  }
183
189
  if (r.inputs !== undefined && (typeof r.inputs !== 'object' || r.inputs === null || Array.isArray(r.inputs))) {
184
190
  throw new TypeError('recompute.inputs must be an object mapping receipt hash to receipt JSON');
@@ -188,7 +194,9 @@ function checkRecomputeArg(rc) {
188
194
  /**
189
195
  * Verify a receipt (SPEC 10.1). `receipt` is JSON text, UTF-8 bytes or a parsed object (only
190
196
  * 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
197
+ * {keyId: publicKeyHex} map, or an array of these. With `recompute`, step 7 runs too; without
198
+ * its `value`, steps 7.1-7.5 run and the result is success at level "signed" with code
199
+ * E_EVAL_UNAVAILABLE (path "$.value") unless they fail. With
192
200
  * `blob`, the content of a blob value, step 7.7 runs last (after step 7 when recomputing,
193
201
  * otherwise right after step 6): E_SCHEMA if the value is not a blob, E_BLOB_MISMATCH if the
194
202
  * content does not match. It does not change `level`; `blobChecked` reports it.
@@ -203,20 +211,24 @@ function verify(receipt, keys, recomputeArg, blob) {
203
211
  let hash = null;
204
212
  try {
205
213
  const { receipt: r } = verifySigned(receipt, sources, (h) => (hash = h));
206
- if (rc)
207
- recompute(r, rc, sources);
214
+ const evaluated = rc ? recompute(r, rc, sources) : false;
208
215
  const checked = content !== null;
209
216
  if (checked)
210
- checkContent(r, content);
211
- const what = rc ? 'fully verified: signature valid and value recomputed' : `signature-valid: signed by ${r.computed_by}`;
217
+ checkContent(r, content); // 7.7 also after E_EVAL_UNAVAILABLE; its failure replaces the code
218
+ const unavailable = rc !== null && !evaluated;
219
+ const what = evaluated
220
+ ? 'fully verified: signature valid and value recomputed'
221
+ : unavailable
222
+ ? `signature-valid: signed by ${r.computed_by}; descriptor, arguments and inputs check out, but the function could not be evaluated, so the value was not recomputed`
223
+ : `signature-valid: signed by ${r.computed_by}`;
212
224
  return {
213
225
  ok: true,
214
- code: 'OK',
215
- level: rc ? 'recomputed' : 'signed',
226
+ code: unavailable ? 'E_EVAL_UNAVAILABLE' : 'OK',
227
+ level: evaluated ? 'recomputed' : 'signed',
216
228
  hash,
217
229
  key: r.computed_by,
218
230
  detail: checked ? `${what}; blob content matches` : what,
219
- path: null,
231
+ path: unavailable ? '$.value' : null,
220
232
  blobChecked: checked,
221
233
  };
222
234
  }
@@ -220,12 +220,11 @@ export function checkEnvelope(status, body, keys, request) {
220
220
  const cellId = sha256Hex(jcsBytes(r.receipt.cell));
221
221
  if (cellId !== request.cell_id)
222
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
223
+ // With a requested asof: at or before it, for ok and stale alike (D31, revised in spec-0.3.0). Instants are fixed
224
224
  // width, so string order is time order (SPEC 3.3).
225
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);
226
+ if (request.asof !== undefined && !(asof <= request.asof)) {
227
+ return fail('E_ENVELOPE', `the receipt answers asof ${asof}, later than the requested ${request.asof}`, '$.receipt.asof', r.hash);
229
228
  }
230
229
  // Step 6.
231
230
  const trust = env.trust;
@@ -1,7 +1,10 @@
1
1
  /** The spec revision this library conforms to. */
2
- export declare const SPEC_VERSION = "spec-0.2.1";
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"];
2
+ export declare const SPEC_VERSION = "spec-0.3.0";
3
+ /**
4
+ * Every error code defined by SPEC.md 10.2, in the order of that table. `E_EVAL_UNAVAILABLE` is
5
+ * the one code that is not a failure: `verify` reports it with success (10.1 step 7.6).
6
+ */
7
+ 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_EVAL_UNAVAILABLE", "E_BLOB_MISMATCH", "E_ENVELOPE"];
5
8
  export type ErrorCode = (typeof ERROR_CODES)[number];
6
9
  /** A Datumesh rule was broken. `code` is a SPEC.md 10.2 code; `path` locates the failure. */
7
10
  export declare class DatumeshError extends Error {
@@ -1,6 +1,9 @@
1
1
  /** The spec revision this library conforms to. */
2
- export const SPEC_VERSION = 'spec-0.2.1';
3
- /** Every error code defined by SPEC.md 10.2, in the order of that table. */
2
+ export const SPEC_VERSION = 'spec-0.3.0';
3
+ /**
4
+ * Every error code defined by SPEC.md 10.2, in the order of that table. `E_EVAL_UNAVAILABLE` is
5
+ * the one code that is not a failure: `verify` reports it with success (10.1 step 7.6).
6
+ */
4
7
  export const ERROR_CODES = [
5
8
  'E_PARSE_JSON',
6
9
  'E_PARSE_DUPLICATE_KEY',
@@ -41,6 +44,7 @@ export const ERROR_CODES = [
41
44
  'E_INPUT_INVALID',
42
45
  'E_INPUT_UNDECLARED',
43
46
  'E_VALUE_MISMATCH',
47
+ 'E_EVAL_UNAVAILABLE',
44
48
  'E_BLOB_MISMATCH',
45
49
  'E_ENVELOPE',
46
50
  ];
@@ -7,7 +7,7 @@ export { keysFromResponse, keysFromDocument, fetchKeys, FETCH_TIMEOUT_MS, FETCH_
7
7
  export type { Kind } from './validate.js';
8
8
  export type { JsonValue } from './json.js';
9
9
  /** Library version. */
10
- export declare const VERSION = "0.2.1";
10
+ export declare const VERSION = "0.3.0";
11
11
  /**
12
12
  * Validate `input` as `kind` and return its RFC 8785 canonical bytes. `input` is JSON text,
13
13
  * UTF-8 bytes, or an already-parsed value. `kind` is "scalar", "cell", "ref", "function" or
package/dist/esm/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @datumesh/verify: verify Datumesh receipts (SPEC.md, revision spec-0.2.1).
2
+ * @datumesh/verify: verify Datumesh receipts (SPEC.md, revision spec-0.3.0).
3
3
  *
4
4
  * Everything here is synchronous and offline except fetchKeys, the only function that uses the
5
5
  * network.
@@ -16,7 +16,7 @@ export { checkEnvelope } from './envelope.js';
16
16
  export { parseUri, formatUri, httpsUrl } from './uri.js';
17
17
  export { keysFromResponse, keysFromDocument, fetchKeys, FETCH_TIMEOUT_MS, FETCH_MAX_BYTES } from './keys.js';
18
18
  /** Library version. */
19
- export const VERSION = '0.2.1';
19
+ export const VERSION = '0.3.0';
20
20
  function parseAndValidate(input, kind) {
21
21
  if (kind !== undefined && kind !== null && !KINDS.includes(kind))
22
22
  throw new TypeError(`unknown kind ${JSON.stringify(kind)}; expected one of ${KINDS.join(', ')}`);
@@ -23,10 +23,11 @@ export type KeyMap = Record<string, string>;
23
23
  export type KeySource = KeysResult | KeyMap | Record<string, unknown> | KeySource[];
24
24
  /**
25
25
  * Interpret the HTTPS response for https://<host>/.well-known/datumesh.json (SPEC 9.3).
26
- * `status` is the HTTP status, or null for a network, DNS, TLS or timeout failure. `body` is the
27
- * response body as text or bytes. Never throws.
26
+ * `status` is the HTTP status, or null for a network, DNS, TLS or timeout failure. Only the
27
+ * integer 200 is a retrieved document: any other value (a string "200" included, or undefined)
28
+ * is E_KEY_UNAVAILABLE. `body` is the response body as text or bytes. Never throws.
28
29
  */
29
- export declare function keysFromResponse(host: string, status: number | null, body?: string | Uint8Array | null): KeysResult;
30
+ export declare function keysFromResponse(host: string, status: unknown, body?: string | Uint8Array | null): KeysResult;
30
31
  /**
31
32
  * Validate an already-parsed key document (its `host` member names the host it covers).
32
33
  * Never throws.
package/dist/esm/keys.js CHANGED
@@ -102,15 +102,21 @@ function docResult(host, parse) {
102
102
  }
103
103
  /**
104
104
  * Interpret the HTTPS response for https://<host>/.well-known/datumesh.json (SPEC 9.3).
105
- * `status` is the HTTP status, or null for a network, DNS, TLS or timeout failure. `body` is the
106
- * response body as text or bytes. Never throws.
105
+ * `status` is the HTTP status, or null for a network, DNS, TLS or timeout failure. Only the
106
+ * integer 200 is a retrieved document: any other value (a string "200" included, or undefined)
107
+ * is E_KEY_UNAVAILABLE. `body` is the response body as text or bytes. Never throws.
107
108
  */
108
109
  export function keysFromResponse(host, status, body) {
109
110
  const h = typeof host === 'string' ? host : String(host);
110
111
  try {
111
- if (status === null || status === undefined) {
112
+ if (status === null) {
112
113
  return { ok: false, code: 'E_KEY_UNAVAILABLE', host: h, detail: 'network, DNS, TLS or timeout failure' };
113
114
  }
115
+ if (status === undefined)
116
+ return { ok: false, code: 'E_KEY_UNAVAILABLE', host: h, detail: 'no HTTP status' };
117
+ if (typeof status !== 'number') {
118
+ return { ok: false, code: 'E_KEY_UNAVAILABLE', host: h, detail: `HTTP status ${JSON.stringify(String(status))} is not an integer (only the integer 200 is a retrieved document)` };
119
+ }
114
120
  if (status !== 200) {
115
121
  return { ok: false, code: 'E_KEY_UNAVAILABLE', host: h, detail: `HTTP status ${String(status)} (only 200 is accepted; redirects are not followed)` };
116
122
  }
@@ -2,9 +2,17 @@ import { type ErrorCode } from './errors.js';
2
2
  import { type JsonObject, type JsonValue } from './json.js';
3
3
  import { type Sources } from './keys.js';
4
4
  export interface VerifyResult {
5
- /** True when the receipt is signature-valid (and, if recompute was given, fully verified). */
5
+ /**
6
+ * True when the receipt is signature-valid (and, if recompute was given with a value, fully
7
+ * verified).
8
+ */
6
9
  ok: boolean;
7
- /** "OK" or a SPEC 10.2 error code. */
10
+ /**
11
+ * "OK" or a SPEC 10.2 error code. With `ok` true it is "OK", or `E_EVAL_UNAVAILABLE` when
12
+ * recompute was given without a value: steps 7.1-7.5 passed but the function was not evaluated
13
+ * (SPEC 10.1 step 7.6), so `level` is "signed" and `path` is "$.value". That is the one code
14
+ * reported with success.
15
+ */
8
16
  code: 'OK' | ErrorCode;
9
17
  /** "signed" (steps 1-6) or "recomputed" (steps 1-7) when ok, else null. */
10
18
  level: 'signed' | 'recomputed' | null;
@@ -21,12 +29,21 @@ export interface VerifyResult {
21
29
  }
22
30
  /** Input for SPEC 10.1 step 7. The library does not evaluate functions; `value` is the result. */
23
31
  export interface Recompute {
24
- /** The function descriptor for cell.fn_hash (object or JSON text); absent: E_FN_UNAVAILABLE. */
32
+ /**
33
+ * The function descriptor for cell.fn_hash: a parsed JSON value, or JSON text / UTF-8 bytes,
34
+ * parsed strictly first. Absent (undefined): E_FN_UNAVAILABLE. Anything else obtained, `null`
35
+ * included, is a descriptor offered: if it is not the descriptor it fails the hash check
36
+ * (E_FN_HASH_MISMATCH; SPEC 10.1 step 7.1). A JS string is always JSON text.
37
+ */
25
38
  descriptor?: unknown;
26
39
  /** Input receipts by receipt hash, as JSON text (or bytes, or parsed objects). */
27
40
  inputs?: Record<string, unknown>;
28
- /** What evaluating the function produced: a typed scalar. */
29
- value: {
41
+ /**
42
+ * What evaluating the function produced: a typed scalar. Absent (undefined) when the function
43
+ * could not be evaluated: once 7.1-7.5 pass, the result is success at level "signed" with code
44
+ * E_EVAL_UNAVAILABLE (SPEC 10.1 step 7.6).
45
+ */
46
+ value?: {
30
47
  type: string;
31
48
  value: string | boolean;
32
49
  };
@@ -40,7 +57,9 @@ declare class Stop extends Error {
40
57
  /**
41
58
  * Verify a receipt (SPEC 10.1). `receipt` is JSON text, UTF-8 bytes or a parsed object (only
42
59
  * text and bytes allow duplicate-member detection). `keys` is a key document, a KeysResult, a
43
- * {keyId: publicKeyHex} map, or an array of these. With `recompute`, step 7 runs too. With
60
+ * {keyId: publicKeyHex} map, or an array of these. With `recompute`, step 7 runs too; without
61
+ * its `value`, steps 7.1-7.5 run and the result is success at level "signed" with code
62
+ * E_EVAL_UNAVAILABLE (path "$.value") unless they fail. With
44
63
  * `blob`, the content of a blob value, step 7.7 runs last (after step 7 when recomputing,
45
64
  * otherwise right after step 6): E_SCHEMA if the value is not a blob, E_BLOB_MISMATCH if the
46
65
  * content does not match. It does not change `level`; `blobChecked` reports it.
@@ -63,11 +63,12 @@ function verifySigned(input, sources, setHash) {
63
63
  }
64
64
  return { receipt, hash };
65
65
  }
66
+ /** Step 7. Throws Stop; returns false when 7.6 could not run (no value: E_EVAL_UNAVAILABLE). */
66
67
  function recompute(receipt, rc, sources) {
67
68
  const cell = receipt.cell;
68
69
  const cellArgs = cell.args;
69
- // 7.1 Descriptor.
70
- if (rc.descriptor === undefined || rc.descriptor === null)
70
+ // 7.1 Descriptor. Only obtaining nothing is unavailability; null is a descriptor offered (D35).
71
+ if (rc.descriptor === undefined)
71
72
  stop('E_FN_UNAVAILABLE', `no descriptor for ${cell.fn_hash}`, '$.cell.fn_hash');
72
73
  let desc;
73
74
  try {
@@ -130,18 +131,23 @@ function recompute(receipt, rc, sources) {
130
131
  if (!declaredInputs.includes(`${ic.ns}/${ic.fn}`)) {
131
132
  stop('E_INPUT_UNDECLARED', `input ${h} is a cell of an undeclared function`, '$.inputs');
132
133
  }
133
- if (inner.receipt.asof !== receipt.asof)
134
- stop('E_TIME_ORDER', "input receipts must share the receipt's asof", '$.inputs');
134
+ // At or before the receipt's asof (D17, revised in spec-0.3.0); instants are fixed width, so
135
+ // string order is time order (SPEC 3.3).
136
+ if (!(inner.receipt.asof <= receipt.asof))
137
+ stop('E_TIME_ORDER', `input ${h} has asof ${inner.receipt.asof}, after the receipt's asof`, '$.inputs');
135
138
  const itx = inner.receipt.tx;
136
139
  if (latestTx === null || itx > latestTx)
137
140
  latestTx = itx;
138
141
  }
139
142
  if (latestTx !== null && receipt.tx !== latestTx)
140
143
  stop('E_TIME_ORDER', 'tx must be the latest input tx', '$.tx');
141
- // 7.6 Value.
144
+ // 7.6 Value. No value: the function could not be evaluated (E_EVAL_UNAVAILABLE, not a failure).
142
145
  const v = rc.value;
146
+ if (v === undefined)
147
+ return false;
143
148
  if (v.type !== receipt.type || v.value !== receipt.value)
144
149
  stop('E_VALUE_MISMATCH', 'recomputed value differs from the receipt', '$.value');
150
+ return true;
145
151
  }
146
152
  /**
147
153
  * Step 7.7: content is only for a blob value (E_SCHEMA otherwise); it has exactly `length`
@@ -171,8 +177,8 @@ function checkRecomputeArg(rc) {
171
177
  if (typeof rc !== 'object' || Array.isArray(rc))
172
178
  throw new TypeError('recompute must be an object {descriptor, inputs, value}');
173
179
  const r = rc;
174
- if (!r.value || typeof r.value !== 'object' || typeof r.value.type !== 'string' || (typeof r.value.value !== 'string' && typeof r.value.value !== 'boolean')) {
175
- throw new TypeError('recompute.value must be the evaluated typed scalar {type, value}');
180
+ if (r.value !== undefined && (!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}, or absent when the function could not be evaluated');
176
182
  }
177
183
  if (r.inputs !== undefined && (typeof r.inputs !== 'object' || r.inputs === null || Array.isArray(r.inputs))) {
178
184
  throw new TypeError('recompute.inputs must be an object mapping receipt hash to receipt JSON');
@@ -182,7 +188,9 @@ function checkRecomputeArg(rc) {
182
188
  /**
183
189
  * Verify a receipt (SPEC 10.1). `receipt` is JSON text, UTF-8 bytes or a parsed object (only
184
190
  * text and bytes allow duplicate-member detection). `keys` is a key document, a KeysResult, a
185
- * {keyId: publicKeyHex} map, or an array of these. With `recompute`, step 7 runs too. With
191
+ * {keyId: publicKeyHex} map, or an array of these. With `recompute`, step 7 runs too; without
192
+ * its `value`, steps 7.1-7.5 run and the result is success at level "signed" with code
193
+ * E_EVAL_UNAVAILABLE (path "$.value") unless they fail. With
186
194
  * `blob`, the content of a blob value, step 7.7 runs last (after step 7 when recomputing,
187
195
  * otherwise right after step 6): E_SCHEMA if the value is not a blob, E_BLOB_MISMATCH if the
188
196
  * content does not match. It does not change `level`; `blobChecked` reports it.
@@ -197,20 +205,24 @@ export function verify(receipt, keys, recomputeArg, blob) {
197
205
  let hash = null;
198
206
  try {
199
207
  const { receipt: r } = verifySigned(receipt, sources, (h) => (hash = h));
200
- if (rc)
201
- recompute(r, rc, sources);
208
+ const evaluated = rc ? recompute(r, rc, sources) : false;
202
209
  const checked = content !== null;
203
210
  if (checked)
204
- checkContent(r, content);
205
- const what = rc ? 'fully verified: signature valid and value recomputed' : `signature-valid: signed by ${r.computed_by}`;
211
+ checkContent(r, content); // 7.7 also after E_EVAL_UNAVAILABLE; its failure replaces the code
212
+ const unavailable = rc !== null && !evaluated;
213
+ const what = evaluated
214
+ ? 'fully verified: signature valid and value recomputed'
215
+ : unavailable
216
+ ? `signature-valid: signed by ${r.computed_by}; descriptor, arguments and inputs check out, but the function could not be evaluated, so the value was not recomputed`
217
+ : `signature-valid: signed by ${r.computed_by}`;
206
218
  return {
207
219
  ok: true,
208
- code: 'OK',
209
- level: rc ? 'recomputed' : 'signed',
220
+ code: unavailable ? 'E_EVAL_UNAVAILABLE' : 'OK',
221
+ level: evaluated ? 'recomputed' : 'signed',
210
222
  hash,
211
223
  key: r.computed_by,
212
224
  detail: checked ? `${what}; blob content matches` : what,
213
- path: null,
225
+ path: unavailable ? '$.value' : null,
214
226
  blobChecked: checked,
215
227
  };
216
228
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datumesh/verify",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Verifier for Datumesh receipts: strict canonical JSON (RFC 8785), cell URIs, units, blobs, response envelopes, key documents and Ed25519 signatures.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -10,7 +10,7 @@
10
10
  "homepage": "https://datumesh.com",
11
11
  "keywords": ["datumesh", "receipt", "verify", "ed25519", "jcs", "rfc8785", "canonical-json"],
12
12
  "datumesh": {
13
- "spec": "spec-0.2.1"
13
+ "spec": "spec-0.3.0"
14
14
  },
15
15
  "type": "module",
16
16
  "main": "./dist/cjs/index.js",