@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
package/dist/esm/json.js CHANGED
@@ -4,11 +4,33 @@ import { fail } from './errors.js';
4
4
  import { hasLoneSurrogate } from './unicode.js';
5
5
  import { utf8Decode } from './utf8.js';
6
6
  /**
7
- * Nesting deeper than this is rejected with E_PARSE_JSON. SPEC.md sets no depth limit; this is
8
- * an implementation limit (Datumesh objects nest at most five levels) that keeps parsing and
9
- * canonicalisation from exhausting the call stack.
7
+ * SPEC 6.1: at most 64 levels of nesting ([ and { open at once, counted outside strings), checked
8
+ * before anything else in the text. Datumesh objects nest at most five levels.
10
9
  */
11
- export const MAX_DEPTH = 1000;
10
+ export const MAX_DEPTH = 64;
11
+ /** SPEC 6.1 order step 3: true when more than MAX_DEPTH brackets are open at once. */
12
+ function tooDeep(s) {
13
+ let depth = 0;
14
+ let inString = false;
15
+ for (let i = 0; i < s.length; i++) {
16
+ const c = s.charCodeAt(i);
17
+ if (inString) {
18
+ if (c === 0x5c)
19
+ i++; // skip the escaped character
20
+ else if (c === 0x22)
21
+ inString = false;
22
+ }
23
+ else if (c === 0x22)
24
+ inString = true;
25
+ else if (c === 0x5b || c === 0x7b) {
26
+ if (++depth > MAX_DEPTH)
27
+ return true;
28
+ }
29
+ else if (c === 0x5d || c === 0x7d)
30
+ depth--;
31
+ }
32
+ return false;
33
+ }
12
34
  export function isObject(v) {
13
35
  return typeof v === 'object' && v !== null && !Array.isArray(v);
14
36
  }
@@ -26,8 +48,11 @@ export function memberPath(path, name) {
26
48
  return /^[A-Za-z_][A-Za-z0-9_]*$/.test(name) ? `${path}.${name}` : `${path}[${JSON.stringify(name)}]`;
27
49
  }
28
50
  class Parser {
29
- constructor(s) {
51
+ constructor(s,
52
+ /** When given, receives the source text of every number, by JSON path. */
53
+ numbers) {
30
54
  this.s = s;
55
+ this.numbers = numbers;
31
56
  this.i = 0;
32
57
  /** Path of the first string holding an escaped lone surrogate; reported after parsing. */
33
58
  this.surrogatePath = null;
@@ -55,7 +80,7 @@ class Parser {
55
80
  }
56
81
  value(path, depth) {
57
82
  if (depth > MAX_DEPTH)
58
- this.err(`nesting deeper than ${MAX_DEPTH} levels (implementation limit)`);
83
+ this.err(`nesting deeper than ${MAX_DEPTH} levels`); // tooDeep found it first
59
84
  const s = this.s;
60
85
  const c = s[this.i];
61
86
  if (c === '{')
@@ -77,7 +102,7 @@ class Parser {
77
102
  return null;
78
103
  }
79
104
  if (c === '-' || (c >= '0' && c <= '9'))
80
- return this.number();
105
+ return this.number(path);
81
106
  if (c === undefined)
82
107
  this.err('unexpected end of input');
83
108
  if (s.startsWith('NaN', this.i) || s.startsWith('Infinity', this.i) || s.startsWith('-Infinity', this.i)) {
@@ -219,7 +244,7 @@ class Parser {
219
244
  this.surrogatePath = path;
220
245
  return out;
221
246
  }
222
- number() {
247
+ number(path) {
223
248
  const s = this.s;
224
249
  const m = /-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?/y;
225
250
  m.lastIndex = this.i;
@@ -235,30 +260,35 @@ class Parser {
235
260
  const n = Number(text);
236
261
  if (!Number.isFinite(n))
237
262
  fail('E_PARSE_JSON', `number out of IEEE 754 range: ${text.slice(0, 40)}`, '$');
263
+ this.numbers?.set(path, text);
238
264
  return n;
239
265
  }
240
266
  }
241
267
  /** Parse JSON text strictly. */
242
- export function parseJsonText(text) {
268
+ export function parseJsonText(text, numbers) {
269
+ // SPEC 6.1 order: BOM, (invalid UTF-8: parseJsonBytes), nesting, the first of syntax / NaN /
270
+ // overflow / duplicate in text order, then lone surrogates.
243
271
  if (text.charCodeAt(0) === 0xfeff)
244
272
  fail('E_PARSE_JSON', 'byte order mark', '$');
273
+ if (tooDeep(text))
274
+ fail('E_PARSE_JSON', `nesting deeper than ${MAX_DEPTH} levels`, '$');
275
+ const p = new Parser(text, numbers);
276
+ const v = p.parseDocument();
245
277
  if (hasLoneSurrogate(text))
246
278
  fail('E_PARSE_JSON', 'lone surrogate in the text', '$');
247
- const p = new Parser(text);
248
- const v = p.parseDocument();
249
279
  if (p.surrogatePath !== null)
250
280
  fail('E_PARSE_JSON', 'lone surrogate', p.surrogatePath);
251
281
  return v;
252
282
  }
253
283
  /** Parse UTF-8 JSON bytes strictly: no BOM, no invalid UTF-8. */
254
- export function parseJsonBytes(bytes) {
284
+ export function parseJsonBytes(bytes, numbers) {
255
285
  if (bytes.length >= 3 && bytes[0] === 0xef && bytes[1] === 0xbb && bytes[2] === 0xbf) {
256
286
  fail('E_PARSE_JSON', 'byte order mark', '$');
257
287
  }
258
288
  const text = utf8Decode(bytes);
259
289
  if (text === null)
260
290
  fail('E_PARSE_JSON', 'invalid UTF-8', '$');
261
- return parseJsonText(text);
291
+ return parseJsonText(text, numbers);
262
292
  }
263
293
  /**
264
294
  * Check an already-parsed JavaScript value against the JSON data model and return a private
@@ -267,7 +297,8 @@ export function parseJsonBytes(bytes) {
267
297
  * or bytes for that.
268
298
  */
269
299
  export function normalizeValue(v, path = '$', depth = 0) {
270
- if (depth > MAX_DEPTH)
300
+ const container = typeof v === 'object' && v !== null;
301
+ if (container && depth >= MAX_DEPTH)
271
302
  fail('E_PARSE_JSON', `nesting deeper than ${MAX_DEPTH} levels (or a cycle)`, path);
272
303
  if (v === null || typeof v === 'boolean')
273
304
  return v;
@@ -298,12 +329,12 @@ export function normalizeValue(v, path = '$', depth = 0) {
298
329
  fail('E_PARSE_JSON', `${typeof v} is not a JSON value`, path);
299
330
  }
300
331
  /** JSON text (string), UTF-8 bytes (Uint8Array) or an already-parsed value. */
301
- export function toJson(input) {
332
+ export function toJson(input, numbers) {
302
333
  if (typeof input === 'string')
303
- return parseJsonText(input);
334
+ return parseJsonText(input, numbers);
304
335
  if (input instanceof Uint8Array)
305
- return parseJsonBytes(input);
336
+ return parseJsonBytes(input, numbers);
306
337
  if (input instanceof ArrayBuffer)
307
- return parseJsonBytes(new Uint8Array(input));
338
+ return parseJsonBytes(new Uint8Array(input), numbers);
308
339
  return normalizeValue(input);
309
340
  }
@@ -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/esm/keys.js CHANGED
@@ -3,6 +3,15 @@ import { DatumeshError } from './errors.js';
3
3
  import { fromHex, keyIdOf } from './hash.js';
4
4
  import { has, isObject, memberPath, sortedKeys, toJson } from './json.js';
5
5
  import { LIMITS, checkDnsName, isHex, isInstant } from './lexical.js';
6
+ function isDnsName(s) {
7
+ try {
8
+ checkDnsName(s, '$');
9
+ return true;
10
+ }
11
+ catch {
12
+ return false;
13
+ }
14
+ }
6
15
  class KeyDocError extends Error {
7
16
  constructor(msg, path) {
8
17
  super(msg);
@@ -24,6 +33,9 @@ function validateKeyDocument(doc, host) {
24
33
  bad(`missing member ${JSON.stringify(k)}`, '$');
25
34
  if (doc.proto !== 'datumesh/1')
26
35
  bad("proto must be 'datumesh/1'", '$.proto');
36
+ // 9.2 / 9.3: `host` is a DNS name (2.1); a document for any other host cannot satisfy 9.2.
37
+ if (typeof doc.host !== 'string' || !isDnsName(doc.host))
38
+ bad('host must be a DNS name (SPEC 2.1); keys are resolved only for DNS names', '$.host');
27
39
  if (doc.host !== host)
28
40
  bad(`host must be ${JSON.stringify(host)}`, '$.host');
29
41
  const keys = doc.keys;
@@ -126,6 +138,45 @@ export function keysFromDocument(doc) {
126
138
  }
127
139
  /** Default timeout of fetchKeys, in milliseconds. */
128
140
  export const FETCH_TIMEOUT_MS = 10000;
141
+ /** fetchKeys abandons a key document larger than this (SPEC 9.3: SHOULD bound retrieval). */
142
+ export const FETCH_MAX_BYTES = 256 * 1024;
143
+ class TooLarge extends Error {
144
+ }
145
+ /** Read a response body, abandoning it once it exceeds FETCH_MAX_BYTES. */
146
+ async function readBounded(res) {
147
+ const reader = res.body && typeof res.body.getReader === 'function' ? res.body.getReader() : null;
148
+ if (!reader) {
149
+ const b = new Uint8Array(await res.arrayBuffer());
150
+ if (b.length > FETCH_MAX_BYTES)
151
+ throw new TooLarge();
152
+ return b;
153
+ }
154
+ const parts = [];
155
+ let n = 0;
156
+ for (;;) {
157
+ const { done, value } = await reader.read();
158
+ if (done)
159
+ break;
160
+ n += value.length;
161
+ if (n > FETCH_MAX_BYTES) {
162
+ try {
163
+ await reader.cancel();
164
+ }
165
+ catch {
166
+ // abandoned anyway
167
+ }
168
+ throw new TooLarge();
169
+ }
170
+ parts.push(value);
171
+ }
172
+ const out = new Uint8Array(n);
173
+ let o = 0;
174
+ for (const p of parts) {
175
+ out.set(p, o);
176
+ o += p.length;
177
+ }
178
+ return out;
179
+ }
129
180
  /**
130
181
  * Fetch https://<host>/.well-known/datumesh.json once, with TLS verified by the platform, no
131
182
  * redirects followed and a timeout, then interpret it with keysFromResponse. The only function
@@ -157,9 +208,11 @@ export async function fetchKeys(host, timeoutMs = FETCH_TIMEOUT_MS) {
157
208
  status = 0;
158
209
  if (status !== 200)
159
210
  return keysFromResponse(host, status);
160
- body = new Uint8Array(await res.arrayBuffer());
211
+ body = await readBounded(res);
161
212
  }
162
213
  catch (e) {
214
+ if (e instanceof TooLarge)
215
+ return { ok: false, code: 'E_KEY_UNAVAILABLE', host, detail: `key document larger than 256 KiB; retrieval abandoned` };
163
216
  return { ok: false, code: 'E_KEY_UNAVAILABLE', host, detail: `retrieval failed: ${e instanceof Error ? e.message : String(e)}` };
164
217
  }
165
218
  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
@@ -17,7 +17,7 @@ export const LIMITS = {
17
17
  licence_url: 512,
18
18
  keys: 64,
19
19
  };
20
- export const TYPES = ['boolean', 'date', 'instant', 'number', 'string'];
20
+ export const TYPES = ['blob', 'boolean', 'date', 'instant', 'number', 'string'];
21
21
  export const VALID_TIME_TYPES = ['date', 'instant', 'string'];
22
22
  export function isTypeName(t) {
23
23
  return typeof t === 'string' && TYPES.includes(t);
@@ -87,6 +87,10 @@ export function checkKeyRef(s, path) {
87
87
  return { host, id };
88
88
  }
89
89
  const NUMBER_RE = /^-?(?:0|[1-9][0-9]*)(?:\.[0-9]*[1-9])?$/;
90
+ /** SPEC 3.1: a canonical decimal of at most 128 characters. */
91
+ export function isNumber(s) {
92
+ return NUMBER_RE.test(s) && s !== '-0' && s.length <= LIMITS.number;
93
+ }
90
94
  /** SPEC 3.1. Grammar (E_CANON_NUMBER) before the 128-character limit (E_CANON_LIMIT). */
91
95
  export function checkNumber(s, path) {
92
96
  if (!NUMBER_RE.test(s) || s === '-0')
@@ -146,8 +150,24 @@ export function checkString(s, path, opts = {}) {
146
150
  function hex4(cp) {
147
151
  return cp.toString(16).toUpperCase().padStart(4, '0');
148
152
  }
153
+ // SPEC 3.7: <sha256>:<length>:<type>/<subtype>, with RFC 6838 restricted names in lowercase.
154
+ const BLOB_RE = /^[0-9a-f]{64}:(0|[1-9][0-9]*):([a-z0-9][a-z0-9!#$&^_.+-]*\/[a-z0-9][a-z0-9!#$&^_.+-]*)$/;
155
+ const MAX_SAFE = '9007199254740991';
156
+ export const MEDIA_TYPE_MAX = 128;
157
+ /** SPEC 3.7. Grammar, then length <= 2^53 - 1, then media type <= 128 characters; all E_CANON_BLOB. */
158
+ export function checkBlob(s, path) {
159
+ const m = BLOB_RE.exec(s);
160
+ if (!m)
161
+ fail('E_CANON_BLOB', `not <sha256>:<length>:<type/subtype>: ${JSON.stringify(s)}`, path);
162
+ const len = m[1];
163
+ if (len.length > MAX_SAFE.length || (len.length === MAX_SAFE.length && len > MAX_SAFE))
164
+ fail('E_CANON_BLOB', `length above ${MAX_SAFE}`, path);
165
+ if (m[2].length > MEDIA_TYPE_MAX)
166
+ fail('E_CANON_BLOB', `media type longer than ${MEDIA_TYPE_MAX} characters`, path);
167
+ return { sha256: s.slice(0, 64), length: Number(len), mediaType: m[2] };
168
+ }
149
169
  const SPDX_RE = /^[A-Za-z0-9][A-Za-z0-9.+-]*$/;
150
- const URL_RE = /^https:\/\/[!-~]+$/;
170
+ export const URL_RE = /^https:\/\/[!-~]+$/;
151
171
  /**
152
172
  * SPEC 8.1 `licence`: an SPDX licence identifier or an https:// URL. SPEC.md gives no grammar;
153
173
  * this follows schemas/receipt.schema.json (and the reference tool): an identifier matches
@@ -182,6 +202,8 @@ export function checkTypedValue(type, value, path) {
182
202
  checkDate(value, path);
183
203
  else if (type === 'instant')
184
204
  checkInstant(value, path);
205
+ else if (type === 'blob')
206
+ checkBlob(value, path);
185
207
  else
186
208
  checkString(value, path);
187
209
  }
@@ -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,8 @@
1
+ // GENERATED by tools/gen-units.mjs from spec/units.md (the `units-…` fenced blocks).
2
+ // Do not edit by hand; test/units.test.mjs checks that this file matches units.md.
3
+ /** Literal units by group, in units.md order. Index units (units.md 2.4) are a pattern. */
4
+ export const UNIT_GROUPS = {
5
+ 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"],
6
+ physical: ["m", "kg", "s", "A", "K", "mol", "cd", "degC", "m/s", "W", "kWh", "mm", "cm", "km/h", "hPa", "W/m2"],
7
+ ratio: ["percent", "permille", "bp", "1", "count"],
8
+ };
@@ -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,123 @@
1
+ // Units (SPEC.md 7.1, units.md): shapes in a signature (step 2, E_SCHEMA), validity against the
2
+ // closed list (step 3, E_CANON_UNIT), and resolution of a cell's unit (units.md section 3).
3
+ import { fail } from './errors.js';
4
+ import { has, isObject, memberPath } from './json.js';
5
+ import { isDate, isNumber } from './lexical.js';
6
+ import { UNIT_GROUPS } from './units-data.js';
7
+ const CURRENCY = new Set(UNIT_GROUPS.currency);
8
+ const LITERAL = new Set(Object.values(UNIT_GROUPS).flat());
9
+ const INDEX_RE = /^index:([0-9]{4}(?:-[0-9]{2}(?:-[0-9]{2})?)?)=(.*)$/;
10
+ const INDEX_MAX = 64;
11
+ /** A currency code from units.md 2.1 (List One and the withdrawn codes). */
12
+ export function isCurrency(s) {
13
+ return typeof s === 'string' && CURRENCY.has(s);
14
+ }
15
+ /** units.md 2.4: index:<YYYY[-MM[-DD]]>=<number greater than 0>, at most 64 characters. */
16
+ export function isIndexUnit(s) {
17
+ if (s.length > INDEX_MAX)
18
+ return false;
19
+ const m = INDEX_RE.exec(s);
20
+ if (!m)
21
+ return false;
22
+ const [y, mo = '01', d = '01'] = m[1].split('-');
23
+ if (!isDate(`${y}-${mo}-${d}`))
24
+ return false;
25
+ return isNumber(m[2]) && !m[2].startsWith('-') && m[2] !== '0';
26
+ }
27
+ /** A literal unit (units.md section 2): a listed string or an index unit. Byte-for-byte. */
28
+ export function isUnitLiteral(s) {
29
+ return typeof s === 'string' && (LITERAL.has(s) || isIndexUnit(s));
30
+ }
31
+ const isRef = (u) => Object.keys(u).length === 1 && has(u, 'arg') && typeof u.arg === 'string';
32
+ /**
33
+ * Step 2 for a unit: a string, {"arg": <string>}, or {"of": side, "per": side} where a side is
34
+ * a string or {"arg": <string>}. Anything else is E_SCHEMA (SPEC 7.1, units.md 1).
35
+ */
36
+ export function structUnit(u, path) {
37
+ if (typeof u === 'string')
38
+ return;
39
+ if (isObject(u)) {
40
+ if (isRef(u))
41
+ return;
42
+ const keys = Object.keys(u);
43
+ if (keys.length === 2 && has(u, 'of') && has(u, 'per')) {
44
+ for (const side of ['of', 'per']) {
45
+ const v = u[side];
46
+ if (typeof v === 'string' || (isObject(v) && isRef(v)))
47
+ continue;
48
+ fail('E_SCHEMA', 'a rate side is a currency code or {"arg": name}', memberPath(path, side));
49
+ }
50
+ return;
51
+ }
52
+ }
53
+ fail('E_SCHEMA', 'a unit is a string, {"arg": name} or {"of", "per"}', path);
54
+ }
55
+ /**
56
+ * Step 3 for a unit whose shape passed structUnit. `args` is the signature's `args`: a
57
+ * reference must name one of them whose declared type is `string` (E_CANON_UNIT).
58
+ */
59
+ export function canonUnit(u, args, path) {
60
+ const checkRef = (r, p) => {
61
+ const name = r.arg;
62
+ const d = has(args, name) ? args[name] : null;
63
+ if (!isObject(d) || d.type !== 'string')
64
+ fail('E_CANON_UNIT', `a unit names a string argument of the signature: ${JSON.stringify(name)}`, p);
65
+ };
66
+ if (typeof u === 'string') {
67
+ if (!isUnitLiteral(u))
68
+ fail('E_CANON_UNIT', `not a unit listed in units.md: ${JSON.stringify(u)}`, path);
69
+ return;
70
+ }
71
+ const o = u;
72
+ if (has(o, 'arg'))
73
+ return checkRef(o, path);
74
+ for (const side of ['of', 'per']) {
75
+ const v = o[side];
76
+ const p = memberPath(path, side);
77
+ if (typeof v === 'string') {
78
+ if (!isCurrency(v))
79
+ fail('E_CANON_UNIT', `a rate side is a currency code: ${JSON.stringify(v)}`, p);
80
+ }
81
+ else
82
+ checkRef(v, p);
83
+ }
84
+ }
85
+ /**
86
+ * units.md section 3: the unit of one cell's value, for a descriptor and a cell that have
87
+ * passed validation and whose fn_hash matches. `returns` is the signature's `returns`.
88
+ * A referenced argument must be bound (E_ARG_MISSING), not a slice (E_SLICE), of its declared
89
+ * type (E_ARG_TYPE), and a literal unit (E_CANON_UNIT); on a rate side, a currency.
90
+ */
91
+ export function resolveUnit(returns, cellArgs, path, argsPath) {
92
+ if (!isObject(returns))
93
+ return null; // only a number return is an object, and only it has a unit
94
+ const u = returns.unit;
95
+ const side = (v, p, currency) => {
96
+ if (typeof v === 'string')
97
+ return v; // a literal, already checked by canonUnit
98
+ const name = v.arg;
99
+ const ap = memberPath(argsPath, name);
100
+ if (!has(cellArgs, name))
101
+ fail('E_ARG_MISSING', `the cell does not bind ${JSON.stringify(name)}`, argsPath);
102
+ const a = cellArgs[name];
103
+ if (a === '*')
104
+ fail('E_SLICE', `${name} is a slice; its unit is not one string`, ap);
105
+ const scalar = a;
106
+ if (scalar.type !== 'string')
107
+ fail('E_ARG_TYPE', `${name} is declared string`, ap);
108
+ const value = scalar.value;
109
+ if (currency ? !isCurrency(value) : !isUnitLiteral(value)) {
110
+ fail('E_CANON_UNIT', `argument ${name} is not ${currency ? 'a currency code' : 'a unit'}: ${JSON.stringify(value)}`, p);
111
+ }
112
+ return value;
113
+ };
114
+ const p = memberPath(path, 'unit');
115
+ if (typeof u === 'string')
116
+ return u;
117
+ const o = u;
118
+ if (has(o, 'arg'))
119
+ return side(o, p, false);
120
+ const of = side(o.of, memberPath(p, 'of'), true);
121
+ const per = side(o.per, memberPath(p, 'per'), true);
122
+ return `${of}/${per}`;
123
+ }
package/dist/esm/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/esm/uri.js CHANGED
@@ -1,9 +1,9 @@
1
1
  // Cell URIs (SPEC.md section 5).
2
- import { DatumeshError, fail } from './errors.js';
3
- import { isObject, sortedKeys, toJson } from './json.js';
4
- import { LIMITS, checkArgName, checkInstant, checkName, checkNamespace, checkTypedValue } from './lexical.js';
2
+ import { fail } from './errors.js';
3
+ import { isObject, memberPath, normalizeValue, sortedKeys, toJson } from './json.js';
4
+ import { LIMITS, checkArgName, checkDnsName, checkInstant, checkName, checkNamespace, checkTypedValue, isTypeName } from './lexical.js';
5
5
  import { utf8Decode, utf8Encode } from './utf8.js';
6
- import { canonSignature, structSignature, validate } from './validate.js';
6
+ import { validate } from './validate.js';
7
7
  const SCHEME = 'cell://';
8
8
  function isLiteral(c) {
9
9
  return /^[A-Za-z0-9\-._~:]$/.test(c);
@@ -44,19 +44,29 @@ function encodeValue(s) {
44
44
  }
45
45
  return out;
46
46
  }
47
+ /**
48
+ * SPEC 5.4 step 5: the signature is taken as given (it comes from a descriptor already checked
49
+ * under section 7), but it must be readable: an object with an `args` object whose members are
50
+ * objects with a string `type` (E_SCHEMA otherwise, in JCS order of names). Nothing else in it is
51
+ * examined; an unknown type name fails only when a value of that argument is read (step 6).
52
+ */
47
53
  function readSignature(signature) {
48
- if (signature === null || signature === undefined)
49
- throw new TypeError('parseUri needs the function signature (descriptor.signature)');
50
54
  let sig;
51
55
  try {
52
- sig = toJson(signature);
53
- structSignature(sig, 'signature');
54
- canonSignature(sig, 'signature');
56
+ sig = normalizeValue(signature, 'signature');
55
57
  }
56
- catch (e) {
57
- if (e instanceof DatumeshError)
58
- throw new DatumeshError(e.code, `invalid signature: ${e.detail}`, e.path?.replace(/^\$/, 'signature') ?? 'signature');
59
- throw e;
58
+ catch {
59
+ fail('E_SCHEMA', 'the signature is not JSON data', 'signature');
60
+ }
61
+ if (!isObject(sig))
62
+ fail('E_SCHEMA', 'the signature is not an object', 'signature');
63
+ const args = sig.args;
64
+ if (!isObject(args))
65
+ fail('E_SCHEMA', 'the signature has no args object', 'signature.args');
66
+ for (const k of sortedKeys(args)) {
67
+ const d = args[k];
68
+ if (!isObject(d) || typeof d.type !== 'string')
69
+ fail('E_SCHEMA', `the declaration of ${JSON.stringify(k)} is not an object with a string type`, memberPath('signature.args', k));
60
70
  }
61
71
  return sig;
62
72
  }
@@ -68,8 +78,6 @@ function readSignature(signature) {
68
78
  export function parseUri(uri, signature) {
69
79
  if (typeof uri !== 'string')
70
80
  throw new TypeError('uri must be a string');
71
- const sig = readSignature(signature);
72
- const declared = sig.args;
73
81
  // 1. Outer grammar.
74
82
  if (!uri.startsWith(SCHEME) || uri.includes('#'))
75
83
  fail('E_PARSE_URI', 'not cell://<namespace>/<function>[?<query>] without fragment', '$');
@@ -79,8 +87,7 @@ export function parseUri(uri, signature) {
79
87
  const query = q < 0 ? null : rest.slice(q + 1);
80
88
  if (query === '')
81
89
  fail('E_PARSE_URI', 'empty query', '$');
82
- // SPEC 5.1 lists userinfo and a port among outer-grammar failures (E_PARSE_URI). (The
83
- // reference tool reports them as E_CANON_NAME; see DEFECTS.md.)
90
+ // SPEC 5.1: an @ or : in the authority is userinfo or a port (E_PARSE_URI).
84
91
  const authority = path.split('/')[0];
85
92
  if (authority.includes('@'))
86
93
  fail('E_PARSE_URI', 'a cell URI has no userinfo', '$');
@@ -129,7 +136,9 @@ export function parseUri(uri, signature) {
129
136
  // 4. Argument count.
130
137
  if (argParams.length > LIMITS.args)
131
138
  fail('E_CANON_LIMIT', `more than ${LIMITS.args} arguments`, 'uri.query');
132
- // 5. Against the signature: unknown arguments, then missing ones.
139
+ // 5. The signature (readable, E_SCHEMA), then the arguments against it: unknown arguments,
140
+ // then missing ones.
141
+ const declared = readSignature(signature).args;
133
142
  for (const p of argParams) {
134
143
  if (!Object.prototype.hasOwnProperty.call(declared, p.name))
135
144
  fail('E_ARG_UNKNOWN', `function declares no argument ${JSON.stringify(p.name)}`, at(p.name));
@@ -148,6 +157,8 @@ export function parseUri(uri, signature) {
148
157
  }
149
158
  const type = declared[p.name].type;
150
159
  const s = decodeValue(p.value, at(p.name));
160
+ if (!isTypeName(type))
161
+ fail('E_CANON_TYPE', `unknown type ${JSON.stringify(type)} in the signature`, `signature.args.${p.name}.type`);
151
162
  if (type === 'boolean') {
152
163
  if (s !== 'true' && s !== 'false')
153
164
  fail('E_CANON_TYPE', 'boolean must be true or false', at(p.name));
@@ -192,5 +203,7 @@ export function formatUri(ref) {
192
203
  export function httpsUrl(uri, mesh) {
193
204
  if (!uri.startsWith(SCHEME))
194
205
  fail('E_PARSE_URI', 'not a cell URI', '$');
206
+ // SPEC 5.3: the mesh is a DNS name on the default HTTPS port (no port, IP address or userinfo).
207
+ checkDnsName(mesh, 'mesh');
195
208
  return `https://${mesh}/cell/${uri.slice(SCHEME.length)}`;
196
209
  }
@@ -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. */