@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.
- package/README.md +7 -5
- package/dist/cjs/ed25519.js +2 -0
- package/dist/cjs/envelope.d.ts +38 -0
- package/dist/cjs/envelope.js +243 -0
- package/dist/cjs/errors.d.ts +2 -2
- package/dist/cjs/errors.js +5 -1
- package/dist/cjs/index.d.ts +13 -3
- package/dist/cjs/index.js +22 -3
- package/dist/cjs/json.d.ts +6 -7
- package/dist/cjs/json.js +49 -18
- package/dist/cjs/keys.d.ts +2 -0
- package/dist/cjs/keys.js +55 -2
- package/dist/cjs/lexical.d.ts +11 -1
- package/dist/cjs/lexical.js +28 -4
- package/dist/cjs/units-data.d.ts +4 -0
- package/dist/cjs/units-data.js +11 -0
- package/dist/cjs/units.d.ts +24 -0
- package/dist/cjs/units.js +131 -0
- package/dist/cjs/uri.d.ts +14 -2
- package/dist/cjs/uri.js +27 -14
- package/dist/cjs/validate.d.ts +8 -0
- package/dist/cjs/validate.js +63 -4
- package/dist/cjs/verify.d.ts +21 -2
- package/dist/cjs/verify.js +47 -7
- package/dist/esm/ed25519.js +2 -0
- package/dist/esm/envelope.d.ts +38 -0
- package/dist/esm/envelope.js +240 -0
- package/dist/esm/errors.d.ts +2 -2
- package/dist/esm/errors.js +5 -1
- package/dist/esm/index.d.ts +13 -3
- package/dist/esm/index.js +20 -4
- package/dist/esm/json.d.ts +6 -7
- package/dist/esm/json.js +49 -18
- package/dist/esm/keys.d.ts +2 -0
- package/dist/esm/keys.js +54 -1
- package/dist/esm/lexical.d.ts +11 -1
- package/dist/esm/lexical.js +24 -2
- package/dist/esm/units-data.d.ts +4 -0
- package/dist/esm/units-data.js +8 -0
- package/dist/esm/units.d.ts +24 -0
- package/dist/esm/units.js +123 -0
- package/dist/esm/uri.d.ts +14 -2
- package/dist/esm/uri.js +31 -18
- package/dist/esm/validate.d.ts +8 -0
- package/dist/esm/validate.js +61 -4
- package/dist/esm/verify.d.ts +21 -2
- package/dist/esm/verify.js +46 -8
- 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
|
-
*
|
|
8
|
-
*
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
}
|
package/dist/esm/keys.d.ts
CHANGED
|
@@ -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 =
|
|
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);
|
package/dist/esm/lexical.d.ts
CHANGED
|
@@ -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
|
package/dist/esm/lexical.js
CHANGED
|
@@ -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,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
|
|
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 {
|
|
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 {
|
|
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 =
|
|
53
|
-
structSignature(sig, 'signature');
|
|
54
|
-
canonSignature(sig, 'signature');
|
|
56
|
+
sig = normalizeValue(signature, 'signature');
|
|
55
57
|
}
|
|
56
|
-
catch
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
|
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.
|
|
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
|
}
|
package/dist/esm/validate.d.ts
CHANGED
|
@@ -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. */
|