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