@datumesh/verify 0.0.0-stage → 0.1.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/LICENSE +202 -0
- package/README.md +54 -2
- package/dist/cjs/ed25519.d.ts +2 -0
- package/dist/cjs/ed25519.js +82 -0
- package/dist/cjs/errors.d.ts +14 -0
- package/dist/cjs/errors.js +61 -0
- package/dist/cjs/hash.d.ts +6 -0
- package/dist/cjs/hash.js +27 -0
- package/dist/cjs/index.d.ts +27 -0
- package/dist/cjs/index.js +83 -0
- package/dist/cjs/jcs.d.ts +5 -0
- package/dist/cjs/jcs.js +25 -0
- package/dist/cjs/json.d.ts +29 -0
- package/dist/cjs/json.js +321 -0
- package/dist/cjs/keys.d.ts +73 -0
- package/dist/cjs/keys.js +257 -0
- package/dist/cjs/lexical.d.ts +59 -0
- package/dist/cjs/lexical.js +207 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/unicode-data.d.ts +8 -0
- package/dist/cjs/unicode-data.js +14 -0
- package/dist/cjs/unicode.d.ts +12 -0
- package/dist/cjs/unicode.js +192 -0
- package/dist/cjs/uri.d.ts +31 -0
- package/dist/cjs/uri.js +201 -0
- package/dist/cjs/utf8.d.ts +10 -0
- package/dist/cjs/utf8.js +101 -0
- package/dist/cjs/validate.d.ts +17 -0
- package/dist/cjs/validate.js +349 -0
- package/dist/cjs/verify.d.ts +38 -0
- package/dist/cjs/verify.js +192 -0
- package/dist/esm/ed25519.d.ts +2 -0
- package/dist/esm/ed25519.js +79 -0
- package/dist/esm/errors.d.ts +14 -0
- package/dist/esm/errors.js +56 -0
- package/dist/esm/hash.d.ts +6 -0
- package/dist/esm/hash.js +21 -0
- package/dist/esm/index.d.ts +27 -0
- package/dist/esm/index.js +63 -0
- package/dist/esm/jcs.d.ts +5 -0
- package/dist/esm/jcs.js +18 -0
- package/dist/esm/json.d.ts +29 -0
- package/dist/esm/json.js +309 -0
- package/dist/esm/keys.d.ts +73 -0
- package/dist/esm/keys.js +249 -0
- package/dist/esm/lexical.d.ts +59 -0
- package/dist/esm/lexical.js +187 -0
- package/dist/esm/package.json +3 -0
- package/dist/esm/unicode-data.d.ts +8 -0
- package/dist/esm/unicode-data.js +11 -0
- package/dist/esm/unicode.d.ts +12 -0
- package/dist/esm/unicode.js +184 -0
- package/dist/esm/uri.d.ts +31 -0
- package/dist/esm/uri.js +196 -0
- package/dist/esm/utf8.d.ts +10 -0
- package/dist/esm/utf8.js +96 -0
- package/dist/esm/validate.d.ts +17 -0
- package/dist/esm/validate.js +340 -0
- package/dist/esm/verify.d.ts +38 -0
- package/dist/esm/verify.js +189 -0
- package/package.json +54 -4
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/** The spec revision this library conforms to. */
|
|
2
|
+
export const SPEC_VERSION = 'spec-0.1.1';
|
|
3
|
+
/** Every error code defined by SPEC.md 10.2, in the order of that table. */
|
|
4
|
+
export const ERROR_CODES = [
|
|
5
|
+
'E_PARSE_JSON',
|
|
6
|
+
'E_PARSE_DUPLICATE_KEY',
|
|
7
|
+
'E_PARSE_URI',
|
|
8
|
+
'E_SCHEMA',
|
|
9
|
+
'E_CANON_NAME',
|
|
10
|
+
'E_CANON_ARG_NAME',
|
|
11
|
+
'E_CANON_ARG_DUPLICATE',
|
|
12
|
+
'E_CANON_ARG_ORDER',
|
|
13
|
+
'E_CANON_PERCENT',
|
|
14
|
+
'E_CANON_TYPE',
|
|
15
|
+
'E_CANON_NUMBER',
|
|
16
|
+
'E_CANON_DATE',
|
|
17
|
+
'E_CANON_INSTANT',
|
|
18
|
+
'E_CANON_STRING',
|
|
19
|
+
'E_CANON_HEX',
|
|
20
|
+
'E_CANON_LICENCE',
|
|
21
|
+
'E_CANON_SET',
|
|
22
|
+
'E_CANON_LIMIT',
|
|
23
|
+
'E_SLICE',
|
|
24
|
+
'E_PUBLISHER_MISMATCH',
|
|
25
|
+
'E_VALID_MISMATCH',
|
|
26
|
+
'E_TIME_ORDER',
|
|
27
|
+
'E_ARG_UNKNOWN',
|
|
28
|
+
'E_ARG_MISSING',
|
|
29
|
+
'E_KEY_UNAVAILABLE',
|
|
30
|
+
'E_KEY_DOC',
|
|
31
|
+
'E_KEY_UNKNOWN',
|
|
32
|
+
'E_KEY_NOT_VALID',
|
|
33
|
+
'E_SIGNATURE',
|
|
34
|
+
'E_FN_UNAVAILABLE',
|
|
35
|
+
'E_FN_HASH_MISMATCH',
|
|
36
|
+
'E_ARG_TYPE',
|
|
37
|
+
'E_RETURN_TYPE',
|
|
38
|
+
'E_INPUT_UNAVAILABLE',
|
|
39
|
+
'E_INPUT_INVALID',
|
|
40
|
+
'E_INPUT_UNDECLARED',
|
|
41
|
+
'E_VALUE_MISMATCH',
|
|
42
|
+
];
|
|
43
|
+
/** A Datumesh rule was broken. `code` is a SPEC.md 10.2 code; `path` locates the failure. */
|
|
44
|
+
export class DatumeshError extends Error {
|
|
45
|
+
constructor(code, detail, path = null) {
|
|
46
|
+
super(`${code}${path ? ` at ${path}` : ''}: ${detail}`);
|
|
47
|
+
this.name = 'DatumeshError';
|
|
48
|
+
this.code = code;
|
|
49
|
+
this.path = path;
|
|
50
|
+
this.detail = detail;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** Shorthand used throughout: throw a DatumeshError. */
|
|
54
|
+
export function fail(code, detail, path = null) {
|
|
55
|
+
throw new DatumeshError(code, detail, path);
|
|
56
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare function toHex(b: Uint8Array): string;
|
|
2
|
+
export declare function fromHex(s: string): Uint8Array;
|
|
3
|
+
/** SHA-256 in lowercase hex. */
|
|
4
|
+
export declare function sha256Hex(b: Uint8Array): string;
|
|
5
|
+
/** SPEC 9.1: first 16 hex digits of SHA-256 over the raw 32-byte public key. */
|
|
6
|
+
export declare function keyIdOf(publicKey: Uint8Array): string;
|
package/dist/esm/hash.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { sha256 } from '@noble/hashes/sha2';
|
|
2
|
+
export function toHex(b) {
|
|
3
|
+
let s = '';
|
|
4
|
+
for (let i = 0; i < b.length; i++)
|
|
5
|
+
s += b[i].toString(16).padStart(2, '0');
|
|
6
|
+
return s;
|
|
7
|
+
}
|
|
8
|
+
export function fromHex(s) {
|
|
9
|
+
const out = new Uint8Array(s.length / 2);
|
|
10
|
+
for (let i = 0; i < out.length; i++)
|
|
11
|
+
out[i] = parseInt(s.slice(2 * i, 2 * i + 2), 16);
|
|
12
|
+
return out;
|
|
13
|
+
}
|
|
14
|
+
/** SHA-256 in lowercase hex. */
|
|
15
|
+
export function sha256Hex(b) {
|
|
16
|
+
return toHex(sha256(b));
|
|
17
|
+
}
|
|
18
|
+
/** SPEC 9.1: first 16 hex digits of SHA-256 over the raw 32-byte public key. */
|
|
19
|
+
export function keyIdOf(publicKey) {
|
|
20
|
+
return sha256Hex(publicKey).slice(0, 16);
|
|
21
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type Kind } from './validate.js';
|
|
2
|
+
export { DatumeshError, ERROR_CODES, SPEC_VERSION, type ErrorCode } from './errors.js';
|
|
3
|
+
export { verify, type VerifyResult, type Recompute } from './verify.js';
|
|
4
|
+
export { parseUri, formatUri, httpsUrl, type CellRef, type TypedScalar, type Signature } from './uri.js';
|
|
5
|
+
export { keysFromResponse, keysFromDocument, fetchKeys, FETCH_TIMEOUT_MS, type KeysResult, type KeyEntry, type KeyMap, type KeySource } from './keys.js';
|
|
6
|
+
export type { Kind } from './validate.js';
|
|
7
|
+
export type { JsonValue } from './json.js';
|
|
8
|
+
/** Library version. */
|
|
9
|
+
export declare const VERSION = "0.1.1";
|
|
10
|
+
/**
|
|
11
|
+
* Validate `input` as `kind` and return its RFC 8785 canonical bytes. `input` is JSON text,
|
|
12
|
+
* UTF-8 bytes, or an already-parsed value. `kind` is "scalar", "cell", "ref", "function" or
|
|
13
|
+
* "receipt" (inferred from the members when omitted), or "jcs" for plain RFC 8785 with only the
|
|
14
|
+
* strict-parsing rules of SPEC 6.1. A receipt is returned whole, `sig` included, and is checked
|
|
15
|
+
* through SPEC 10.1 step 4. Throws DatumeshError.
|
|
16
|
+
*/
|
|
17
|
+
export declare function canonicalize(input: unknown, kind?: Kind | null): Uint8Array;
|
|
18
|
+
/** Cell id (SPEC 4.4): SHA-256 of the cell's canonical bytes, lowercase hex. Throws DatumeshError. */
|
|
19
|
+
export declare function cellHash(cell: unknown): string;
|
|
20
|
+
/** Function hash (SPEC 7): SHA-256 of the descriptor's canonical bytes. Throws DatumeshError. */
|
|
21
|
+
export declare function functionHash(descriptor: unknown): string;
|
|
22
|
+
/** Receipt hash (SPEC 8.3): SHA-256 of the canonical receipt without `sig`. Throws DatumeshError. */
|
|
23
|
+
export declare function receiptHash(receipt: unknown): string;
|
|
24
|
+
/** The signed payload of a receipt (SPEC 8.3): canonical bytes without `sig`. Throws DatumeshError. */
|
|
25
|
+
export declare function receiptPayload(receipt: unknown): Uint8Array;
|
|
26
|
+
/** Key id of an Ed25519 public key given as 64 lowercase hex digits (SPEC 9.1). Throws DatumeshError. */
|
|
27
|
+
export declare function keyId(publicKeyHex: string): string;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @datumesh/verify: verify Datumesh receipts (SPEC.md, revision spec-0.1.1).
|
|
3
|
+
*
|
|
4
|
+
* Everything here is synchronous and offline except fetchKeys, the only function that uses the
|
|
5
|
+
* network.
|
|
6
|
+
*/
|
|
7
|
+
import { fail } from './errors.js';
|
|
8
|
+
import { sha256Hex, keyIdOf, fromHex } from './hash.js';
|
|
9
|
+
import { newObject, toJson } from './json.js';
|
|
10
|
+
import { jcsBytes } from './jcs.js';
|
|
11
|
+
import { isHex } from './lexical.js';
|
|
12
|
+
import { KINDS, inferKind, validate } from './validate.js';
|
|
13
|
+
export { DatumeshError, ERROR_CODES, SPEC_VERSION } from './errors.js';
|
|
14
|
+
export { verify } from './verify.js';
|
|
15
|
+
export { parseUri, formatUri, httpsUrl } from './uri.js';
|
|
16
|
+
export { keysFromResponse, keysFromDocument, fetchKeys, FETCH_TIMEOUT_MS } from './keys.js';
|
|
17
|
+
/** Library version. */
|
|
18
|
+
export const VERSION = '0.1.1';
|
|
19
|
+
function parseAndValidate(input, kind) {
|
|
20
|
+
if (kind !== undefined && kind !== null && !KINDS.includes(kind))
|
|
21
|
+
throw new TypeError(`unknown kind ${JSON.stringify(kind)}; expected one of ${KINDS.join(', ')}`);
|
|
22
|
+
const value = toJson(input);
|
|
23
|
+
const k = kind ?? inferKind(value);
|
|
24
|
+
validate(value, k);
|
|
25
|
+
return { value, kind: k };
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Validate `input` as `kind` and return its RFC 8785 canonical bytes. `input` is JSON text,
|
|
29
|
+
* UTF-8 bytes, or an already-parsed value. `kind` is "scalar", "cell", "ref", "function" or
|
|
30
|
+
* "receipt" (inferred from the members when omitted), or "jcs" for plain RFC 8785 with only the
|
|
31
|
+
* strict-parsing rules of SPEC 6.1. A receipt is returned whole, `sig` included, and is checked
|
|
32
|
+
* through SPEC 10.1 step 4. Throws DatumeshError.
|
|
33
|
+
*/
|
|
34
|
+
export function canonicalize(input, kind) {
|
|
35
|
+
return jcsBytes(parseAndValidate(input, kind).value);
|
|
36
|
+
}
|
|
37
|
+
/** Cell id (SPEC 4.4): SHA-256 of the cell's canonical bytes, lowercase hex. Throws DatumeshError. */
|
|
38
|
+
export function cellHash(cell) {
|
|
39
|
+
return sha256Hex(jcsBytes(parseAndValidate(cell, 'cell').value));
|
|
40
|
+
}
|
|
41
|
+
/** Function hash (SPEC 7): SHA-256 of the descriptor's canonical bytes. Throws DatumeshError. */
|
|
42
|
+
export function functionHash(descriptor) {
|
|
43
|
+
return sha256Hex(jcsBytes(parseAndValidate(descriptor, 'function').value));
|
|
44
|
+
}
|
|
45
|
+
/** Receipt hash (SPEC 8.3): SHA-256 of the canonical receipt without `sig`. Throws DatumeshError. */
|
|
46
|
+
export function receiptHash(receipt) {
|
|
47
|
+
return sha256Hex(receiptPayload(receipt));
|
|
48
|
+
}
|
|
49
|
+
/** The signed payload of a receipt (SPEC 8.3): canonical bytes without `sig`. Throws DatumeshError. */
|
|
50
|
+
export function receiptPayload(receipt) {
|
|
51
|
+
const r = parseAndValidate(receipt, 'receipt').value;
|
|
52
|
+
const o = newObject();
|
|
53
|
+
for (const k of Object.keys(r))
|
|
54
|
+
if (k !== 'sig')
|
|
55
|
+
o[k] = r[k];
|
|
56
|
+
return jcsBytes(o);
|
|
57
|
+
}
|
|
58
|
+
/** Key id of an Ed25519 public key given as 64 lowercase hex digits (SPEC 9.1). Throws DatumeshError. */
|
|
59
|
+
export function keyId(publicKeyHex) {
|
|
60
|
+
if (typeof publicKeyHex !== 'string' || !isHex(publicKeyHex, 64))
|
|
61
|
+
fail('E_CANON_HEX', 'expected 64 lowercase hex digits', '$');
|
|
62
|
+
return keyIdOf(fromHex(publicKeyHex));
|
|
63
|
+
}
|
package/dist/esm/jcs.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// RFC 8785 JSON Canonicalization Scheme. Serialisation is delegated to the `canonicalize`
|
|
2
|
+
// package (the RFC authors' implementation), which relies on ECMAScript JSON.stringify for
|
|
3
|
+
// strings and numbers and on the default (UTF-16 code unit) sort for member names, exactly as
|
|
4
|
+
// RFC 8785 specifies. It is only ever given values produced by ./json.ts (plain data, finite
|
|
5
|
+
// numbers, well-formed strings), so its toJSON/undefined handling never comes into play.
|
|
6
|
+
import serialize from 'canonicalize';
|
|
7
|
+
import { utf8Encode } from './utf8.js';
|
|
8
|
+
/** RFC 8785 canonical text of a JSON value. */
|
|
9
|
+
export function jcsText(v) {
|
|
10
|
+
const s = serialize(v);
|
|
11
|
+
if (typeof s !== 'string')
|
|
12
|
+
throw new Error('canonicalize returned no text');
|
|
13
|
+
return s;
|
|
14
|
+
}
|
|
15
|
+
/** RFC 8785 canonical bytes (UTF-8) of a JSON value. */
|
|
16
|
+
export function jcsBytes(v) {
|
|
17
|
+
return utf8Encode(jcsText(v));
|
|
18
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export type JsonValue = null | boolean | number | string | JsonValue[] | JsonObject;
|
|
2
|
+
export interface JsonObject {
|
|
3
|
+
[key: string]: JsonValue;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Nesting deeper than this is rejected with E_PARSE_JSON. SPEC.md sets no depth limit; this is
|
|
7
|
+
* an implementation limit (Datumesh objects nest at most five levels) that keeps parsing and
|
|
8
|
+
* canonicalisation from exhausting the call stack.
|
|
9
|
+
*/
|
|
10
|
+
export declare const MAX_DEPTH = 1000;
|
|
11
|
+
export declare function isObject(v: unknown): v is JsonObject;
|
|
12
|
+
export declare function newObject(): JsonObject;
|
|
13
|
+
export declare function has(o: JsonObject, k: string): boolean;
|
|
14
|
+
/** Member names in JCS order (UTF-16 code units, which is JavaScript's default sort). */
|
|
15
|
+
export declare function sortedKeys(o: JsonObject): string[];
|
|
16
|
+
export declare function memberPath(path: string, name: string): string;
|
|
17
|
+
/** Parse JSON text strictly. */
|
|
18
|
+
export declare function parseJsonText(text: string): JsonValue;
|
|
19
|
+
/** Parse UTF-8 JSON bytes strictly: no BOM, no invalid UTF-8. */
|
|
20
|
+
export declare function parseJsonBytes(bytes: Uint8Array): JsonValue;
|
|
21
|
+
/**
|
|
22
|
+
* Check an already-parsed JavaScript value against the JSON data model and return a private
|
|
23
|
+
* deep copy (plain data with null-prototype objects), so later checks cannot be affected by
|
|
24
|
+
* getters, prototypes or mutation. Duplicate members cannot be detected here: pass JSON text
|
|
25
|
+
* or bytes for that.
|
|
26
|
+
*/
|
|
27
|
+
export declare function normalizeValue(v: unknown, path?: string, depth?: number): JsonValue;
|
|
28
|
+
/** JSON text (string), UTF-8 bytes (Uint8Array) or an already-parsed value. */
|
|
29
|
+
export declare function toJson(input: unknown): JsonValue;
|
package/dist/esm/json.js
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
// Strict JSON parsing (SPEC.md 6.1, I-JSON / RFC 7493). JSON.parse is not used: it accepts
|
|
2
|
+
// duplicate members (last wins) and lone surrogates, both of which must be rejected.
|
|
3
|
+
import { fail } from './errors.js';
|
|
4
|
+
import { hasLoneSurrogate } from './unicode.js';
|
|
5
|
+
import { utf8Decode } from './utf8.js';
|
|
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.
|
|
10
|
+
*/
|
|
11
|
+
export const MAX_DEPTH = 1000;
|
|
12
|
+
export function isObject(v) {
|
|
13
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
14
|
+
}
|
|
15
|
+
export function newObject() {
|
|
16
|
+
return Object.create(null);
|
|
17
|
+
}
|
|
18
|
+
export function has(o, k) {
|
|
19
|
+
return Object.prototype.hasOwnProperty.call(o, k);
|
|
20
|
+
}
|
|
21
|
+
/** Member names in JCS order (UTF-16 code units, which is JavaScript's default sort). */
|
|
22
|
+
export function sortedKeys(o) {
|
|
23
|
+
return Object.keys(o).sort();
|
|
24
|
+
}
|
|
25
|
+
export function memberPath(path, name) {
|
|
26
|
+
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(name) ? `${path}.${name}` : `${path}[${JSON.stringify(name)}]`;
|
|
27
|
+
}
|
|
28
|
+
class Parser {
|
|
29
|
+
constructor(s) {
|
|
30
|
+
this.s = s;
|
|
31
|
+
this.i = 0;
|
|
32
|
+
/** Path of the first string holding an escaped lone surrogate; reported after parsing. */
|
|
33
|
+
this.surrogatePath = null;
|
|
34
|
+
}
|
|
35
|
+
err(msg) {
|
|
36
|
+
fail('E_PARSE_JSON', `${msg} at offset ${this.i}`, '$');
|
|
37
|
+
}
|
|
38
|
+
ws() {
|
|
39
|
+
const s = this.s;
|
|
40
|
+
while (this.i < s.length) {
|
|
41
|
+
const c = s.charCodeAt(this.i);
|
|
42
|
+
if (c === 0x20 || c === 0x09 || c === 0x0a || c === 0x0d)
|
|
43
|
+
this.i++;
|
|
44
|
+
else
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
parseDocument() {
|
|
49
|
+
this.ws();
|
|
50
|
+
const v = this.value('$', 0);
|
|
51
|
+
this.ws();
|
|
52
|
+
if (this.i !== this.s.length)
|
|
53
|
+
this.err('unexpected data after the JSON value');
|
|
54
|
+
return v;
|
|
55
|
+
}
|
|
56
|
+
value(path, depth) {
|
|
57
|
+
if (depth > MAX_DEPTH)
|
|
58
|
+
this.err(`nesting deeper than ${MAX_DEPTH} levels (implementation limit)`);
|
|
59
|
+
const s = this.s;
|
|
60
|
+
const c = s[this.i];
|
|
61
|
+
if (c === '{')
|
|
62
|
+
return this.object(path, depth);
|
|
63
|
+
if (c === '[')
|
|
64
|
+
return this.array(path, depth);
|
|
65
|
+
if (c === '"')
|
|
66
|
+
return this.string(path);
|
|
67
|
+
if (c === 't' && s.startsWith('true', this.i)) {
|
|
68
|
+
this.i += 4;
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
if (c === 'f' && s.startsWith('false', this.i)) {
|
|
72
|
+
this.i += 5;
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
if (c === 'n' && s.startsWith('null', this.i)) {
|
|
76
|
+
this.i += 4;
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
if (c === '-' || (c >= '0' && c <= '9'))
|
|
80
|
+
return this.number();
|
|
81
|
+
if (c === undefined)
|
|
82
|
+
this.err('unexpected end of input');
|
|
83
|
+
if (s.startsWith('NaN', this.i) || s.startsWith('Infinity', this.i) || s.startsWith('-Infinity', this.i)) {
|
|
84
|
+
this.err('NaN and Infinity are not JSON');
|
|
85
|
+
}
|
|
86
|
+
this.err(`unexpected character ${JSON.stringify(c)}`);
|
|
87
|
+
}
|
|
88
|
+
object(path, depth) {
|
|
89
|
+
this.i++; // {
|
|
90
|
+
const o = newObject();
|
|
91
|
+
let dup = null;
|
|
92
|
+
this.ws();
|
|
93
|
+
if (this.s[this.i] === '}') {
|
|
94
|
+
this.i++;
|
|
95
|
+
return o;
|
|
96
|
+
}
|
|
97
|
+
for (;;) {
|
|
98
|
+
this.ws();
|
|
99
|
+
if (this.s[this.i] !== '"')
|
|
100
|
+
this.err('expected a member name');
|
|
101
|
+
const k = this.string(path);
|
|
102
|
+
this.ws();
|
|
103
|
+
if (this.s[this.i] !== ':')
|
|
104
|
+
this.err("expected ':'");
|
|
105
|
+
this.i++;
|
|
106
|
+
this.ws();
|
|
107
|
+
const v = this.value(memberPath(path, k), depth + 1);
|
|
108
|
+
if (has(o, k)) {
|
|
109
|
+
if (dup === null)
|
|
110
|
+
dup = k;
|
|
111
|
+
}
|
|
112
|
+
else
|
|
113
|
+
o[k] = v;
|
|
114
|
+
this.ws();
|
|
115
|
+
const c = this.s[this.i];
|
|
116
|
+
if (c === ',') {
|
|
117
|
+
this.i++;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (c === '}') {
|
|
121
|
+
this.i++;
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
this.err("expected ',' or '}'");
|
|
125
|
+
}
|
|
126
|
+
// Like an object-pairs hook, duplicates are reported once the object is complete.
|
|
127
|
+
if (dup !== null)
|
|
128
|
+
fail('E_PARSE_DUPLICATE_KEY', `duplicate member ${JSON.stringify(dup)}`, path);
|
|
129
|
+
return o;
|
|
130
|
+
}
|
|
131
|
+
array(path, depth) {
|
|
132
|
+
this.i++; // [
|
|
133
|
+
const a = [];
|
|
134
|
+
this.ws();
|
|
135
|
+
if (this.s[this.i] === ']') {
|
|
136
|
+
this.i++;
|
|
137
|
+
return a;
|
|
138
|
+
}
|
|
139
|
+
for (;;) {
|
|
140
|
+
this.ws();
|
|
141
|
+
a.push(this.value(`${path}[${a.length}]`, depth + 1));
|
|
142
|
+
this.ws();
|
|
143
|
+
const c = this.s[this.i];
|
|
144
|
+
if (c === ',') {
|
|
145
|
+
this.i++;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (c === ']') {
|
|
149
|
+
this.i++;
|
|
150
|
+
return a;
|
|
151
|
+
}
|
|
152
|
+
this.err("expected ',' or ']'");
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
string(path) {
|
|
156
|
+
const s = this.s;
|
|
157
|
+
this.i++; // opening quote
|
|
158
|
+
let out = '';
|
|
159
|
+
let start = this.i;
|
|
160
|
+
for (;;) {
|
|
161
|
+
if (this.i >= s.length)
|
|
162
|
+
this.err('unterminated string');
|
|
163
|
+
const c = s.charCodeAt(this.i);
|
|
164
|
+
if (c === 0x22) {
|
|
165
|
+
out += s.slice(start, this.i);
|
|
166
|
+
this.i++;
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
if (c < 0x20)
|
|
170
|
+
this.err('control character in string');
|
|
171
|
+
if (c === 0x5c) {
|
|
172
|
+
out += s.slice(start, this.i);
|
|
173
|
+
const e = s[this.i + 1];
|
|
174
|
+
let ch;
|
|
175
|
+
switch (e) {
|
|
176
|
+
case '"':
|
|
177
|
+
ch = '"';
|
|
178
|
+
break;
|
|
179
|
+
case '\\':
|
|
180
|
+
ch = '\\';
|
|
181
|
+
break;
|
|
182
|
+
case '/':
|
|
183
|
+
ch = '/';
|
|
184
|
+
break;
|
|
185
|
+
case 'b':
|
|
186
|
+
ch = '\b';
|
|
187
|
+
break;
|
|
188
|
+
case 'f':
|
|
189
|
+
ch = '\f';
|
|
190
|
+
break;
|
|
191
|
+
case 'n':
|
|
192
|
+
ch = '\n';
|
|
193
|
+
break;
|
|
194
|
+
case 'r':
|
|
195
|
+
ch = '\r';
|
|
196
|
+
break;
|
|
197
|
+
case 't':
|
|
198
|
+
ch = '\t';
|
|
199
|
+
break;
|
|
200
|
+
case 'u': {
|
|
201
|
+
const h = s.slice(this.i + 2, this.i + 6);
|
|
202
|
+
if (!/^[0-9A-Fa-f]{4}$/.test(h))
|
|
203
|
+
this.err('bad \\u escape');
|
|
204
|
+
ch = String.fromCharCode(parseInt(h, 16));
|
|
205
|
+
this.i += 4;
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
default:
|
|
209
|
+
this.err('bad escape');
|
|
210
|
+
}
|
|
211
|
+
out += ch;
|
|
212
|
+
this.i += 2;
|
|
213
|
+
start = this.i;
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
this.i++;
|
|
217
|
+
}
|
|
218
|
+
if (this.surrogatePath === null && hasLoneSurrogate(out))
|
|
219
|
+
this.surrogatePath = path;
|
|
220
|
+
return out;
|
|
221
|
+
}
|
|
222
|
+
number() {
|
|
223
|
+
const s = this.s;
|
|
224
|
+
const m = /-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?/y;
|
|
225
|
+
m.lastIndex = this.i;
|
|
226
|
+
const r = m.exec(s);
|
|
227
|
+
if (!r || r[0] === '-' || r[0] === '')
|
|
228
|
+
this.err('bad number');
|
|
229
|
+
const text = r[0];
|
|
230
|
+
// A digit right after "0" (e.g. 01) is not JSON.
|
|
231
|
+
const next = s[this.i + text.length];
|
|
232
|
+
if (next !== undefined && /[0-9.eE+-]/.test(next))
|
|
233
|
+
this.err('bad number');
|
|
234
|
+
this.i += text.length;
|
|
235
|
+
const n = Number(text);
|
|
236
|
+
if (!Number.isFinite(n))
|
|
237
|
+
fail('E_PARSE_JSON', `number out of IEEE 754 range: ${text.slice(0, 40)}`, '$');
|
|
238
|
+
return n;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
/** Parse JSON text strictly. */
|
|
242
|
+
export function parseJsonText(text) {
|
|
243
|
+
if (text.charCodeAt(0) === 0xfeff)
|
|
244
|
+
fail('E_PARSE_JSON', 'byte order mark', '$');
|
|
245
|
+
if (hasLoneSurrogate(text))
|
|
246
|
+
fail('E_PARSE_JSON', 'lone surrogate in the text', '$');
|
|
247
|
+
const p = new Parser(text);
|
|
248
|
+
const v = p.parseDocument();
|
|
249
|
+
if (p.surrogatePath !== null)
|
|
250
|
+
fail('E_PARSE_JSON', 'lone surrogate', p.surrogatePath);
|
|
251
|
+
return v;
|
|
252
|
+
}
|
|
253
|
+
/** Parse UTF-8 JSON bytes strictly: no BOM, no invalid UTF-8. */
|
|
254
|
+
export function parseJsonBytes(bytes) {
|
|
255
|
+
if (bytes.length >= 3 && bytes[0] === 0xef && bytes[1] === 0xbb && bytes[2] === 0xbf) {
|
|
256
|
+
fail('E_PARSE_JSON', 'byte order mark', '$');
|
|
257
|
+
}
|
|
258
|
+
const text = utf8Decode(bytes);
|
|
259
|
+
if (text === null)
|
|
260
|
+
fail('E_PARSE_JSON', 'invalid UTF-8', '$');
|
|
261
|
+
return parseJsonText(text);
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Check an already-parsed JavaScript value against the JSON data model and return a private
|
|
265
|
+
* deep copy (plain data with null-prototype objects), so later checks cannot be affected by
|
|
266
|
+
* getters, prototypes or mutation. Duplicate members cannot be detected here: pass JSON text
|
|
267
|
+
* or bytes for that.
|
|
268
|
+
*/
|
|
269
|
+
export function normalizeValue(v, path = '$', depth = 0) {
|
|
270
|
+
if (depth > MAX_DEPTH)
|
|
271
|
+
fail('E_PARSE_JSON', `nesting deeper than ${MAX_DEPTH} levels (or a cycle)`, path);
|
|
272
|
+
if (v === null || typeof v === 'boolean')
|
|
273
|
+
return v;
|
|
274
|
+
if (typeof v === 'number') {
|
|
275
|
+
if (!Number.isFinite(v))
|
|
276
|
+
fail('E_PARSE_JSON', 'NaN and Infinity are not JSON', path);
|
|
277
|
+
return v;
|
|
278
|
+
}
|
|
279
|
+
if (typeof v === 'string') {
|
|
280
|
+
if (hasLoneSurrogate(v))
|
|
281
|
+
fail('E_PARSE_JSON', 'lone surrogate', path);
|
|
282
|
+
return v;
|
|
283
|
+
}
|
|
284
|
+
if (Array.isArray(v))
|
|
285
|
+
return v.map((x, i) => normalizeValue(x, `${path}[${i}]`, depth + 1));
|
|
286
|
+
if (typeof v === 'object') {
|
|
287
|
+
const proto = Object.getPrototypeOf(v);
|
|
288
|
+
if (proto !== Object.prototype && proto !== null)
|
|
289
|
+
fail('E_PARSE_JSON', 'not a plain JSON object', path);
|
|
290
|
+
const o = newObject();
|
|
291
|
+
for (const k of Object.keys(v)) {
|
|
292
|
+
if (hasLoneSurrogate(k))
|
|
293
|
+
fail('E_PARSE_JSON', 'lone surrogate in a member name', path);
|
|
294
|
+
o[k] = normalizeValue(v[k], memberPath(path, k), depth + 1);
|
|
295
|
+
}
|
|
296
|
+
return o;
|
|
297
|
+
}
|
|
298
|
+
fail('E_PARSE_JSON', `${typeof v} is not a JSON value`, path);
|
|
299
|
+
}
|
|
300
|
+
/** JSON text (string), UTF-8 bytes (Uint8Array) or an already-parsed value. */
|
|
301
|
+
export function toJson(input) {
|
|
302
|
+
if (typeof input === 'string')
|
|
303
|
+
return parseJsonText(input);
|
|
304
|
+
if (input instanceof Uint8Array)
|
|
305
|
+
return parseJsonBytes(input);
|
|
306
|
+
if (input instanceof ArrayBuffer)
|
|
307
|
+
return parseJsonBytes(new Uint8Array(input));
|
|
308
|
+
return normalizeValue(input);
|
|
309
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { type ErrorCode } from './errors.js';
|
|
2
|
+
/** One validated key from a key document. */
|
|
3
|
+
export interface KeyEntry {
|
|
4
|
+
alg: 'ed25519';
|
|
5
|
+
id: string;
|
|
6
|
+
public: string;
|
|
7
|
+
valid_from: string;
|
|
8
|
+
valid_to?: string;
|
|
9
|
+
}
|
|
10
|
+
/** The outcome of resolving one host's keys (SPEC 9.3). */
|
|
11
|
+
export interface KeysResult {
|
|
12
|
+
ok: boolean;
|
|
13
|
+
/** "OK", "E_KEY_UNAVAILABLE" or "E_KEY_DOC". */
|
|
14
|
+
code: 'OK' | 'E_KEY_UNAVAILABLE' | 'E_KEY_DOC';
|
|
15
|
+
host: string;
|
|
16
|
+
detail: string;
|
|
17
|
+
/** The validated keys, in document order, when ok. */
|
|
18
|
+
keys?: KeyEntry[];
|
|
19
|
+
}
|
|
20
|
+
/** A mapping of key id to 64-hex public key: usable for any host, with no validity windows. */
|
|
21
|
+
export type KeyMap = Record<string, string>;
|
|
22
|
+
/** Anything `verify` accepts as keys. */
|
|
23
|
+
export type KeySource = KeysResult | KeyMap | Record<string, unknown> | KeySource[];
|
|
24
|
+
/**
|
|
25
|
+
* Interpret the HTTPS response for https://<host>/.well-known/datumesh.json (SPEC 9.3).
|
|
26
|
+
* `status` is the HTTP status, or null for a network, DNS, TLS or timeout failure. `body` is the
|
|
27
|
+
* response body as text or bytes. Never throws.
|
|
28
|
+
*/
|
|
29
|
+
export declare function keysFromResponse(host: string, status: number | null, body?: string | Uint8Array | null): KeysResult;
|
|
30
|
+
/**
|
|
31
|
+
* Validate an already-parsed key document (its `host` member names the host it covers).
|
|
32
|
+
* Never throws.
|
|
33
|
+
*/
|
|
34
|
+
export declare function keysFromDocument(doc: unknown): KeysResult;
|
|
35
|
+
/** Default timeout of fetchKeys, in milliseconds. */
|
|
36
|
+
export declare const FETCH_TIMEOUT_MS = 10000;
|
|
37
|
+
/**
|
|
38
|
+
* Fetch https://<host>/.well-known/datumesh.json once, with TLS verified by the platform, no
|
|
39
|
+
* redirects followed and a timeout, then interpret it with keysFromResponse. The only function
|
|
40
|
+
* in this library that touches the network. Never throws.
|
|
41
|
+
*/
|
|
42
|
+
export declare function fetchKeys(host: string, timeoutMs?: number): Promise<KeysResult>;
|
|
43
|
+
interface HostSource {
|
|
44
|
+
host: string;
|
|
45
|
+
result: KeysResult;
|
|
46
|
+
}
|
|
47
|
+
interface AnySource {
|
|
48
|
+
ok: boolean;
|
|
49
|
+
code: ErrorCode | 'OK';
|
|
50
|
+
detail: string;
|
|
51
|
+
keys: KeyEntry[];
|
|
52
|
+
}
|
|
53
|
+
export interface Sources {
|
|
54
|
+
hosts: HostSource[];
|
|
55
|
+
any: AnySource[];
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Normalise the `keys` argument of verify. Throws TypeError only for arguments that are not key
|
|
59
|
+
* sources at all (programmer errors); malformed key material becomes an error source.
|
|
60
|
+
*/
|
|
61
|
+
export declare function collectSources(keys: unknown, into?: Sources, depth?: number): Sources;
|
|
62
|
+
export type KeyLookup = {
|
|
63
|
+
ok: true;
|
|
64
|
+
key: KeyEntry;
|
|
65
|
+
windows: boolean;
|
|
66
|
+
} | {
|
|
67
|
+
ok: false;
|
|
68
|
+
code: ErrorCode;
|
|
69
|
+
detail: string;
|
|
70
|
+
};
|
|
71
|
+
/** SPEC 10.1 step 5 for key reference host#id at time `issued`. */
|
|
72
|
+
export declare function lookupKey(sources: Sources, host: string, id: string, issued: string): KeyLookup;
|
|
73
|
+
export {};
|