@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
@@ -34,6 +34,8 @@ export declare function keysFromResponse(host: string, status: number | null, bo
34
34
  export declare function keysFromDocument(doc: unknown): KeysResult;
35
35
  /** Default timeout of fetchKeys, in milliseconds. */
36
36
  export declare const FETCH_TIMEOUT_MS = 10000;
37
+ /** fetchKeys abandons a key document larger than this (SPEC 9.3: SHOULD bound retrieval). */
38
+ export declare const FETCH_MAX_BYTES: number;
37
39
  /**
38
40
  * Fetch https://<host>/.well-known/datumesh.json once, with TLS verified by the platform, no
39
41
  * redirects followed and a timeout, then interpret it with keysFromResponse. The only function
package/dist/cjs/keys.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.FETCH_TIMEOUT_MS = void 0;
3
+ exports.FETCH_MAX_BYTES = exports.FETCH_TIMEOUT_MS = void 0;
4
4
  exports.keysFromResponse = keysFromResponse;
5
5
  exports.keysFromDocument = keysFromDocument;
6
6
  exports.fetchKeys = fetchKeys;
@@ -11,6 +11,15 @@ const errors_js_1 = require("./errors.js");
11
11
  const hash_js_1 = require("./hash.js");
12
12
  const json_js_1 = require("./json.js");
13
13
  const lexical_js_1 = require("./lexical.js");
14
+ function isDnsName(s) {
15
+ try {
16
+ (0, lexical_js_1.checkDnsName)(s, '$');
17
+ return true;
18
+ }
19
+ catch {
20
+ return false;
21
+ }
22
+ }
14
23
  class KeyDocError extends Error {
15
24
  constructor(msg, path) {
16
25
  super(msg);
@@ -32,6 +41,9 @@ function validateKeyDocument(doc, host) {
32
41
  bad(`missing member ${JSON.stringify(k)}`, '$');
33
42
  if (doc.proto !== 'datumesh/1')
34
43
  bad("proto must be 'datumesh/1'", '$.proto');
44
+ // 9.2 / 9.3: `host` is a DNS name (2.1); a document for any other host cannot satisfy 9.2.
45
+ if (typeof doc.host !== 'string' || !isDnsName(doc.host))
46
+ bad('host must be a DNS name (SPEC 2.1); keys are resolved only for DNS names', '$.host');
35
47
  if (doc.host !== host)
36
48
  bad(`host must be ${JSON.stringify(host)}`, '$.host');
37
49
  const keys = doc.keys;
@@ -134,6 +146,45 @@ function keysFromDocument(doc) {
134
146
  }
135
147
  /** Default timeout of fetchKeys, in milliseconds. */
136
148
  exports.FETCH_TIMEOUT_MS = 10000;
149
+ /** fetchKeys abandons a key document larger than this (SPEC 9.3: SHOULD bound retrieval). */
150
+ exports.FETCH_MAX_BYTES = 256 * 1024;
151
+ class TooLarge extends Error {
152
+ }
153
+ /** Read a response body, abandoning it once it exceeds FETCH_MAX_BYTES. */
154
+ async function readBounded(res) {
155
+ const reader = res.body && typeof res.body.getReader === 'function' ? res.body.getReader() : null;
156
+ if (!reader) {
157
+ const b = new Uint8Array(await res.arrayBuffer());
158
+ if (b.length > exports.FETCH_MAX_BYTES)
159
+ throw new TooLarge();
160
+ return b;
161
+ }
162
+ const parts = [];
163
+ let n = 0;
164
+ for (;;) {
165
+ const { done, value } = await reader.read();
166
+ if (done)
167
+ break;
168
+ n += value.length;
169
+ if (n > exports.FETCH_MAX_BYTES) {
170
+ try {
171
+ await reader.cancel();
172
+ }
173
+ catch {
174
+ // abandoned anyway
175
+ }
176
+ throw new TooLarge();
177
+ }
178
+ parts.push(value);
179
+ }
180
+ const out = new Uint8Array(n);
181
+ let o = 0;
182
+ for (const p of parts) {
183
+ out.set(p, o);
184
+ o += p.length;
185
+ }
186
+ return out;
187
+ }
137
188
  /**
138
189
  * Fetch https://<host>/.well-known/datumesh.json once, with TLS verified by the platform, no
139
190
  * redirects followed and a timeout, then interpret it with keysFromResponse. The only function
@@ -165,9 +216,11 @@ async function fetchKeys(host, timeoutMs = exports.FETCH_TIMEOUT_MS) {
165
216
  status = 0;
166
217
  if (status !== 200)
167
218
  return keysFromResponse(host, status);
168
- body = new Uint8Array(await res.arrayBuffer());
219
+ body = await readBounded(res);
169
220
  }
170
221
  catch (e) {
222
+ if (e instanceof TooLarge)
223
+ return { ok: false, code: 'E_KEY_UNAVAILABLE', host, detail: `key document larger than 256 KiB; retrieval abandoned` };
171
224
  return { ok: false, code: 'E_KEY_UNAVAILABLE', host, detail: `retrieval failed: ${e instanceof Error ? e.message : String(e)}` };
172
225
  }
173
226
  return keysFromResponse(host, status, body);
@@ -11,7 +11,7 @@ export declare const LIMITS: {
11
11
  readonly licence_url: 512;
12
12
  readonly keys: 64;
13
13
  };
14
- export declare const TYPES: readonly ["boolean", "date", "instant", "number", "string"];
14
+ export declare const TYPES: readonly ["blob", "boolean", "date", "instant", "number", "string"];
15
15
  export type TypeName = (typeof TYPES)[number];
16
16
  export declare const VALID_TIME_TYPES: readonly string[];
17
17
  export declare function isTypeName(t: unknown): t is TypeName;
@@ -32,6 +32,8 @@ export declare function checkKeyRef(s: string, path: string): {
32
32
  host: string;
33
33
  id: string;
34
34
  };
35
+ /** SPEC 3.1: a canonical decimal of at most 128 characters. */
36
+ export declare function isNumber(s: string): boolean;
35
37
  /** SPEC 3.1. Grammar (E_CANON_NUMBER) before the 128-character limit (E_CANON_LIMIT). */
36
38
  export declare function checkNumber(s: string, path: string): void;
37
39
  export declare function isDate(s: string): boolean;
@@ -48,6 +50,14 @@ export declare function checkString(s: string, path: string, opts?: {
48
50
  body?: boolean;
49
51
  limit?: number;
50
52
  }): void;
53
+ export declare const MEDIA_TYPE_MAX = 128;
54
+ /** SPEC 3.7. Grammar, then length <= 2^53 - 1, then media type <= 128 characters; all E_CANON_BLOB. */
55
+ export declare function checkBlob(s: string, path: string): {
56
+ sha256: string;
57
+ length: number;
58
+ mediaType: string;
59
+ };
60
+ export declare const URL_RE: RegExp;
51
61
  /**
52
62
  * SPEC 8.1 `licence`: an SPDX licence identifier or an https:// URL. SPEC.md gives no grammar;
53
63
  * this follows schemas/receipt.schema.json (and the reference tool): an identifier matches
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.VALID_TIME_TYPES = exports.TYPES = exports.LIMITS = void 0;
3
+ exports.URL_RE = exports.MEDIA_TYPE_MAX = exports.VALID_TIME_TYPES = exports.TYPES = exports.LIMITS = void 0;
4
4
  exports.isTypeName = isTypeName;
5
5
  exports.checkName = checkName;
6
6
  exports.checkArgName = checkArgName;
@@ -10,12 +10,14 @@ exports.checkFunctionName = checkFunctionName;
10
10
  exports.isHex = isHex;
11
11
  exports.checkHex = checkHex;
12
12
  exports.checkKeyRef = checkKeyRef;
13
+ exports.isNumber = isNumber;
13
14
  exports.checkNumber = checkNumber;
14
15
  exports.isDate = isDate;
15
16
  exports.isInstant = isInstant;
16
17
  exports.checkDate = checkDate;
17
18
  exports.checkInstant = checkInstant;
18
19
  exports.checkString = checkString;
20
+ exports.checkBlob = checkBlob;
19
21
  exports.checkLicence = checkLicence;
20
22
  exports.checkTypedValue = checkTypedValue;
21
23
  // Lexical rules of SPEC.md sections 2-3 and 8.1. Each check throws a DatumeshError with the
@@ -37,7 +39,7 @@ exports.LIMITS = {
37
39
  licence_url: 512,
38
40
  keys: 64,
39
41
  };
40
- exports.TYPES = ['boolean', 'date', 'instant', 'number', 'string'];
42
+ exports.TYPES = ['blob', 'boolean', 'date', 'instant', 'number', 'string'];
41
43
  exports.VALID_TIME_TYPES = ['date', 'instant', 'string'];
42
44
  function isTypeName(t) {
43
45
  return typeof t === 'string' && exports.TYPES.includes(t);
@@ -107,6 +109,10 @@ function checkKeyRef(s, path) {
107
109
  return { host, id };
108
110
  }
109
111
  const NUMBER_RE = /^-?(?:0|[1-9][0-9]*)(?:\.[0-9]*[1-9])?$/;
112
+ /** SPEC 3.1: a canonical decimal of at most 128 characters. */
113
+ function isNumber(s) {
114
+ return NUMBER_RE.test(s) && s !== '-0' && s.length <= exports.LIMITS.number;
115
+ }
110
116
  /** SPEC 3.1. Grammar (E_CANON_NUMBER) before the 128-character limit (E_CANON_LIMIT). */
111
117
  function checkNumber(s, path) {
112
118
  if (!NUMBER_RE.test(s) || s === '-0')
@@ -166,8 +172,24 @@ function checkString(s, path, opts = {}) {
166
172
  function hex4(cp) {
167
173
  return cp.toString(16).toUpperCase().padStart(4, '0');
168
174
  }
175
+ // SPEC 3.7: <sha256>:<length>:<type>/<subtype>, with RFC 6838 restricted names in lowercase.
176
+ const BLOB_RE = /^[0-9a-f]{64}:(0|[1-9][0-9]*):([a-z0-9][a-z0-9!#$&^_.+-]*\/[a-z0-9][a-z0-9!#$&^_.+-]*)$/;
177
+ const MAX_SAFE = '9007199254740991';
178
+ exports.MEDIA_TYPE_MAX = 128;
179
+ /** SPEC 3.7. Grammar, then length <= 2^53 - 1, then media type <= 128 characters; all E_CANON_BLOB. */
180
+ function checkBlob(s, path) {
181
+ const m = BLOB_RE.exec(s);
182
+ if (!m)
183
+ (0, errors_js_1.fail)('E_CANON_BLOB', `not <sha256>:<length>:<type/subtype>: ${JSON.stringify(s)}`, path);
184
+ const len = m[1];
185
+ if (len.length > MAX_SAFE.length || (len.length === MAX_SAFE.length && len > MAX_SAFE))
186
+ (0, errors_js_1.fail)('E_CANON_BLOB', `length above ${MAX_SAFE}`, path);
187
+ if (m[2].length > exports.MEDIA_TYPE_MAX)
188
+ (0, errors_js_1.fail)('E_CANON_BLOB', `media type longer than ${exports.MEDIA_TYPE_MAX} characters`, path);
189
+ return { sha256: s.slice(0, 64), length: Number(len), mediaType: m[2] };
190
+ }
169
191
  const SPDX_RE = /^[A-Za-z0-9][A-Za-z0-9.+-]*$/;
170
- const URL_RE = /^https:\/\/[!-~]+$/;
192
+ exports.URL_RE = /^https:\/\/[!-~]+$/;
171
193
  /**
172
194
  * SPEC 8.1 `licence`: an SPDX licence identifier or an https:// URL. SPEC.md gives no grammar;
173
195
  * this follows schemas/receipt.schema.json (and the reference tool): an identifier matches
@@ -176,7 +198,7 @@ const URL_RE = /^https:\/\/[!-~]+$/;
176
198
  */
177
199
  function checkLicence(s, path) {
178
200
  if (s.startsWith('https://')) {
179
- if (!URL_RE.test(s))
201
+ if (!exports.URL_RE.test(s))
180
202
  (0, errors_js_1.fail)('E_CANON_LICENCE', 'licence URL has characters outside printable ASCII', path);
181
203
  if (s.length > exports.LIMITS.licence_url)
182
204
  (0, errors_js_1.fail)('E_CANON_LIMIT', 'licence URL too long', path);
@@ -202,6 +224,8 @@ function checkTypedValue(type, value, path) {
202
224
  checkDate(value, path);
203
225
  else if (type === 'instant')
204
226
  checkInstant(value, path);
227
+ else if (type === 'blob')
228
+ checkBlob(value, path);
205
229
  else
206
230
  checkString(value, path);
207
231
  }
@@ -0,0 +1,4 @@
1
+ /** Literal units by group, in units.md order. Index units (units.md 2.4) are a pattern. */
2
+ export declare const UNIT_GROUPS: {
3
+ readonly [group: string]: readonly string[];
4
+ };
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ // GENERATED by tools/gen-units.mjs from spec/units.md (the `units-…` fenced blocks).
3
+ // Do not edit by hand; test/units.test.mjs checks that this file matches units.md.
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.UNIT_GROUPS = void 0;
6
+ /** Literal units by group, in units.md order. Index units (units.md 2.4) are a pattern. */
7
+ exports.UNIT_GROUPS = {
8
+ currency: ["AED", "AFN", "ALL", "AMD", "AOA", "ARS", "AUD", "AWG", "AZN", "BAM", "BBD", "BDT", "BHD", "BIF", "BMD", "BND", "BOB", "BOV", "BRL", "BSD", "BTN", "BWP", "BYN", "BZD", "CAD", "CDF", "CHE", "CHF", "CHW", "CLF", "CLP", "CNY", "COP", "COU", "CRC", "CUP", "CVE", "CZK", "DJF", "DKK", "DOP", "DZD", "EGP", "ERN", "ETB", "EUR", "FJD", "FKP", "GBP", "GEL", "GHS", "GIP", "GMD", "GNF", "GTQ", "GYD", "HKD", "HNL", "HTG", "HUF", "IDR", "ILS", "INR", "IQD", "IRR", "ISK", "JMD", "JOD", "JPY", "KES", "KGS", "KHR", "KMF", "KPW", "KRW", "KWD", "KYD", "KZT", "LAK", "LBP", "LKR", "LRD", "LSL", "LYD", "MAD", "MDL", "MGA", "MKD", "MMK", "MNT", "MOP", "MRU", "MUR", "MVR", "MWK", "MXN", "MXV", "MYR", "MZN", "NAD", "NGN", "NIO", "NOK", "NPR", "NZD", "OMR", "PAB", "PEN", "PGK", "PHP", "PKR", "PLN", "PYG", "QAR", "RON", "RSD", "RUB", "RWF", "SAR", "SBD", "SCR", "SDG", "SEK", "SGD", "SHP", "SLE", "SOS", "SRD", "SSP", "STN", "SVC", "SYP", "SZL", "THB", "TJS", "TMT", "TND", "TOP", "TRY", "TTD", "TWD", "TZS", "UAH", "UGX", "USD", "USN", "UYI", "UYU", "UYW", "UZS", "VED", "VES", "VND", "VUV", "WST", "XAD", "XAF", "XAG", "XAU", "XBA", "XBB", "XBC", "XBD", "XCD", "XCG", "XDR", "XOF", "XPD", "XPF", "XPT", "XSU", "XTS", "XUA", "YER", "ZAR", "ZMW", "ZWG", "BGN", "CYP", "EEK", "GRD", "HRK", "LTL", "LVL", "MTL", "ROL", "SIT", "SKK", "TRL"],
9
+ physical: ["m", "kg", "s", "A", "K", "mol", "cd", "degC", "m/s", "W", "kWh", "mm", "cm", "km/h", "hPa", "W/m2"],
10
+ ratio: ["percent", "permille", "bp", "1", "count"],
11
+ };
@@ -0,0 +1,24 @@
1
+ import { type JsonObject, type JsonValue } from './json.js';
2
+ /** A currency code from units.md 2.1 (List One and the withdrawn codes). */
3
+ export declare function isCurrency(s: unknown): boolean;
4
+ /** units.md 2.4: index:<YYYY[-MM[-DD]]>=<number greater than 0>, at most 64 characters. */
5
+ export declare function isIndexUnit(s: string): boolean;
6
+ /** A literal unit (units.md section 2): a listed string or an index unit. Byte-for-byte. */
7
+ export declare function isUnitLiteral(s: unknown): boolean;
8
+ /**
9
+ * Step 2 for a unit: a string, {"arg": <string>}, or {"of": side, "per": side} where a side is
10
+ * a string or {"arg": <string>}. Anything else is E_SCHEMA (SPEC 7.1, units.md 1).
11
+ */
12
+ export declare function structUnit(u: JsonValue, path: string): void;
13
+ /**
14
+ * Step 3 for a unit whose shape passed structUnit. `args` is the signature's `args`: a
15
+ * reference must name one of them whose declared type is `string` (E_CANON_UNIT).
16
+ */
17
+ export declare function canonUnit(u: JsonValue, args: JsonObject, path: string): void;
18
+ /**
19
+ * units.md section 3: the unit of one cell's value, for a descriptor and a cell that have
20
+ * passed validation and whose fn_hash matches. `returns` is the signature's `returns`.
21
+ * A referenced argument must be bound (E_ARG_MISSING), not a slice (E_SLICE), of its declared
22
+ * type (E_ARG_TYPE), and a literal unit (E_CANON_UNIT); on a rate side, a currency.
23
+ */
24
+ export declare function resolveUnit(returns: JsonValue, cellArgs: JsonObject, path: string, argsPath: string): string | null;
@@ -0,0 +1,131 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isCurrency = isCurrency;
4
+ exports.isIndexUnit = isIndexUnit;
5
+ exports.isUnitLiteral = isUnitLiteral;
6
+ exports.structUnit = structUnit;
7
+ exports.canonUnit = canonUnit;
8
+ exports.resolveUnit = resolveUnit;
9
+ // Units (SPEC.md 7.1, units.md): shapes in a signature (step 2, E_SCHEMA), validity against the
10
+ // closed list (step 3, E_CANON_UNIT), and resolution of a cell's unit (units.md section 3).
11
+ const errors_js_1 = require("./errors.js");
12
+ const json_js_1 = require("./json.js");
13
+ const lexical_js_1 = require("./lexical.js");
14
+ const units_data_js_1 = require("./units-data.js");
15
+ const CURRENCY = new Set(units_data_js_1.UNIT_GROUPS.currency);
16
+ const LITERAL = new Set(Object.values(units_data_js_1.UNIT_GROUPS).flat());
17
+ const INDEX_RE = /^index:([0-9]{4}(?:-[0-9]{2}(?:-[0-9]{2})?)?)=(.*)$/;
18
+ const INDEX_MAX = 64;
19
+ /** A currency code from units.md 2.1 (List One and the withdrawn codes). */
20
+ function isCurrency(s) {
21
+ return typeof s === 'string' && CURRENCY.has(s);
22
+ }
23
+ /** units.md 2.4: index:<YYYY[-MM[-DD]]>=<number greater than 0>, at most 64 characters. */
24
+ function isIndexUnit(s) {
25
+ if (s.length > INDEX_MAX)
26
+ return false;
27
+ const m = INDEX_RE.exec(s);
28
+ if (!m)
29
+ return false;
30
+ const [y, mo = '01', d = '01'] = m[1].split('-');
31
+ if (!(0, lexical_js_1.isDate)(`${y}-${mo}-${d}`))
32
+ return false;
33
+ return (0, lexical_js_1.isNumber)(m[2]) && !m[2].startsWith('-') && m[2] !== '0';
34
+ }
35
+ /** A literal unit (units.md section 2): a listed string or an index unit. Byte-for-byte. */
36
+ function isUnitLiteral(s) {
37
+ return typeof s === 'string' && (LITERAL.has(s) || isIndexUnit(s));
38
+ }
39
+ const isRef = (u) => Object.keys(u).length === 1 && (0, json_js_1.has)(u, 'arg') && typeof u.arg === 'string';
40
+ /**
41
+ * Step 2 for a unit: a string, {"arg": <string>}, or {"of": side, "per": side} where a side is
42
+ * a string or {"arg": <string>}. Anything else is E_SCHEMA (SPEC 7.1, units.md 1).
43
+ */
44
+ function structUnit(u, path) {
45
+ if (typeof u === 'string')
46
+ return;
47
+ if ((0, json_js_1.isObject)(u)) {
48
+ if (isRef(u))
49
+ return;
50
+ const keys = Object.keys(u);
51
+ if (keys.length === 2 && (0, json_js_1.has)(u, 'of') && (0, json_js_1.has)(u, 'per')) {
52
+ for (const side of ['of', 'per']) {
53
+ const v = u[side];
54
+ if (typeof v === 'string' || ((0, json_js_1.isObject)(v) && isRef(v)))
55
+ continue;
56
+ (0, errors_js_1.fail)('E_SCHEMA', 'a rate side is a currency code or {"arg": name}', (0, json_js_1.memberPath)(path, side));
57
+ }
58
+ return;
59
+ }
60
+ }
61
+ (0, errors_js_1.fail)('E_SCHEMA', 'a unit is a string, {"arg": name} or {"of", "per"}', path);
62
+ }
63
+ /**
64
+ * Step 3 for a unit whose shape passed structUnit. `args` is the signature's `args`: a
65
+ * reference must name one of them whose declared type is `string` (E_CANON_UNIT).
66
+ */
67
+ function canonUnit(u, args, path) {
68
+ const checkRef = (r, p) => {
69
+ const name = r.arg;
70
+ const d = (0, json_js_1.has)(args, name) ? args[name] : null;
71
+ if (!(0, json_js_1.isObject)(d) || d.type !== 'string')
72
+ (0, errors_js_1.fail)('E_CANON_UNIT', `a unit names a string argument of the signature: ${JSON.stringify(name)}`, p);
73
+ };
74
+ if (typeof u === 'string') {
75
+ if (!isUnitLiteral(u))
76
+ (0, errors_js_1.fail)('E_CANON_UNIT', `not a unit listed in units.md: ${JSON.stringify(u)}`, path);
77
+ return;
78
+ }
79
+ const o = u;
80
+ if ((0, json_js_1.has)(o, 'arg'))
81
+ return checkRef(o, path);
82
+ for (const side of ['of', 'per']) {
83
+ const v = o[side];
84
+ const p = (0, json_js_1.memberPath)(path, side);
85
+ if (typeof v === 'string') {
86
+ if (!isCurrency(v))
87
+ (0, errors_js_1.fail)('E_CANON_UNIT', `a rate side is a currency code: ${JSON.stringify(v)}`, p);
88
+ }
89
+ else
90
+ checkRef(v, p);
91
+ }
92
+ }
93
+ /**
94
+ * units.md section 3: the unit of one cell's value, for a descriptor and a cell that have
95
+ * passed validation and whose fn_hash matches. `returns` is the signature's `returns`.
96
+ * A referenced argument must be bound (E_ARG_MISSING), not a slice (E_SLICE), of its declared
97
+ * type (E_ARG_TYPE), and a literal unit (E_CANON_UNIT); on a rate side, a currency.
98
+ */
99
+ function resolveUnit(returns, cellArgs, path, argsPath) {
100
+ if (!(0, json_js_1.isObject)(returns))
101
+ return null; // only a number return is an object, and only it has a unit
102
+ const u = returns.unit;
103
+ const side = (v, p, currency) => {
104
+ if (typeof v === 'string')
105
+ return v; // a literal, already checked by canonUnit
106
+ const name = v.arg;
107
+ const ap = (0, json_js_1.memberPath)(argsPath, name);
108
+ if (!(0, json_js_1.has)(cellArgs, name))
109
+ (0, errors_js_1.fail)('E_ARG_MISSING', `the cell does not bind ${JSON.stringify(name)}`, argsPath);
110
+ const a = cellArgs[name];
111
+ if (a === '*')
112
+ (0, errors_js_1.fail)('E_SLICE', `${name} is a slice; its unit is not one string`, ap);
113
+ const scalar = a;
114
+ if (scalar.type !== 'string')
115
+ (0, errors_js_1.fail)('E_ARG_TYPE', `${name} is declared string`, ap);
116
+ const value = scalar.value;
117
+ if (currency ? !isCurrency(value) : !isUnitLiteral(value)) {
118
+ (0, errors_js_1.fail)('E_CANON_UNIT', `argument ${name} is not ${currency ? 'a currency code' : 'a unit'}: ${JSON.stringify(value)}`, p);
119
+ }
120
+ return value;
121
+ };
122
+ const p = (0, json_js_1.memberPath)(path, 'unit');
123
+ if (typeof u === 'string')
124
+ return u;
125
+ const o = u;
126
+ if ((0, json_js_1.has)(o, 'arg'))
127
+ return side(o, p, false);
128
+ const of = side(o.of, (0, json_js_1.memberPath)(p, 'of'), true);
129
+ const per = side(o.per, (0, json_js_1.memberPath)(p, 'per'), true);
130
+ return `${of}/${per}`;
131
+ }
package/dist/cjs/uri.d.ts CHANGED
@@ -11,13 +11,25 @@ export interface CellRef {
11
11
  ns: string;
12
12
  asof?: string;
13
13
  }
14
- /** A function descriptor's `signature` member (SPEC 7). */
14
+ /** A unit in a signature (SPEC 7.1): a literal from units.md, an argument reference, or a rate. */
15
+ export type UnitRef = {
16
+ arg: string;
17
+ };
18
+ export type Unit = string | UnitRef | {
19
+ of: string | UnitRef;
20
+ per: string | UnitRef;
21
+ };
22
+ /** A function descriptor's `signature` member (SPEC 7). Numbers declare a unit. */
15
23
  export interface Signature {
16
24
  args: Record<string, {
17
25
  type: TypeName;
18
26
  valid: boolean;
27
+ unit?: Unit;
19
28
  }>;
20
- returns: TypeName;
29
+ returns: Exclude<TypeName, 'number'> | {
30
+ type: 'number';
31
+ unit: Unit;
32
+ };
21
33
  }
22
34
  /**
23
35
  * Parse a cell URI into a cell reference, taking argument types from the function's
package/dist/cjs/uri.js CHANGED
@@ -49,19 +49,29 @@ function encodeValue(s) {
49
49
  }
50
50
  return out;
51
51
  }
52
+ /**
53
+ * SPEC 5.4 step 5: the signature is taken as given (it comes from a descriptor already checked
54
+ * under section 7), but it must be readable: an object with an `args` object whose members are
55
+ * objects with a string `type` (E_SCHEMA otherwise, in JCS order of names). Nothing else in it is
56
+ * examined; an unknown type name fails only when a value of that argument is read (step 6).
57
+ */
52
58
  function readSignature(signature) {
53
- if (signature === null || signature === undefined)
54
- throw new TypeError('parseUri needs the function signature (descriptor.signature)');
55
59
  let sig;
56
60
  try {
57
- sig = (0, json_js_1.toJson)(signature);
58
- (0, validate_js_1.structSignature)(sig, 'signature');
59
- (0, validate_js_1.canonSignature)(sig, 'signature');
61
+ sig = (0, json_js_1.normalizeValue)(signature, 'signature');
60
62
  }
61
- catch (e) {
62
- if (e instanceof errors_js_1.DatumeshError)
63
- throw new errors_js_1.DatumeshError(e.code, `invalid signature: ${e.detail}`, e.path?.replace(/^\$/, 'signature') ?? 'signature');
64
- throw e;
63
+ catch {
64
+ (0, errors_js_1.fail)('E_SCHEMA', 'the signature is not JSON data', 'signature');
65
+ }
66
+ if (!(0, json_js_1.isObject)(sig))
67
+ (0, errors_js_1.fail)('E_SCHEMA', 'the signature is not an object', 'signature');
68
+ const args = sig.args;
69
+ if (!(0, json_js_1.isObject)(args))
70
+ (0, errors_js_1.fail)('E_SCHEMA', 'the signature has no args object', 'signature.args');
71
+ for (const k of (0, json_js_1.sortedKeys)(args)) {
72
+ const d = args[k];
73
+ if (!(0, json_js_1.isObject)(d) || typeof d.type !== 'string')
74
+ (0, errors_js_1.fail)('E_SCHEMA', `the declaration of ${JSON.stringify(k)} is not an object with a string type`, (0, json_js_1.memberPath)('signature.args', k));
65
75
  }
66
76
  return sig;
67
77
  }
@@ -73,8 +83,6 @@ function readSignature(signature) {
73
83
  function parseUri(uri, signature) {
74
84
  if (typeof uri !== 'string')
75
85
  throw new TypeError('uri must be a string');
76
- const sig = readSignature(signature);
77
- const declared = sig.args;
78
86
  // 1. Outer grammar.
79
87
  if (!uri.startsWith(SCHEME) || uri.includes('#'))
80
88
  (0, errors_js_1.fail)('E_PARSE_URI', 'not cell://<namespace>/<function>[?<query>] without fragment', '$');
@@ -84,8 +92,7 @@ function parseUri(uri, signature) {
84
92
  const query = q < 0 ? null : rest.slice(q + 1);
85
93
  if (query === '')
86
94
  (0, errors_js_1.fail)('E_PARSE_URI', 'empty query', '$');
87
- // SPEC 5.1 lists userinfo and a port among outer-grammar failures (E_PARSE_URI). (The
88
- // reference tool reports them as E_CANON_NAME; see DEFECTS.md.)
95
+ // SPEC 5.1: an @ or : in the authority is userinfo or a port (E_PARSE_URI).
89
96
  const authority = path.split('/')[0];
90
97
  if (authority.includes('@'))
91
98
  (0, errors_js_1.fail)('E_PARSE_URI', 'a cell URI has no userinfo', '$');
@@ -134,7 +141,9 @@ function parseUri(uri, signature) {
134
141
  // 4. Argument count.
135
142
  if (argParams.length > lexical_js_1.LIMITS.args)
136
143
  (0, errors_js_1.fail)('E_CANON_LIMIT', `more than ${lexical_js_1.LIMITS.args} arguments`, 'uri.query');
137
- // 5. Against the signature: unknown arguments, then missing ones.
144
+ // 5. The signature (readable, E_SCHEMA), then the arguments against it: unknown arguments,
145
+ // then missing ones.
146
+ const declared = readSignature(signature).args;
138
147
  for (const p of argParams) {
139
148
  if (!Object.prototype.hasOwnProperty.call(declared, p.name))
140
149
  (0, errors_js_1.fail)('E_ARG_UNKNOWN', `function declares no argument ${JSON.stringify(p.name)}`, at(p.name));
@@ -153,6 +162,8 @@ function parseUri(uri, signature) {
153
162
  }
154
163
  const type = declared[p.name].type;
155
164
  const s = decodeValue(p.value, at(p.name));
165
+ if (!(0, lexical_js_1.isTypeName)(type))
166
+ (0, errors_js_1.fail)('E_CANON_TYPE', `unknown type ${JSON.stringify(type)} in the signature`, `signature.args.${p.name}.type`);
156
167
  if (type === 'boolean') {
157
168
  if (s !== 'true' && s !== 'false')
158
169
  (0, errors_js_1.fail)('E_CANON_TYPE', 'boolean must be true or false', at(p.name));
@@ -197,5 +208,7 @@ function formatUri(ref) {
197
208
  function httpsUrl(uri, mesh) {
198
209
  if (!uri.startsWith(SCHEME))
199
210
  (0, errors_js_1.fail)('E_PARSE_URI', 'not a cell URI', '$');
211
+ // SPEC 5.3: the mesh is a DNS name on the default HTTPS port (no port, IP address or userinfo).
212
+ (0, lexical_js_1.checkDnsName)(mesh, 'mesh');
200
213
  return `https://${mesh}/cell/${uri.slice(SCHEME.length)}`;
201
214
  }
@@ -3,8 +3,16 @@ export type Kind = 'scalar' | 'cell' | 'ref' | 'function' | 'receipt' | 'jcs';
3
3
  export declare const KINDS: readonly Kind[];
4
4
  export declare function structSignature(v: JsonValue, path: string): void;
5
5
  export declare function canonSignature(sig: JsonObject, path: string): void;
6
+ /** The return type named by a valid signature's `returns` (SPEC 10.1 step 7.4). */
7
+ export declare function returnType(sig: JsonObject): string;
6
8
  /** Validate `v` as `kind` (steps 2-3; for receipts also step 4). Throws DatumeshError. */
7
9
  export declare function validate(v: JsonValue, kind: Kind, path?: string): void;
10
+ /**
11
+ * The unit of a cell's value (SPEC 7.1, units.md 3). Order: the cell (structure, canonical
12
+ * form), fn_hash against the descriptor (E_FN_HASH_MISMATCH), the descriptor (structure,
13
+ * canonical form), then substitution.
14
+ */
15
+ export declare function cellUnit(desc: JsonValue, cell: JsonValue, fnHash: (d: JsonValue) => string): string | null;
8
16
  /** Receipt step 2 only: true when the receipt has the right members and JSON types. */
9
17
  export declare function receiptStructure(v: JsonValue): void;
10
18
  /** Receipt steps 3 and 4, after receiptStructure has passed. */
@@ -3,7 +3,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.KINDS = void 0;
4
4
  exports.structSignature = structSignature;
5
5
  exports.canonSignature = canonSignature;
6
+ exports.returnType = returnType;
6
7
  exports.validate = validate;
8
+ exports.cellUnit = cellUnit;
7
9
  exports.receiptStructure = receiptStructure;
8
10
  exports.receiptCanonicalAndConsistent = receiptCanonicalAndConsistent;
9
11
  exports.inferKind = inferKind;
@@ -13,6 +15,7 @@ exports.inferKind = inferKind;
13
15
  const errors_js_1 = require("./errors.js");
14
16
  const json_js_1 = require("./json.js");
15
17
  const lexical_js_1 = require("./lexical.js");
18
+ const units_js_1 = require("./units.js");
16
19
  exports.KINDS = ['scalar', 'cell', 'ref', 'function', 'receipt', 'jcs'];
17
20
  const SLICE = '*';
18
21
  // ---------------------------------------------------------------- step 2: structure
@@ -85,12 +88,24 @@ function structSignature(v, path) {
85
88
  for (const k of (0, json_js_1.sortedKeys)(args)) {
86
89
  const p = (0, json_js_1.memberPath)(argsPath, k);
87
90
  const d = expectObject(args[k], p);
88
- members(d, ['type', 'valid'], [], p);
91
+ members(d, ['type', 'valid'], ['unit'], p);
89
92
  expectString(d, 'type', p);
93
+ if ((0, json_js_1.has)(d, 'unit'))
94
+ (0, units_js_1.structUnit)(d.unit, (0, json_js_1.memberPath)(p, 'unit'));
90
95
  if (typeof d.valid !== 'boolean')
91
96
  (0, errors_js_1.fail)('E_SCHEMA', 'valid must be a boolean', (0, json_js_1.memberPath)(p, 'valid'));
92
97
  }
93
- expectString(sig, 'returns', path);
98
+ // SPEC 7: a type name, or {"type", "unit"} (the unit's absence is E_CANON_UNIT, in step 3).
99
+ const rp = (0, json_js_1.memberPath)(path, 'returns');
100
+ const r = sig.returns;
101
+ if (typeof r === 'string')
102
+ return;
103
+ if (!(0, json_js_1.isObject)(r))
104
+ (0, errors_js_1.fail)('E_SCHEMA', 'returns must be a type name or an object', rp);
105
+ members(r, ['type'], ['unit'], rp);
106
+ expectString(r, 'type', rp);
107
+ if ((0, json_js_1.has)(r, 'unit'))
108
+ (0, units_js_1.structUnit)(r.unit, (0, json_js_1.memberPath)(rp, 'unit'));
94
109
  }
95
110
  const RECEIPT_MEMBERS = [
96
111
  'asof',
@@ -194,14 +209,43 @@ function canonSignature(sig, path) {
194
209
  const p = (0, json_js_1.memberPath)(argsPath, k);
195
210
  (0, lexical_js_1.checkArgName)(k, p);
196
211
  const d = args[k];
212
+ // SPEC 7: type, then unit, then valid.
197
213
  if (!(0, lexical_js_1.isTypeName)(d.type))
198
214
  (0, errors_js_1.fail)('E_CANON_TYPE', `unknown type ${JSON.stringify(d.type)}`, (0, json_js_1.memberPath)(p, 'type'));
215
+ if (d.type === 'number') {
216
+ if (!(0, json_js_1.has)(d, 'unit'))
217
+ (0, errors_js_1.fail)('E_CANON_UNIT', 'a number argument declares its unit', p);
218
+ (0, units_js_1.canonUnit)(d.unit, args, (0, json_js_1.memberPath)(p, 'unit'));
219
+ }
220
+ else if ((0, json_js_1.has)(d, 'unit')) {
221
+ (0, errors_js_1.fail)('E_CANON_UNIT', 'only number arguments have a unit', (0, json_js_1.memberPath)(p, 'unit'));
222
+ }
199
223
  if (d.valid === true && !lexical_js_1.VALID_TIME_TYPES.includes(d.type)) {
200
224
  (0, errors_js_1.fail)('E_CANON_TYPE', 'a valid-time argument must be a date, instant or string', p);
201
225
  }
202
226
  }
203
- if (!(0, lexical_js_1.isTypeName)(sig.returns))
204
- (0, errors_js_1.fail)('E_CANON_TYPE', `unknown type ${JSON.stringify(sig.returns)}`, (0, json_js_1.memberPath)(path, 'returns'));
227
+ const rp = (0, json_js_1.memberPath)(path, 'returns');
228
+ const r = sig.returns;
229
+ if (typeof r === 'string') {
230
+ if (!(0, lexical_js_1.isTypeName)(r))
231
+ (0, errors_js_1.fail)('E_CANON_TYPE', `unknown type ${JSON.stringify(r)}`, rp);
232
+ if (r === 'number')
233
+ (0, errors_js_1.fail)('E_CANON_UNIT', 'a number return is {"type": "number", "unit": ...}, not the bare name', rp);
234
+ return;
235
+ }
236
+ const o = r;
237
+ if (!(0, lexical_js_1.isTypeName)(o.type))
238
+ (0, errors_js_1.fail)('E_CANON_TYPE', `unknown type ${JSON.stringify(o.type)}`, (0, json_js_1.memberPath)(rp, 'type'));
239
+ if (o.type !== 'number')
240
+ (0, errors_js_1.fail)('E_CANON_UNIT', 'only a number return is written as an object', rp);
241
+ if (!(0, json_js_1.has)(o, 'unit'))
242
+ (0, errors_js_1.fail)('E_CANON_UNIT', 'a number return declares its unit', rp);
243
+ (0, units_js_1.canonUnit)(o.unit, args, (0, json_js_1.memberPath)(rp, 'unit'));
244
+ }
245
+ /** The return type named by a valid signature's `returns` (SPEC 10.1 step 7.4). */
246
+ function returnType(sig) {
247
+ const r = sig.returns;
248
+ return typeof r === 'string' ? r : r.type;
205
249
  }
206
250
  function canonFunction(o, path) {
207
251
  const code = o.code;
@@ -317,6 +361,21 @@ function validate(v, kind, path = '$') {
317
361
  return;
318
362
  }
319
363
  }
364
+ /**
365
+ * The unit of a cell's value (SPEC 7.1, units.md 3). Order: the cell (structure, canonical
366
+ * form), fn_hash against the descriptor (E_FN_HASH_MISMATCH), the descriptor (structure,
367
+ * canonical form), then substitution.
368
+ */
369
+ function cellUnit(desc, cell, fnHash) {
370
+ structCell(cell, 'cell', false);
371
+ canonCell(cell, 'cell');
372
+ if (fnHash(desc) !== cell.fn_hash)
373
+ (0, errors_js_1.fail)('E_FN_HASH_MISMATCH', 'the descriptor does not hash to cell.fn_hash', 'descriptor');
374
+ structFunction(desc, 'descriptor');
375
+ canonFunction(desc, 'descriptor');
376
+ const sig = desc.signature;
377
+ return (0, units_js_1.resolveUnit)(sig.returns, cell.args, 'descriptor.signature.returns', 'cell.args');
378
+ }
320
379
  /** Receipt step 2 only: true when the receipt has the right members and JSON types. */
321
380
  function receiptStructure(v) {
322
381
  structReceipt(v, '$');