@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.
Files changed (48) hide show
  1. package/README.md +7 -5
  2. package/dist/cjs/ed25519.js +2 -0
  3. package/dist/cjs/envelope.d.ts +38 -0
  4. package/dist/cjs/envelope.js +243 -0
  5. package/dist/cjs/errors.d.ts +2 -2
  6. package/dist/cjs/errors.js +5 -1
  7. package/dist/cjs/index.d.ts +13 -3
  8. package/dist/cjs/index.js +22 -3
  9. package/dist/cjs/json.d.ts +6 -7
  10. package/dist/cjs/json.js +49 -18
  11. package/dist/cjs/keys.d.ts +2 -0
  12. package/dist/cjs/keys.js +55 -2
  13. package/dist/cjs/lexical.d.ts +11 -1
  14. package/dist/cjs/lexical.js +28 -4
  15. package/dist/cjs/units-data.d.ts +4 -0
  16. package/dist/cjs/units-data.js +11 -0
  17. package/dist/cjs/units.d.ts +24 -0
  18. package/dist/cjs/units.js +131 -0
  19. package/dist/cjs/uri.d.ts +14 -2
  20. package/dist/cjs/uri.js +27 -14
  21. package/dist/cjs/validate.d.ts +8 -0
  22. package/dist/cjs/validate.js +63 -4
  23. package/dist/cjs/verify.d.ts +21 -2
  24. package/dist/cjs/verify.js +47 -7
  25. package/dist/esm/ed25519.js +2 -0
  26. package/dist/esm/envelope.d.ts +38 -0
  27. package/dist/esm/envelope.js +240 -0
  28. package/dist/esm/errors.d.ts +2 -2
  29. package/dist/esm/errors.js +5 -1
  30. package/dist/esm/index.d.ts +13 -3
  31. package/dist/esm/index.js +20 -4
  32. package/dist/esm/json.d.ts +6 -7
  33. package/dist/esm/json.js +49 -18
  34. package/dist/esm/keys.d.ts +2 -0
  35. package/dist/esm/keys.js +54 -1
  36. package/dist/esm/lexical.d.ts +11 -1
  37. package/dist/esm/lexical.js +24 -2
  38. package/dist/esm/units-data.d.ts +4 -0
  39. package/dist/esm/units-data.js +8 -0
  40. package/dist/esm/units.d.ts +24 -0
  41. package/dist/esm/units.js +123 -0
  42. package/dist/esm/uri.d.ts +14 -2
  43. package/dist/esm/uri.js +31 -18
  44. package/dist/esm/validate.d.ts +8 -0
  45. package/dist/esm/validate.js +61 -4
  46. package/dist/esm/verify.d.ts +21 -2
  47. package/dist/esm/verify.js +46 -8
  48. package/package.json +3 -3
@@ -4,6 +4,7 @@
4
4
  import { fail } from './errors.js';
5
5
  import { has, isObject, memberPath, sortedKeys } from './json.js';
6
6
  import { LIMITS, VALID_TIME_TYPES, checkArgName, checkFunctionName, checkHex, checkInstant, checkKeyRef, checkLicence, checkName, checkNamespace, checkString, checkTypedValue, isTypeName, } from './lexical.js';
7
+ import { canonUnit, resolveUnit, structUnit } from './units.js';
7
8
  export const KINDS = ['scalar', 'cell', 'ref', 'function', 'receipt', 'jcs'];
8
9
  const SLICE = '*';
9
10
  // ---------------------------------------------------------------- step 2: structure
@@ -76,12 +77,24 @@ export function structSignature(v, path) {
76
77
  for (const k of sortedKeys(args)) {
77
78
  const p = memberPath(argsPath, k);
78
79
  const d = expectObject(args[k], p);
79
- members(d, ['type', 'valid'], [], p);
80
+ members(d, ['type', 'valid'], ['unit'], p);
80
81
  expectString(d, 'type', p);
82
+ if (has(d, 'unit'))
83
+ structUnit(d.unit, memberPath(p, 'unit'));
81
84
  if (typeof d.valid !== 'boolean')
82
85
  fail('E_SCHEMA', 'valid must be a boolean', memberPath(p, 'valid'));
83
86
  }
84
- expectString(sig, 'returns', path);
87
+ // SPEC 7: a type name, or {"type", "unit"} (the unit's absence is E_CANON_UNIT, in step 3).
88
+ const rp = memberPath(path, 'returns');
89
+ const r = sig.returns;
90
+ if (typeof r === 'string')
91
+ return;
92
+ if (!isObject(r))
93
+ fail('E_SCHEMA', 'returns must be a type name or an object', rp);
94
+ members(r, ['type'], ['unit'], rp);
95
+ expectString(r, 'type', rp);
96
+ if (has(r, 'unit'))
97
+ structUnit(r.unit, memberPath(rp, 'unit'));
85
98
  }
86
99
  const RECEIPT_MEMBERS = [
87
100
  'asof',
@@ -185,14 +198,43 @@ export function canonSignature(sig, path) {
185
198
  const p = memberPath(argsPath, k);
186
199
  checkArgName(k, p);
187
200
  const d = args[k];
201
+ // SPEC 7: type, then unit, then valid.
188
202
  if (!isTypeName(d.type))
189
203
  fail('E_CANON_TYPE', `unknown type ${JSON.stringify(d.type)}`, memberPath(p, 'type'));
204
+ if (d.type === 'number') {
205
+ if (!has(d, 'unit'))
206
+ fail('E_CANON_UNIT', 'a number argument declares its unit', p);
207
+ canonUnit(d.unit, args, memberPath(p, 'unit'));
208
+ }
209
+ else if (has(d, 'unit')) {
210
+ fail('E_CANON_UNIT', 'only number arguments have a unit', memberPath(p, 'unit'));
211
+ }
190
212
  if (d.valid === true && !VALID_TIME_TYPES.includes(d.type)) {
191
213
  fail('E_CANON_TYPE', 'a valid-time argument must be a date, instant or string', p);
192
214
  }
193
215
  }
194
- if (!isTypeName(sig.returns))
195
- fail('E_CANON_TYPE', `unknown type ${JSON.stringify(sig.returns)}`, memberPath(path, 'returns'));
216
+ const rp = memberPath(path, 'returns');
217
+ const r = sig.returns;
218
+ if (typeof r === 'string') {
219
+ if (!isTypeName(r))
220
+ fail('E_CANON_TYPE', `unknown type ${JSON.stringify(r)}`, rp);
221
+ if (r === 'number')
222
+ fail('E_CANON_UNIT', 'a number return is {"type": "number", "unit": ...}, not the bare name', rp);
223
+ return;
224
+ }
225
+ const o = r;
226
+ if (!isTypeName(o.type))
227
+ fail('E_CANON_TYPE', `unknown type ${JSON.stringify(o.type)}`, memberPath(rp, 'type'));
228
+ if (o.type !== 'number')
229
+ fail('E_CANON_UNIT', 'only a number return is written as an object', rp);
230
+ if (!has(o, 'unit'))
231
+ fail('E_CANON_UNIT', 'a number return declares its unit', rp);
232
+ canonUnit(o.unit, args, memberPath(rp, 'unit'));
233
+ }
234
+ /** The return type named by a valid signature's `returns` (SPEC 10.1 step 7.4). */
235
+ export function returnType(sig) {
236
+ const r = sig.returns;
237
+ return typeof r === 'string' ? r : r.type;
196
238
  }
197
239
  function canonFunction(o, path) {
198
240
  const code = o.code;
@@ -308,6 +350,21 @@ export function validate(v, kind, path = '$') {
308
350
  return;
309
351
  }
310
352
  }
353
+ /**
354
+ * The unit of a cell's value (SPEC 7.1, units.md 3). Order: the cell (structure, canonical
355
+ * form), fn_hash against the descriptor (E_FN_HASH_MISMATCH), the descriptor (structure,
356
+ * canonical form), then substitution.
357
+ */
358
+ export function cellUnit(desc, cell, fnHash) {
359
+ structCell(cell, 'cell', false);
360
+ canonCell(cell, 'cell');
361
+ if (fnHash(desc) !== cell.fn_hash)
362
+ fail('E_FN_HASH_MISMATCH', 'the descriptor does not hash to cell.fn_hash', 'descriptor');
363
+ structFunction(desc, 'descriptor');
364
+ canonFunction(desc, 'descriptor');
365
+ const sig = desc.signature;
366
+ return resolveUnit(sig.returns, cell.args, 'descriptor.signature.returns', 'cell.args');
367
+ }
311
368
  /** Receipt step 2 only: true when the receipt has the right members and JSON types. */
312
369
  export function receiptStructure(v) {
313
370
  structReceipt(v, '$');
@@ -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 };
@@ -5,7 +5,8 @@ import { fromHex, sha256Hex } from './hash.js';
5
5
  import { has, newObject, sortedKeys, toJson } from './json.js';
6
6
  import { jcsBytes } from './jcs.js';
7
7
  import { collectSources, lookupKey } from './keys.js';
8
- import { receiptCanonicalAndConsistent, receiptStructure, validate } from './validate.js';
8
+ import { checkBlob } from './lexical.js';
9
+ import { receiptCanonicalAndConsistent, receiptStructure, returnType, validate } from './validate.js';
9
10
  class Stop extends Error {
10
11
  constructor(code, detail, path) {
11
12
  super(detail);
@@ -103,9 +104,10 @@ function recompute(receipt, rc, sources) {
103
104
  if (validNames.join('\u0000') !== declaredValid.join('\u0000')) {
104
105
  stop('E_VALID_MISMATCH', 'valid must echo exactly the declared valid-time arguments', '$.valid');
105
106
  }
106
- // 7.4 Return type.
107
- if (receipt.type !== sig.returns)
108
- stop('E_RETURN_TYPE', `function returns ${String(sig.returns)}`, '$.type');
107
+ // 7.4 Return type: `returns`, or its `type` member when it is an object.
108
+ const rt = returnType(sig);
109
+ if (receipt.type !== rt)
110
+ stop('E_RETURN_TYPE', `function returns ${rt}`, '$.type');
109
111
  // 7.5 Inputs.
110
112
  const declaredInputs = desc.inputs;
111
113
  const supplied = rc.inputs ?? {};
@@ -141,6 +143,28 @@ function recompute(receipt, rc, sources) {
141
143
  if (v.type !== receipt.type || v.value !== receipt.value)
142
144
  stop('E_VALUE_MISMATCH', 'recomputed value differs from the receipt', '$.value');
143
145
  }
146
+ /**
147
+ * Step 7.7: content is only for a blob value (E_SCHEMA otherwise); it has exactly `length`
148
+ * bytes, then a SHA-256 equal to the blob's hash (E_BLOB_MISMATCH).
149
+ */
150
+ function checkContent(receipt, content) {
151
+ if (receipt.type !== 'blob')
152
+ stop('E_SCHEMA', `content was supplied, but the value is a ${String(receipt.type)}, not a blob`, '$.type');
153
+ const blob = checkBlob(receipt.value, '$.value'); // already checked in step 3
154
+ if (content.length !== blob.length)
155
+ stop('E_BLOB_MISMATCH', `content is ${content.length} bytes, the blob says ${blob.length}`, '$.value');
156
+ if (sha256Hex(content) !== blob.sha256)
157
+ stop('E_BLOB_MISMATCH', "the content's SHA-256 is not the blob's hash", '$.value');
158
+ }
159
+ function checkBlobArg(b) {
160
+ if (b === undefined || b === null)
161
+ return null;
162
+ if (b instanceof Uint8Array)
163
+ return b;
164
+ if (b instanceof ArrayBuffer)
165
+ return new Uint8Array(b);
166
+ throw new TypeError('blob must be the content bytes (Uint8Array or ArrayBuffer)');
167
+ }
144
168
  function checkRecomputeArg(rc) {
145
169
  if (rc === undefined || rc === null)
146
170
  return null;
@@ -158,32 +182,46 @@ function checkRecomputeArg(rc) {
158
182
  /**
159
183
  * Verify a receipt (SPEC 10.1). `receipt` is JSON text, UTF-8 bytes or a parsed object (only
160
184
  * text and bytes allow duplicate-member detection). `keys` is a key document, a KeysResult, a
161
- * {keyId: publicKeyHex} map, or an array of these. With `recompute`, step 7 runs too.
185
+ * {keyId: publicKeyHex} map, or an array of these. With `recompute`, step 7 runs too. With
186
+ * `blob`, the content of a blob value, step 7.7 runs last (after step 7 when recomputing,
187
+ * otherwise right after step 6): E_SCHEMA if the value is not a blob, E_BLOB_MISMATCH if the
188
+ * content does not match. It does not change `level`; `blobChecked` reports it.
162
189
  *
163
190
  * Never throws for an invalid receipt or bad key material; throws TypeError only when an
164
191
  * argument is of the wrong kind altogether.
165
192
  */
166
- export function verify(receipt, keys, recomputeArg) {
193
+ export function verify(receipt, keys, recomputeArg, blob) {
167
194
  const sources = collectSources(keys);
168
195
  const rc = checkRecomputeArg(recomputeArg);
196
+ const content = checkBlobArg(blob);
169
197
  let hash = null;
170
198
  try {
171
199
  const { receipt: r } = verifySigned(receipt, sources, (h) => (hash = h));
172
200
  if (rc)
173
201
  recompute(r, rc, sources);
202
+ const checked = content !== null;
203
+ if (checked)
204
+ checkContent(r, content);
205
+ const what = rc ? 'fully verified: signature valid and value recomputed' : `signature-valid: signed by ${r.computed_by}`;
174
206
  return {
175
207
  ok: true,
176
208
  code: 'OK',
177
209
  level: rc ? 'recomputed' : 'signed',
178
210
  hash,
179
211
  key: r.computed_by,
180
- detail: rc ? 'fully verified: signature valid and value recomputed' : `signature-valid: signed by ${r.computed_by}`,
212
+ detail: checked ? `${what}; blob content matches` : what,
181
213
  path: null,
214
+ blobChecked: checked,
182
215
  };
183
216
  }
184
217
  catch (e) {
185
218
  if (e instanceof Stop)
186
- return { ok: false, code: e.code, level: null, hash, key: null, detail: e.detail, path: e.path };
219
+ return { ok: false, code: e.code, level: null, hash, key: null, detail: e.detail, path: e.path, blobChecked: false };
187
220
  throw e; // a bug in this library, not a property of the receipt
188
221
  }
189
222
  }
223
+ /** Steps 1-6 for an already-parsed receipt (used by checkEnvelope). Throws a Stop. */
224
+ export function verifyEmbedded(receipt, sources) {
225
+ return verifySigned(receipt, sources, () => undefined);
226
+ }
227
+ export { Stop };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@datumesh/verify",
3
- "version": "0.1.1",
4
- "description": "Verifier for Datumesh receipts: strict canonical JSON (RFC 8785), cell URIs, key documents and Ed25519 signatures.",
3
+ "version": "0.2.1",
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": {
7
7
  "type": "git",
@@ -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.1.1"
13
+ "spec": "spec-0.2.1"
14
14
  },
15
15
  "type": "module",
16
16
  "main": "./dist/cjs/index.js",