@cygnuxlabs/tracekit 0.0.0-stage → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +294 -2
  2. package/bin/tracekit.mjs +30 -0
  3. package/dist/v1.d.ts +97 -0
  4. package/dist/v1.js +535 -0
  5. package/dist/v2/adapters/claude-agent-sdk.d.ts +45 -0
  6. package/dist/v2/adapters/claude-agent-sdk.js +142 -0
  7. package/dist/v2/adapters/gate.d.ts +62 -0
  8. package/dist/v2/adapters/gate.js +195 -0
  9. package/dist/v2/adapters/openai-agents.d.ts +85 -0
  10. package/dist/v2/adapters/openai-agents.js +114 -0
  11. package/dist/v2/adapters/vercel-ai.d.ts +51 -0
  12. package/dist/v2/adapters/vercel-ai.js +132 -0
  13. package/dist/v2/client.d.ts +159 -0
  14. package/dist/v2/client.js +749 -0
  15. package/dist/v2/executor.d.ts +49 -0
  16. package/dist/v2/executor.js +57 -0
  17. package/dist/v2/jcs.d.ts +5 -0
  18. package/dist/v2/jcs.js +12 -0
  19. package/dist/v2/launcher.d.ts +10 -0
  20. package/dist/v2/launcher.js +50 -0
  21. package/dist/v2/rpc_schema.d.ts +3 -0
  22. package/dist/v2/rpc_schema.js +43 -0
  23. package/dist/v2/validate.d.ts +6 -0
  24. package/dist/v2/validate.js +70 -0
  25. package/dist/verify/cli.d.ts +1 -0
  26. package/dist/verify/cli.js +46 -0
  27. package/dist/verify/detect.d.ts +41 -0
  28. package/dist/verify/detect.js +289 -0
  29. package/dist/verify/index.d.ts +34 -0
  30. package/dist/verify/index.js +930 -0
  31. package/dist/verify/jcs.d.ts +19 -0
  32. package/dist/verify/jcs.js +154 -0
  33. package/dist/verify/primitives.d.ts +62 -0
  34. package/dist/verify/primitives.js +298 -0
  35. package/dist/verify/schema.d.ts +11 -0
  36. package/dist/verify/schema.js +98 -0
  37. package/dist/verify/tracekit.event.v2.json +4588 -0
  38. package/dist/verify/zip.d.ts +17 -0
  39. package/dist/verify/zip.js +145 -0
  40. package/package.json +42 -4
@@ -0,0 +1,19 @@
1
+ /**
2
+ * JCS (RFC 8785) and the strict parser of docs/format-v2.md §1, byte for byte (tests/vectors/jcs.jsonl).
3
+ * Strict: bytes that are not UTF-8, duplicate keys, NaN/±Infinity (and overflow such as 1e400), lone surrogates and
4
+ * integer tokens (no `.` or exponent) outside ±(2^53-1) are refused, and so are arrays and objects nested deeper than
5
+ * MAX_DEPTH. No node: imports: browsers load this too.
6
+ */
7
+ /** `code` is one of: invalid_utf8, syntax, duplicate_key, non_finite, int_range, lone_surrogate, depth. */
8
+ export declare class StrictJSONError extends Error {
9
+ readonly code: string;
10
+ constructor(code: string, message: string);
11
+ }
12
+ export declare const MAX_DEPTH = 128;
13
+ /** Where number tokens with a fraction or exponent sit (container -> keys): JSON Schema's "integer" excludes them. */
14
+ export type Floats = WeakMap<object, Set<string | number>>;
15
+ export declare function strictParse(text: string, floats?: Floats): unknown;
16
+ /** JCS text of a JSON value; StrictJSONError for anything JCS can't encode. */
17
+ export declare function canonicalize(v: unknown): string;
18
+ /** strictParse of UTF-8 bytes (a byte order mark is not whitespace). */
19
+ export declare function strictParseBytes(data: Uint8Array, floats?: Floats): unknown;
@@ -0,0 +1,154 @@
1
+ /**
2
+ * JCS (RFC 8785) and the strict parser of docs/format-v2.md §1, byte for byte (tests/vectors/jcs.jsonl).
3
+ * Strict: bytes that are not UTF-8, duplicate keys, NaN/±Infinity (and overflow such as 1e400), lone surrogates and
4
+ * integer tokens (no `.` or exponent) outside ±(2^53-1) are refused, and so are arrays and objects nested deeper than
5
+ * MAX_DEPTH. No node: imports: browsers load this too.
6
+ */
7
+ /** `code` is one of: invalid_utf8, syntax, duplicate_key, non_finite, int_range, lone_surrogate, depth. */
8
+ export class StrictJSONError extends Error {
9
+ code;
10
+ constructor(code, message) {
11
+ super(`${code}: ${message}`);
12
+ this.code = code;
13
+ }
14
+ }
15
+ const WS = /[ \t\n\r]*/y;
16
+ const ESC = /\\(?:["\\/bfnrt]|u[0-9a-fA-F]{4})/y;
17
+ const NUM = /-?(?:0|[1-9]\d*)(\.\d+)?([eE][+-]?\d+)?/y;
18
+ export const MAX_DEPTH = 128;
19
+ const LONE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/;
20
+ export function strictParse(text, floats) {
21
+ let i = 0;
22
+ let float = false; // the last value read was a number token with a fraction or exponent
23
+ const mark = (o, k) => {
24
+ if (!float || !floats)
25
+ return;
26
+ if (!floats.has(o))
27
+ floats.set(o, new Set());
28
+ floats.get(o).add(k);
29
+ };
30
+ const fail = (code, msg) => { throw new StrictJSONError(code, msg); };
31
+ const match = (re) => {
32
+ re.lastIndex = i;
33
+ const m = re.exec(text);
34
+ if (m)
35
+ i = re.lastIndex;
36
+ return m;
37
+ };
38
+ const str = () => {
39
+ const start = i;
40
+ if (text[i++] !== '"')
41
+ fail("syntax", `string expected at ${start}`);
42
+ for (let c; (c = text.charCodeAt(i)) !== 0x22;) {
43
+ if (c === 0x5c)
44
+ match(ESC) ?? fail("syntax", `bad escape at ${i}`);
45
+ else if (c < 0x20 || Number.isNaN(c))
46
+ fail("syntax", `unterminated string at ${start}`);
47
+ else
48
+ i++;
49
+ }
50
+ const raw = text.slice(start, ++i), s = JSON.parse(raw);
51
+ if (LONE.test(s))
52
+ fail("lone_surrogate", raw.slice(0, 80));
53
+ return s;
54
+ };
55
+ const expect = (c) => {
56
+ match(WS);
57
+ if (text[i++] !== c)
58
+ fail("syntax", `${c} expected at ${i - 1}`);
59
+ };
60
+ const value = (depth) => {
61
+ float = false;
62
+ match(WS);
63
+ const c = text[i];
64
+ if ((c === "{" || c === "[") && depth === MAX_DEPTH)
65
+ fail("depth", `nesting deeper than ${MAX_DEPTH} at ${i}`);
66
+ if (c === "{") {
67
+ i++;
68
+ const o = {};
69
+ match(WS);
70
+ if (text[i] === "}")
71
+ return i++, o;
72
+ do {
73
+ match(WS);
74
+ const k = str();
75
+ if (Object.hasOwn(o, k))
76
+ fail("duplicate_key", JSON.stringify(k));
77
+ expect(":");
78
+ Object.defineProperty(o, k, { value: value(depth + 1), enumerable: true, writable: true, configurable: true }); // "__proto__" too
79
+ mark(o, k);
80
+ match(WS);
81
+ } while (text[i] === "," && ++i);
82
+ expect("}");
83
+ float = false;
84
+ return o;
85
+ }
86
+ if (c === "[") {
87
+ i++;
88
+ const a = [];
89
+ match(WS);
90
+ if (text[i] === "]")
91
+ return i++, a;
92
+ do
93
+ a.push(value(depth + 1)), mark(a, a.length - 1);
94
+ while ((match(WS), text[i] === ",") && ++i);
95
+ expect("]");
96
+ float = false;
97
+ return a;
98
+ }
99
+ if (c === '"')
100
+ return str();
101
+ for (const [lit, v] of [["true", true], ["false", false], ["null", null]])
102
+ if (text.startsWith(lit, i))
103
+ return (i += lit.length), v;
104
+ for (const lit of ["NaN", "Infinity", "-Infinity"])
105
+ if (text.startsWith(lit, i))
106
+ fail("non_finite", lit);
107
+ const m = match(NUM) ?? fail("syntax", `value expected at ${i}`);
108
+ const n = Number(m[0]);
109
+ if (!Number.isFinite(n))
110
+ fail("non_finite", m[0]);
111
+ if (!m[1] && !m[2] && !Number.isSafeInteger(n))
112
+ fail("int_range", m[0]);
113
+ float = !!(m[1] || m[2]);
114
+ return n;
115
+ };
116
+ const v = value(0);
117
+ match(WS);
118
+ if (i < text.length)
119
+ fail("syntax", `extra data at ${i}`);
120
+ return v;
121
+ }
122
+ /** JCS text of a JSON value; StrictJSONError for anything JCS can't encode. */
123
+ export function canonicalize(v) {
124
+ if (typeof v === "string") {
125
+ if (LONE.test(v))
126
+ throw new StrictJSONError("lone_surrogate", JSON.stringify(v));
127
+ return JSON.stringify(v);
128
+ }
129
+ if (typeof v === "number") {
130
+ if (!Number.isFinite(v))
131
+ throw new StrictJSONError("non_finite", String(v));
132
+ return JSON.stringify(v); // ECMAScript Number::toString, -0 as 0: what JCS specifies
133
+ }
134
+ if (v === null || typeof v === "boolean")
135
+ return String(v);
136
+ if (Array.isArray(v))
137
+ return `[${v.map(canonicalize).join(",")}]`;
138
+ if (typeof v === "object") {
139
+ const o = v;
140
+ return `{${Object.keys(o).sort().map((k) => `${canonicalize(k)}:${canonicalize(o[k])}`).join(",")}}`; // UTF-16 order
141
+ }
142
+ throw new StrictJSONError("syntax", `${typeof v} is not JSON`);
143
+ }
144
+ /** strictParse of UTF-8 bytes (a byte order mark is not whitespace). */
145
+ export function strictParseBytes(data, floats) {
146
+ let text;
147
+ try {
148
+ text = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(data);
149
+ }
150
+ catch {
151
+ throw new StrictJSONError("invalid_utf8", "not UTF-8");
152
+ }
153
+ return strictParse(text, floats);
154
+ }
@@ -0,0 +1,62 @@
1
+ export declare const bytes: (s: string) => Uint8Array<ArrayBuffer>;
2
+ export declare const concat: (...parts: Uint8Array[]) => Uint8Array<ArrayBuffer>;
3
+ export declare const equal: (a: Uint8Array, b: Uint8Array) => boolean;
4
+ export declare const hex: (b: Uint8Array) => string;
5
+ export declare function unhex(s: string): Uint8Array;
6
+ export declare function sha256(...parts: Uint8Array[]): Promise<Uint8Array>;
7
+ export declare const sha256hex: (data: Uint8Array) => Promise<string>;
8
+ export declare function b64encode(data: Uint8Array): string;
9
+ /** Standard base64: only the alphabet, then at most two `=`, padded to whole quads (padding after a whole quad is
10
+ * tolerated); bits past the last byte are ignored. */
11
+ export declare function b64decode(s: unknown): Uint8Array;
12
+ /** Canonical base64 only (checkpoint notes and vkeys). */
13
+ export declare function unb64(s: string): Uint8Array;
14
+ export declare function ed25519Verify(pub: Uint8Array, msg: Uint8Array, sig: Uint8Array): Promise<boolean>;
15
+ /** The algorithm of a DER SubjectPublicKeyInfo, or null when it is not one this format defines. */
16
+ export declare const keyAlg: (spki: Uint8Array) => "ed25519" | null;
17
+ export declare const spkiKid: (spki: Uint8Array) => Promise<string>;
18
+ export declare const spkiOf: (pub: Uint8Array) => Uint8Array<ArrayBuffer>;
19
+ /** §4: false unless `alg` is the key's own algorithm and the signature holds under its v2 rules. */
20
+ export declare const verifyV2: (alg: string, spki: Uint8Array, msg: Uint8Array, sig: Uint8Array) => Promise<boolean>;
21
+ export declare const leafHash: (data: Uint8Array) => Promise<Uint8Array<ArrayBufferLike>>;
22
+ /** RFC 9162 §2.1.3.2. */
23
+ export declare function verifyInclusion(index: number, size: number, leaf: Uint8Array, proof: Uint8Array[], root: Uint8Array | undefined): Promise<boolean>;
24
+ /** RFC 9162 §2.1.4.2. */
25
+ export declare function verifyConsistency(first: number, second: number, firstRoot: Uint8Array, secondRoot: Uint8Array, proof: Uint8Array[]): Promise<boolean>;
26
+ export declare const ED25519 = 1, COSIGNATURE = 4, HYBRID = 255;
27
+ export declare const SLH_DSA: Uint8Array<ArrayBuffer>;
28
+ export declare class NoteError extends Error {
29
+ }
30
+ export declare const keyId: (name: string, type: number, pub: Uint8Array) => Promise<Uint8Array<ArrayBufferLike>>;
31
+ export declare function vkey(name: string, type: number, pub: Uint8Array): Promise<string>;
32
+ export type VKey = {
33
+ name: string;
34
+ id: Uint8Array;
35
+ type: number;
36
+ pub: Uint8Array;
37
+ };
38
+ /** (name, key id, type, public key) of a vkey `name+hex id+base64(type ‖ key)`. */
39
+ export declare function parseVkey(text: string): Promise<VKey>;
40
+ export type Note = {
41
+ origin: string;
42
+ size: number;
43
+ root: Uint8Array;
44
+ cosigs: [string, bigint][];
45
+ unchecked: boolean;
46
+ };
47
+ /**
48
+ * Open a checkpoint note with pinned vkeys: a pinned Ed25519 log key named after the origin must sign it, and each
49
+ * pinned hybrid key of the origin must have a line. Web Crypto has no SLH-DSA, so a hybrid line is left `unchecked`.
50
+ */
51
+ export declare function openNote(data: Uint8Array, logKeys: string[], witnessKeys: string[]): Promise<Note>;
52
+ export declare const LEAF_TYPES: string[];
53
+ export declare const SIGNER_LEAVES: string[];
54
+ export declare const registryOrigin: (logOrigin: string, tsalt: Uint8Array) => Promise<string>;
55
+ export declare const runHash: (tsalt: Uint8Array, runId: string) => Promise<Uint8Array<ArrayBufferLike>>;
56
+ export declare function parseLeaf(data: Uint8Array): {
57
+ type: string;
58
+ runHash: Uint8Array<ArrayBufferLike>;
59
+ logId: string;
60
+ seq: number;
61
+ hash: string;
62
+ };
@@ -0,0 +1,298 @@
1
+ /**
2
+ * Hashes, base64, strict Ed25519 (docs/format-v2.md §4), RFC 9162 proofs (§5), C2SP checkpoint notes (§6) and registry
3
+ * leaves (§7), on Web Crypto only.
4
+ */
5
+ const subtle = globalThis.crypto.subtle;
6
+ const utf8 = new TextEncoder();
7
+ export const bytes = (s) => utf8.encode(s);
8
+ export const concat = (...parts) => {
9
+ const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
10
+ let at = 0;
11
+ for (const p of parts)
12
+ out.set(p, (at += p.length) - p.length);
13
+ return out;
14
+ };
15
+ export const equal = (a, b) => a.length === b.length && a.every((x, i) => x === b[i]);
16
+ export const hex = (b) => Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
17
+ export function unhex(s) {
18
+ if (!/^(?:[0-9a-fA-F]{2})*$/.test(s))
19
+ throw new Error(`not hex: ${s.slice(0, 80)}`);
20
+ return Uint8Array.from(s.match(/../g) ?? [], (x) => parseInt(x, 16));
21
+ }
22
+ export async function sha256(...parts) {
23
+ return new Uint8Array(await subtle.digest("SHA-256", concat(...parts)));
24
+ }
25
+ export const sha256hex = async (data) => hex(await sha256(data));
26
+ const B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
27
+ export function b64encode(data) {
28
+ let s = "";
29
+ for (let i = 0; i < data.length; i += 3) {
30
+ const n = (data[i] << 16) | ((data[i + 1] ?? 0) << 8) | (data[i + 2] ?? 0);
31
+ s += B64[n >> 18] + B64[(n >> 12) & 63] + (i + 1 < data.length ? B64[(n >> 6) & 63] : "=")
32
+ + (i + 2 < data.length ? B64[n & 63] : "=");
33
+ }
34
+ return s;
35
+ }
36
+ /** Standard base64: only the alphabet, then at most two `=`, padded to whole quads (padding after a whole quad is
37
+ * tolerated); bits past the last byte are ignored. */
38
+ export function b64decode(s) {
39
+ const m = typeof s === "string" ? /^([A-Za-z0-9+/]*)(={0,2})$/.exec(s) : null;
40
+ const r = m ? m[1].length % 4 : 1;
41
+ if (!m || r === 1 || (r && m[1].length % 4 + m[2].length < 4))
42
+ throw new Error("bad base64");
43
+ const out = [];
44
+ let acc = 0, bits = 0;
45
+ for (const c of m[1]) {
46
+ acc = (acc << 6) | B64.indexOf(c);
47
+ if ((bits += 6) >= 8)
48
+ out.push((acc >> (bits -= 8)) & 255);
49
+ }
50
+ return Uint8Array.from(out);
51
+ }
52
+ /** Canonical base64 only (checkpoint notes and vkeys). */
53
+ export function unb64(s) {
54
+ const raw = b64decode(s);
55
+ if (b64encode(raw) !== s)
56
+ throw new Error("non-canonical base64");
57
+ return raw;
58
+ }
59
+ // Ed25519: S < L, a canonical public key (y < p) that is not of small order, then Web Crypto's cofactorless check
60
+ const L = (1n << 252n) + 27742317777372353535851937790883648493n;
61
+ const P = (1n << 255n) - 19n;
62
+ const SMALL_ORDER = new Set([
63
+ "0100000000000000000000000000000000000000000000000000000000000000",
64
+ "ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f",
65
+ "0000000000000000000000000000000000000000000000000000000000000000",
66
+ "0000000000000000000000000000000000000000000000000000000000000080",
67
+ "26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc05",
68
+ "26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc85",
69
+ "c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac037a",
70
+ "c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac03fa",
71
+ ]);
72
+ const le = (b) => b.reduceRight((n, x) => (n << 8n) | BigInt(x), 0n);
73
+ export async function ed25519Verify(pub, msg, sig) {
74
+ if (pub.length !== 32 || sig.length !== 64 || le(sig.subarray(32)) >= L)
75
+ return false;
76
+ const y = le(pub) & ((1n << 255n) - 1n);
77
+ // x = 0 with the sign bit set is no encoding at all; y = ±1 are the small-order points with x = 0
78
+ if (y >= P || SMALL_ORDER.has(hex(pub)) || (pub[31] & 0x80 && (y === 1n || y === P - 1n)))
79
+ return false;
80
+ try {
81
+ const key = await subtle.importKey("raw", pub, { name: "Ed25519" }, false, ["verify"]);
82
+ return await subtle.verify({ name: "Ed25519" }, key, sig, msg);
83
+ }
84
+ catch {
85
+ return false; // not a point on the curve
86
+ }
87
+ }
88
+ const ED25519_SPKI = unhex("302a300506032b6570032100");
89
+ /** The algorithm of a DER SubjectPublicKeyInfo, or null when it is not one this format defines. */
90
+ export const keyAlg = (spki) => spki.length === 44 && equal(spki.subarray(0, 12), ED25519_SPKI) ? "ed25519" : null;
91
+ export const spkiKid = async (spki) => "sha256:" + await sha256hex(spki);
92
+ export const spkiOf = (pub) => concat(ED25519_SPKI, pub);
93
+ /** §4: false unless `alg` is the key's own algorithm and the signature holds under its v2 rules. */
94
+ export const verifyV2 = async (alg, spki, msg, sig) => keyAlg(spki) === alg && ed25519Verify(spki.subarray(12), msg, sig);
95
+ // RFC 9162 Merkle trees; sizes are below 2^53, so halving is exact in a Number
96
+ export const leafHash = (data) => sha256(Uint8Array.of(0), data);
97
+ const nodeHash = (l, r) => sha256(Uint8Array.of(1), l, r);
98
+ const odd = (n) => n % 2 === 1;
99
+ const half = (n) => Math.floor(n / 2);
100
+ /** RFC 9162 §2.1.3.2. */
101
+ export async function verifyInclusion(index, size, leaf, proof, root) {
102
+ if (!(index >= 0 && index < size) || !root)
103
+ return false;
104
+ let fn = index, sn = size - 1, r = leaf;
105
+ for (const p of proof) {
106
+ if (sn === 0)
107
+ return false;
108
+ if (odd(fn) || fn === sn) {
109
+ r = await nodeHash(p, r);
110
+ if (!odd(fn))
111
+ while (fn && !odd(fn))
112
+ fn = half(fn), sn = half(sn);
113
+ }
114
+ else
115
+ r = await nodeHash(r, p);
116
+ fn = half(fn);
117
+ sn = half(sn);
118
+ }
119
+ return sn === 0 && equal(r, root);
120
+ }
121
+ /** RFC 9162 §2.1.4.2. */
122
+ export async function verifyConsistency(first, second, firstRoot, secondRoot, proof) {
123
+ if (first === 0 || first > second)
124
+ return false;
125
+ if (first === second)
126
+ return !proof.length && equal(firstRoot, secondRoot);
127
+ let pow = first;
128
+ while (!odd(pow))
129
+ pow = half(pow);
130
+ const path = pow === 1 ? [firstRoot, ...proof] : proof; // a power of two: its root is the first proof node
131
+ if (!path.length)
132
+ return false;
133
+ let fn = first - 1, sn = second - 1;
134
+ while (odd(fn))
135
+ fn = half(fn), sn = half(sn);
136
+ let fr = path[0], sr = path[0];
137
+ for (const c of path.slice(1)) {
138
+ if (sn === 0)
139
+ return false;
140
+ if (odd(fn) || fn === sn) {
141
+ fr = await nodeHash(c, fr);
142
+ sr = await nodeHash(c, sr);
143
+ if (!odd(fn))
144
+ while (fn && !odd(fn))
145
+ fn = half(fn), sn = half(sn);
146
+ }
147
+ else
148
+ sr = await nodeHash(sr, c);
149
+ fn = half(fn);
150
+ sn = half(sn);
151
+ }
152
+ return sn === 0 && equal(fr, firstRoot) && equal(sr, secondRoot);
153
+ }
154
+ // C2SP signed notes: tlog-checkpoint, the log's type 0x01 line, witnesses' type 0x04 cosignatures, the hybrid 0xff line
155
+ export const ED25519 = 0x01, COSIGNATURE = 0x04, HYBRID = 0xff;
156
+ export const SLH_DSA = bytes("tracekit/slh-dsa-sha2-128s");
157
+ const MAX_NOTE = 1 << 20;
158
+ const DASH = "— ";
159
+ // Unicode whitespace as str.isspace sees it, besides the control characters a name can't hold anyway
160
+ const SPACE = /[\x1c-\x20\x85\xa0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000]/;
161
+ export class NoteError extends Error {
162
+ }
163
+ function checkName(name) {
164
+ if (!name || name.includes("+") || bytes(name).length > 255 || SPACE.test(name) || /[\x00-\x1f]/.test(name))
165
+ throw new NoteError(`bad key name ${JSON.stringify(name.slice(0, 64))}`);
166
+ }
167
+ export const keyId = async (name, type, pub) => (await sha256(bytes(name), Uint8Array.of(0x0a, type), pub)).subarray(0, 4);
168
+ export async function vkey(name, type, pub) {
169
+ checkName(name);
170
+ return `${name}+${hex(await keyId(name, type, pub))}+${b64encode(concat(Uint8Array.of(type), pub))}`;
171
+ }
172
+ /** (name, key id, type, public key) of a vkey `name+hex id+base64(type ‖ key)`. */
173
+ export async function parseVkey(text) {
174
+ const i = text.indexOf("+"), j = text.indexOf("+", i + 1);
175
+ if (i < 0 || j < 0)
176
+ throw new NoteError("a vkey is name+id+key");
177
+ const name = text.slice(0, i), hid = text.slice(i + 1, j);
178
+ checkName(name);
179
+ let raw;
180
+ try {
181
+ raw = unb64(text.slice(j + 1));
182
+ }
183
+ catch (e) {
184
+ throw new NoteError(e.message);
185
+ }
186
+ if (!(raw.length === 33 && (raw[0] === ED25519 || raw[0] === COSIGNATURE)
187
+ || raw.length === 1 + SLH_DSA.length + 32 && raw[0] === HYBRID && equal(raw.subarray(1, 1 + SLH_DSA.length), SLH_DSA)))
188
+ throw new NoteError("not an Ed25519 log, cosignature or SLH-DSA log vkey");
189
+ const id = await keyId(name, raw[0], raw.subarray(1));
190
+ if (hid !== hex(id))
191
+ throw new NoteError("vkey id does not match the key");
192
+ return { name, id, type: raw[0], pub: raw.subarray(1) };
193
+ }
194
+ /**
195
+ * Open a checkpoint note with pinned vkeys: a pinned Ed25519 log key named after the origin must sign it, and each
196
+ * pinned hybrid key of the origin must have a line. Web Crypto has no SLH-DSA, so a hybrid line is left `unchecked`.
197
+ */
198
+ export async function openNote(data, logKeys, witnessKeys) {
199
+ if (data.length > MAX_NOTE)
200
+ throw new NoteError("note too large");
201
+ let note;
202
+ try {
203
+ note = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(data);
204
+ }
205
+ catch {
206
+ throw new NoteError("note is not UTF-8");
207
+ }
208
+ if (!note.endsWith("\n") || /[\x00-\x09\x0b-\x1f]/.test(note))
209
+ throw new NoteError("malformed note");
210
+ const i = note.indexOf("\n\n");
211
+ if (i < 0)
212
+ throw new NoteError("no signature block");
213
+ const text = note.slice(0, i + 1), lines = note.slice(i + 2, -1).split("\n");
214
+ if (lines.length > 64)
215
+ throw new NoteError("too many signatures");
216
+ const head = text.slice(0, -1).split("\n");
217
+ if (head.length !== 3)
218
+ throw new NoteError("a checkpoint is origin, size and root");
219
+ const [origin, size] = head;
220
+ checkName(origin);
221
+ if (!/^(0|[1-9][0-9]{0,19})$/.test(size))
222
+ throw new NoteError("bad tree size");
223
+ let root;
224
+ try {
225
+ root = unb64(head[2]);
226
+ }
227
+ catch (e) {
228
+ throw new NoteError(e.message);
229
+ }
230
+ if (root.length !== 32)
231
+ throw new NoteError("bad root hash");
232
+ const pinned = new Map();
233
+ for (const [keys, want] of [[logKeys, [ED25519, HYBRID]], [witnessKeys, [COSIGNATURE]]])
234
+ for (const k of keys) {
235
+ const p = await parseVkey(k);
236
+ if (!want.includes(p.type))
237
+ throw new NoteError(`pinned key ${p.name} has the wrong signature type`);
238
+ pinned.set(`${p.name}\n${hex(p.id)}`, [k, p]);
239
+ }
240
+ const hybrids = new Set([...pinned].filter(([, [, p]]) => p.type === HYBRID && p.name === origin).map(([id]) => id));
241
+ const seen = new Set(), cosigs = [], msg = bytes(text);
242
+ let logged = false, unchecked = false;
243
+ for (const line of lines) {
244
+ const parts = line.startsWith(DASH) ? line.slice(DASH.length).split(" ") : [];
245
+ if (parts.length !== 2)
246
+ throw new NoteError("malformed signature line");
247
+ const [name, blob] = parts;
248
+ checkName(name);
249
+ let raw;
250
+ try {
251
+ raw = unb64(blob);
252
+ }
253
+ catch (e) {
254
+ throw new NoteError(e.message);
255
+ }
256
+ if (raw.length < 5)
257
+ throw new NoteError("short signature");
258
+ const id = `${name}\n${hex(raw.subarray(0, 4))}`;
259
+ if (seen.has(id))
260
+ throw new NoteError("two signatures from one key");
261
+ seen.add(id);
262
+ const key = pinned.get(id);
263
+ if (!key)
264
+ continue;
265
+ const [k, p] = key, sig = raw.subarray(4);
266
+ if (p.type === ED25519) {
267
+ if (!await verifyV2("ed25519", spkiOf(p.pub), msg, sig))
268
+ throw new NoteError(`bad signature from pinned key ${name}`);
269
+ logged ||= name === origin;
270
+ }
271
+ else if (p.type === HYBRID) {
272
+ unchecked = true;
273
+ hybrids.delete(id);
274
+ }
275
+ else {
276
+ const ts = sig.length === 72 ? new DataView(sig.buffer, sig.byteOffset).getBigUint64(0) : null;
277
+ if (ts === null || !await verifyV2("ed25519", spkiOf(p.pub), bytes(`cosignature/v1\ntime ${ts}\n${text}`), sig.subarray(8)))
278
+ throw new NoteError(`bad cosignature from pinned witness ${name}`);
279
+ cosigs.push([k, ts]);
280
+ }
281
+ }
282
+ if (!logged)
283
+ throw new NoteError(`no signature from a pinned log key for origin ${origin}`);
284
+ if (hybrids.size)
285
+ throw new NoteError(`no SLH-DSA signature from the pinned hybrid key for origin ${origin}`);
286
+ return { origin, size: Number(size), root, cosigs, unchecked };
287
+ }
288
+ // registry leaves: type u8 ‖ H(tenant_salt ‖ run_id) 32B ‖ record log_id 16B ‖ seq u64 ‖ record hash 32B
289
+ export const LEAF_TYPES = ["run.registered", "run.final", "log.closed", "key.retire", "capture.gap", "trace.tamper"];
290
+ export const SIGNER_LEAVES = ["log.closed", "key.retire", "capture.gap", "trace.tamper"];
291
+ export const registryOrigin = async (logOrigin, tsalt) => `${logOrigin}/registry/${(await sha256hex(concat(bytes("registry "), tsalt))).slice(0, 32)}`;
292
+ export const runHash = (tsalt, runId) => sha256(tsalt, bytes(runId));
293
+ export function parseLeaf(data) {
294
+ if (data.length !== 89 || !(data[0] >= 1 && data[0] <= LEAF_TYPES.length))
295
+ throw new Error("not a registry leaf");
296
+ return { type: LEAF_TYPES[data[0] - 1], runHash: data.subarray(1, 33), logId: hex(data.subarray(33, 49)),
297
+ seq: Number(new DataView(data.buffer, data.byteOffset).getBigUint64(49)), hash: "sha256:" + hex(data.subarray(57)) };
298
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The JSON Schema 2020-12 subset tracekit.event.v2.json uses (docs/format-v2.md §3): type, const, enum, required,
3
+ * properties, additionalProperties, items, oneOf, allOf, if/then, $ref to #/$defs, pattern (full match, ASCII),
4
+ * minimum, maximum, minLength, maxLength (code points), minItems, maxItems. "integer" excludes number tokens written with a
5
+ * fraction or exponent. Any other keyword throws, so the schema can't silently lose a check.
6
+ */
7
+ import type { Floats } from "./jcs.js";
8
+ /** JSON equality with numbers and booleans compared as numbers, as the reference's Python does. */
9
+ export declare function same(a: unknown, b: unknown): boolean;
10
+ /** Error strings of a tracekit.event.v2 event (empty when valid); `floats` from strictParse. */
11
+ export declare function validateEvent(event: unknown, floats: Floats): string[];
@@ -0,0 +1,98 @@
1
+ import SCHEMA from "./tracekit.event.v2.json" with { type: "json" };
2
+ const KEYWORDS = new Set(["type", "const", "enum", "required", "properties", "additionalProperties", "items", "oneOf",
3
+ "allOf", "if", "then", "$ref", "pattern", "minimum", "maximum", "minLength", "maxLength", "minItems", "maxItems", "$schema", "$id",
4
+ "$defs", "title", "description"]);
5
+ const isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
6
+ const isNumber = (v) => typeof v === "number";
7
+ const PATTERNS = new Map();
8
+ /** JSON equality with numbers and booleans compared as numbers, as the reference's Python does. */
9
+ export function same(a, b) {
10
+ if ((isNumber(a) || typeof a === "boolean") && (isNumber(b) || typeof b === "boolean"))
11
+ return Number(a) === Number(b);
12
+ if (Array.isArray(a) && Array.isArray(b))
13
+ return a.length === b.length && a.every((x, i) => same(x, b[i]));
14
+ if (isObject(a) && isObject(b)) {
15
+ const k = Object.keys(a);
16
+ return k.length === Object.keys(b).length && k.every((x) => Object.hasOwn(b, x) && same(a[x], b[x]));
17
+ }
18
+ return a === b;
19
+ }
20
+ function check(v, s, path, errs, float, floats) {
21
+ const unknown = Object.keys(s).filter((k) => !KEYWORDS.has(k));
22
+ if (unknown.length)
23
+ throw new Error(`unknown schema keyword(s) ${unknown} at ${path}`);
24
+ if ("$ref" in s)
25
+ return check(v, SCHEMA.$defs[s.$ref.slice("#/$defs/".length)], path, errs, float, floats);
26
+ if ("type" in s) {
27
+ const ts = Array.isArray(s.type) ? s.type : [s.type];
28
+ const is = (t) => t === "integer" ? isNumber(v) && Number.isInteger(v) && !float : t === "number" ? isNumber(v)
29
+ : t === "object" ? isObject(v) : t === "array" ? Array.isArray(v) : t === "null" ? v === null : typeof v === t;
30
+ if (!ts.some(is))
31
+ return errs.push(`${path}: expected ${ts.join("/")}`);
32
+ }
33
+ if ("const" in s && !same(v, s.const))
34
+ errs.push(`${path}: must be ${JSON.stringify(s.const)}`);
35
+ if ("enum" in s && !s.enum.some((x) => same(v, x)))
36
+ errs.push(`${path}: ${JSON.stringify(v)} not in ${JSON.stringify(s.enum)}`);
37
+ if (typeof v === "string") {
38
+ if ("pattern" in s) {
39
+ if (!PATTERNS.has(s.pattern))
40
+ PATTERNS.set(s.pattern, new RegExp(`^(?:${s.pattern})$`));
41
+ if (!PATTERNS.get(s.pattern).test(v))
42
+ errs.push(`${path}: does not match ${s.pattern}`);
43
+ }
44
+ const n = [...v].length;
45
+ if ("minLength" in s && n < s.minLength)
46
+ errs.push(`${path}: shorter than ${s.minLength}`);
47
+ if ("maxLength" in s && n > s.maxLength)
48
+ errs.push(`${path}: longer than ${s.maxLength}`);
49
+ }
50
+ if (isNumber(v) && "minimum" in s && v < s.minimum)
51
+ errs.push(`${path}: below ${s.minimum}`);
52
+ if (isNumber(v) && "maximum" in s && v > s.maximum)
53
+ errs.push(`${path}: above ${s.maximum}`);
54
+ if (Array.isArray(v) && "maxItems" in s && v.length > s.maxItems)
55
+ errs.push(`${path}: more than ${s.maxItems} items`);
56
+ if (Array.isArray(v) && "minItems" in s && v.length < s.minItems)
57
+ errs.push(`${path}: fewer than ${s.minItems} items`);
58
+ if (isObject(v)) {
59
+ for (const k of s.required ?? [])
60
+ if (!Object.hasOwn(v, k))
61
+ errs.push(`${path}: missing required '${k}'`);
62
+ const props = s.properties ?? {}, ap = s.additionalProperties ?? true;
63
+ for (const [k, x] of Object.entries(v)) {
64
+ const f = floats.get(v)?.has(k) ?? false;
65
+ if (Object.hasOwn(props, k))
66
+ check(x, props[k], `${path}.${k}`, errs, f, floats);
67
+ else if (ap === false)
68
+ errs.push(`${path}: unexpected field '${k}'`);
69
+ else if (isObject(ap))
70
+ check(x, ap, `${path}.${k}`, errs, f, floats);
71
+ }
72
+ }
73
+ if (Array.isArray(v) && "items" in s)
74
+ v.forEach((x, i) => check(x, s.items, `${path}[${i}]`, errs, floats.get(v)?.has(i) ?? false, floats));
75
+ if ("oneOf" in s) {
76
+ const matched = s.oneOf.filter((sub) => {
77
+ const e = [];
78
+ check(v, sub, path, e, float, floats);
79
+ return !e.length;
80
+ }).length;
81
+ if (matched !== 1)
82
+ errs.push(`${path}: must match exactly one alternative (matched ${matched})`);
83
+ }
84
+ for (const sub of s.allOf ?? [])
85
+ check(v, sub, path, errs, float, floats);
86
+ if ("if" in s) {
87
+ const e = [];
88
+ check(v, s.if, path, e, float, floats);
89
+ if (!e.length && "then" in s)
90
+ check(v, s.then, path, errs, float, floats);
91
+ }
92
+ }
93
+ /** Error strings of a tracekit.event.v2 event (empty when valid); `floats` from strictParse. */
94
+ export function validateEvent(event, floats) {
95
+ const errs = [];
96
+ check(event, SCHEMA, "event", errs, false, floats);
97
+ return errs;
98
+ }