@cygnuxlabs/tracekit 1.3.0 → 1.4.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.
@@ -4,6 +4,8 @@
4
4
  * integer tokens (no `.` or exponent) outside ±(2^53-1) are refused, and so are arrays and objects nested deeper than
5
5
  * MAX_DEPTH. No node: imports: browsers load this too.
6
6
  */
7
+ /** A string as Python's repr() writes it, as the reference verifier's messages quote names and keys. */
8
+ export declare function repr(s: string): string;
7
9
  /** `code` is one of: invalid_utf8, syntax, duplicate_key, non_finite, int_range, lone_surrogate, depth. */
8
10
  export declare class StrictJSONError extends Error {
9
11
  readonly code: string;
@@ -4,6 +4,14 @@
4
4
  * integer tokens (no `.` or exponent) outside ±(2^53-1) are refused, and so are arrays and objects nested deeper than
5
5
  * MAX_DEPTH. No node: imports: browsers load this too.
6
6
  */
7
+ /** A string as Python's repr() writes it, as the reference verifier's messages quote names and keys. */
8
+ export function repr(s) {
9
+ const q = s.includes("'") && !s.includes('"') ? '"' : "'";
10
+ const named = { "\\": "\\\\", "\n": "\\n", "\r": "\\r", "\t": "\\t", [q]: "\\" + q };
11
+ // lean: control characters only; Python also escapes other code points it can't print (U+0080..U+009F, U+2028...)
12
+ return q + s.replace(/[\\'"\x00-\x1f\x7f]/g, (c) => named[c] ?? (c === "'" || c === '"' ? c
13
+ : "\\x" + c.charCodeAt(0).toString(16).padStart(2, "0"))) + q;
14
+ }
7
15
  /** `code` is one of: invalid_utf8, syntax, duplicate_key, non_finite, int_range, lone_surrogate, depth. */
8
16
  export class StrictJSONError extends Error {
9
17
  code;
@@ -62,7 +70,7 @@ export function strictParse(text, floats) {
62
70
  match(WS);
63
71
  const c = text[i];
64
72
  if ((c === "{" || c === "[") && depth === MAX_DEPTH)
65
- fail("depth", `nesting deeper than ${MAX_DEPTH} at ${i}`);
73
+ fail("depth", `nesting deeper than ${MAX_DEPTH}`);
66
74
  if (c === "{") {
67
75
  i++;
68
76
  const o = {};
@@ -73,7 +81,7 @@ export function strictParse(text, floats) {
73
81
  match(WS);
74
82
  const k = str();
75
83
  if (Object.hasOwn(o, k))
76
- fail("duplicate_key", JSON.stringify(k));
84
+ fail("duplicate_key", repr(k));
77
85
  expect(":");
78
86
  Object.defineProperty(o, k, { value: value(depth + 1), enumerable: true, writable: true, configurable: true }); // "__proto__" too
79
87
  mark(o, k);
@@ -47,8 +47,9 @@ export type Note = {
47
47
  /**
48
48
  * Open a checkpoint note with pinned vkeys: a pinned Ed25519 log key named after the origin must sign it, and each
49
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
+ * `anchored` false (no trust config, so `logKeys` is empty): no log signature is required.
50
51
  */
51
- export declare function openNote(data: Uint8Array, logKeys: string[], witnessKeys: string[]): Promise<Note>;
52
+ export declare function openNote(data: Uint8Array, logKeys: string[], witnessKeys: string[], anchored?: boolean): Promise<Note>;
52
53
  export declare const LEAF_TYPES: string[];
53
54
  export declare const SIGNER_LEAVES: string[];
54
55
  export declare const registryOrigin: (logOrigin: string, tsalt: Uint8Array) => Promise<string>;
@@ -194,8 +194,9 @@ export async function parseVkey(text) {
194
194
  /**
195
195
  * Open a checkpoint note with pinned vkeys: a pinned Ed25519 log key named after the origin must sign it, and each
196
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
+ * `anchored` false (no trust config, so `logKeys` is empty): no log signature is required.
197
198
  */
198
- export async function openNote(data, logKeys, witnessKeys) {
199
+ export async function openNote(data, logKeys, witnessKeys, anchored = true) {
199
200
  if (data.length > MAX_NOTE)
200
201
  throw new NoteError("note too large");
201
202
  let note;
@@ -279,7 +280,7 @@ export async function openNote(data, logKeys, witnessKeys) {
279
280
  cosigs.push([k, ts]);
280
281
  }
281
282
  }
282
- if (!logged)
283
+ if (!logged && anchored)
283
284
  throw new NoteError(`no signature from a pinned log key for origin ${origin}`);
284
285
  if (hybrids.size)
285
286
  throw new NoteError(`no SLH-DSA signature from the pinned hybrid key for origin ${origin}`);
@@ -1,8 +1,8 @@
1
1
  /**
2
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.
3
+ * properties, additionalProperties, propertyNames, items, oneOf, allOf, if/then, $ref to #/$defs, pattern (full match,
4
+ * ASCII), minimum, maximum, minLength, maxLength (code points), minItems, maxItems. "integer" excludes number tokens
5
+ * written with a fraction or exponent. Any other keyword throws, so the schema can't silently lose a check.
6
6
  */
7
7
  import type { Floats } from "./jcs.js";
8
8
  /** JSON equality with numbers and booleans compared as numbers, as the reference's Python does. */
@@ -1,7 +1,7 @@
1
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"]);
2
+ const KEYWORDS = new Set(["type", "const", "enum", "required", "properties", "additionalProperties", "propertyNames",
3
+ "items", "oneOf", "allOf", "if", "then", "$ref", "pattern", "minimum", "maximum", "minLength", "maxLength", "minItems",
4
+ "maxItems", "$schema", "$id", "$defs", "title", "description"]);
5
5
  const isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
6
6
  const isNumber = (v) => typeof v === "number";
7
7
  const PATTERNS = new Map();
@@ -68,6 +68,8 @@ function check(v, s, path, errs, float, floats) {
68
68
  errs.push(`${path}: unexpected field '${k}'`);
69
69
  else if (isObject(ap))
70
70
  check(x, ap, `${path}.${k}`, errs, f, floats);
71
+ if ("propertyNames" in s)
72
+ check(k, s.propertyNames, `${path}.${k}`, errs, false, floats);
71
73
  }
72
74
  }
73
75
  if (Array.isArray(v) && "items" in s)
@@ -142,7 +142,9 @@
142
142
  "incident.resolved",
143
143
  "incident.response",
144
144
  "identity.suspension",
145
- "effect.observed"
145
+ "effect.observed",
146
+ "constraint.added",
147
+ "ext"
146
148
  ]
147
149
  },
148
150
  "data": {
@@ -284,6 +286,13 @@
284
286
  "maxLength": 128,
285
287
  "pattern": "^[A-Za-z0-9._-]+@[A-Za-z0-9._+-]+$",
286
288
  "description": "Policy engine id@version."
289
+ },
290
+ "ext": {
291
+ "type": "object",
292
+ "propertyNames": {
293
+ "$ref": "#/$defs/ext_name"
294
+ },
295
+ "description": "Non-critical fields (format 1.4), on any record: {name: any strict JSON value}. A verifier that does not interpret a name still checks the record's signature, chain and inclusion, and says it did not interpret the name (docs/format-v2.md section 9)."
287
296
  }
288
297
  },
289
298
  "allOf": [
@@ -1162,7 +1171,46 @@
1162
1171
  "then": {
1163
1172
  "properties": {
1164
1173
  "data": {
1165
- "$ref": "#/$defs/effect_observed"
1174
+ "oneOf": [
1175
+ {
1176
+ "$ref": "#/$defs/effect_observed"
1177
+ },
1178
+ {
1179
+ "$ref": "#/$defs/sensor_effect"
1180
+ }
1181
+ ]
1182
+ }
1183
+ }
1184
+ }
1185
+ },
1186
+ {
1187
+ "if": {
1188
+ "properties": {
1189
+ "type": {
1190
+ "const": "constraint.added"
1191
+ }
1192
+ }
1193
+ },
1194
+ "then": {
1195
+ "properties": {
1196
+ "data": {
1197
+ "$ref": "#/$defs/constraint_added"
1198
+ }
1199
+ }
1200
+ }
1201
+ },
1202
+ {
1203
+ "if": {
1204
+ "properties": {
1205
+ "type": {
1206
+ "const": "ext"
1207
+ }
1208
+ }
1209
+ },
1210
+ "then": {
1211
+ "properties": {
1212
+ "data": {
1213
+ "$ref": "#/$defs/ext"
1166
1214
  }
1167
1215
  }
1168
1216
  }
@@ -2007,6 +2055,29 @@
2007
2055
  },
2008
2056
  "verdict": {
2009
2057
  "type": "object"
2058
+ },
2059
+ "cites": {
2060
+ "type": "array",
2061
+ "maxItems": 256,
2062
+ "items": {
2063
+ "type": "object",
2064
+ "additionalProperties": false,
2065
+ "required": [
2066
+ "run_seq",
2067
+ "hash"
2068
+ ],
2069
+ "properties": {
2070
+ "run_seq": {
2071
+ "type": "integer",
2072
+ "minimum": 0,
2073
+ "maximum": 9007199254740991
2074
+ },
2075
+ "hash": {
2076
+ "$ref": "#/$defs/hash"
2077
+ }
2078
+ }
2079
+ },
2080
+ "description": "A signed finding (format 1.4; review_finding), in a findings run: the records of the analysed run it rests on, by run_seq and record hash. A review without cites (a migrated v1 judge verdict) is imported, not checked."
2010
2081
  }
2011
2082
  }
2012
2083
  },
@@ -2139,7 +2210,8 @@
2139
2210
  "held_call_unresolved",
2140
2211
  "observer_lost",
2141
2212
  "effect_outside_run",
2142
- "result_unchecked"
2213
+ "result_unchecked",
2214
+ "constraint_refused"
2143
2215
  ],
2144
2216
  "description": "Gap and tamper kinds (signer-written only)."
2145
2217
  },
@@ -2153,6 +2225,14 @@
2153
2225
  "null"
2154
2226
  ],
2155
2227
  "maxLength": 4096
2228
+ },
2229
+ "sensor": {
2230
+ "enum": [
2231
+ "tetragon",
2232
+ "falco",
2233
+ "openshell"
2234
+ ],
2235
+ "description": "format 1.4: the sensor (a kernel sensor, or OpenShell) whose import an observer_lost gap is about"
2156
2236
  }
2157
2237
  }
2158
2238
  },
@@ -2340,6 +2420,10 @@
2340
2420
  },
2341
2421
  "maxItems": 1024
2342
2422
  },
2423
+ "said_only": {
2424
+ "const": true,
2425
+ "description": "format 1.4: a response recorded with what the model said and without the calls it asked for (a harness whose hooks name no call); reconciliation reads no call from it"
2426
+ },
2343
2427
  "tool_results_sent": {
2344
2428
  "type": "array",
2345
2429
  "items": {
@@ -4526,6 +4610,117 @@
4526
4610
  }
4527
4611
  }
4528
4612
  },
4613
+ "sensor_effect": {
4614
+ "type": "object",
4615
+ "additionalProperties": false,
4616
+ "required": [
4617
+ "observer",
4618
+ "sensor",
4619
+ "kind",
4620
+ "ts",
4621
+ "pid",
4622
+ "binary",
4623
+ "attribution"
4624
+ ],
4625
+ "description": "format 1.4: what a kernel sensor (Tetragon, Falco) reported a process of the run did, or what OpenShell reported of its sandbox (its connections allowed and blocked, its entrypoint's start), imported under the import's own signer identity (an observer's assertion); signer-written",
4626
+ "properties": {
4627
+ "observer": {
4628
+ "type": "string",
4629
+ "maxLength": 256,
4630
+ "description": "the import's signer identity (scheme:subject)"
4631
+ },
4632
+ "sensor": {
4633
+ "enum": [
4634
+ "tetragon",
4635
+ "falco",
4636
+ "openshell"
4637
+ ]
4638
+ },
4639
+ "kind": {
4640
+ "enum": [
4641
+ "exec",
4642
+ "exit",
4643
+ "connect",
4644
+ "file"
4645
+ ],
4646
+ "description": "exec: the process started; exit: it ended; connect: it connected to host:port; file: the sensor's policy selected a file event of it (a write outside the workspace, in the shipped samples)"
4647
+ },
4648
+ "ts": {
4649
+ "type": "string",
4650
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{6}Z$",
4651
+ "maxLength": 27
4652
+ },
4653
+ "pid": {
4654
+ "type": "integer",
4655
+ "minimum": 0,
4656
+ "maximum": 4294967295
4657
+ },
4658
+ "binary": {
4659
+ "type": "string",
4660
+ "minLength": 1,
4661
+ "maxLength": 4096
4662
+ },
4663
+ "exec_id": {
4664
+ "type": "string",
4665
+ "minLength": 1,
4666
+ "maxLength": 256,
4667
+ "description": "the sensor's id of the process (Tetragon's exec_id)"
4668
+ },
4669
+ "parent_exec_id": {
4670
+ "type": "string",
4671
+ "minLength": 1,
4672
+ "maxLength": 256,
4673
+ "description": "exec: the sensor's id of the process that started this one (Tetragon's parent_exec_id)"
4674
+ },
4675
+ "args_commitment": {
4676
+ "$ref": "#/$defs/commitment",
4677
+ "description": "commitment to sha256 of the arguments string as the sensor reported it"
4678
+ },
4679
+ "salt_id": {
4680
+ "$ref": "#/$defs/id",
4681
+ "description": "the signer's salt id for this record's commitments (salt label <type>:<salt_id>)"
4682
+ },
4683
+ "status": {
4684
+ "type": "integer",
4685
+ "minimum": 0,
4686
+ "maximum": 4294967295
4687
+ },
4688
+ "signal": {
4689
+ "type": "string",
4690
+ "maxLength": 32
4691
+ },
4692
+ "host": {
4693
+ "type": "string",
4694
+ "minLength": 1,
4695
+ "maxLength": 253
4696
+ },
4697
+ "port": {
4698
+ "type": "integer",
4699
+ "minimum": 0,
4700
+ "maximum": 65535
4701
+ },
4702
+ "path": {
4703
+ "type": "string",
4704
+ "minLength": 1,
4705
+ "maxLength": 4096
4706
+ },
4707
+ "tool_call_id": {
4708
+ "$ref": "#/$defs/id",
4709
+ "description": "the tool call id the process's environment carried (CODEX_TOOL_CALL_ID): the agent's claim, recorded as it came and checked by no signer (TK-X206 checks it)"
4710
+ },
4711
+ "denied": {
4712
+ "const": true,
4713
+ "description": "the sensor (OpenShell's network policy) blocked it"
4714
+ },
4715
+ "attribution": {
4716
+ "enum": [
4717
+ "token",
4718
+ "identity"
4719
+ ],
4720
+ "description": "identity: the import mapped the process to the run's owner and the signer picked that owner's latest open run"
4721
+ }
4722
+ }
4723
+ },
4529
4724
  "reconcile_effect": {
4530
4725
  "type": "object",
4531
4726
  "additionalProperties": false,
@@ -4606,6 +4801,52 @@
4606
4801
  "description": "the status rests on the operator's posture (signer.yaml), not on anything the signer measured"
4607
4802
  }
4608
4803
  }
4804
+ },
4805
+ "constraint_added": {
4806
+ "type": "object",
4807
+ "additionalProperties": false,
4808
+ "required": [
4809
+ "kind"
4810
+ ],
4811
+ "description": "format 1.4: a live constraint of the run (constraint_add): what the person's request said not to do in the rest of it, as its client reported it (the Claude Code hook, at UserPromptSubmit). From then on the signer raises a call that breaks it to an ask (rule TK-CONSTRAINT) and never lowers a decision, so a made-up constraint can only hold more calls",
4812
+ "properties": {
4813
+ "kind": {
4814
+ "enum": [
4815
+ "keep_away",
4816
+ "only_change",
4817
+ "read_only",
4818
+ "no_push",
4819
+ "no_network"
4820
+ ],
4821
+ "description": "keep_away: writes of the file; only_change: writes of any other file; read_only: any write; no_push: a git push; no_network: a call that reaches the network"
4822
+ },
4823
+ "object_commitment": {
4824
+ "$ref": "#/$defs/commitment",
4825
+ "description": "keep_away and only_change: commitment to sha256 of the file's lower-cased basename; never the name"
4826
+ },
4827
+ "salt_id": {
4828
+ "$ref": "#/$defs/id",
4829
+ "description": "the signer's salt id for this record's commitment (salt label <type>:<salt_id>)"
4830
+ }
4831
+ }
4832
+ },
4833
+ "ext_name": {
4834
+ "type": "string",
4835
+ "pattern": "^[a-z][a-z0-9_.-]*$",
4836
+ "maxLength": 64,
4837
+ "description": "The name of a non-critical field or record (format 1.4)."
4838
+ },
4839
+ "ext": {
4840
+ "type": "object",
4841
+ "required": [
4842
+ "name"
4843
+ ],
4844
+ "properties": {
4845
+ "name": {
4846
+ "$ref": "#/$defs/ext_name"
4847
+ }
4848
+ },
4849
+ "description": "A non-critical record (format 1.4): its name and any other strict JSON fields, checked as the ext field is."
4609
4850
  }
4610
4851
  }
4611
4852
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,215 @@
1
+ /**
2
+ * The browser verifier: verify.html, which scripts/verify-page.mjs builds from this module, the verifier it imports and
3
+ * page.html. The bundle and trust config a person chooses are read and verified in the page, whose Content Security
4
+ * Policy lets it load and send nothing, so neither file leaves the browser. It reports what the TypeScript CLI does: a
5
+ * bundle that relies on a check this verifier doesn't implement is UNVERIFIABLE, never VERIFIED. Without a trust config
6
+ * it checks what needs no pinned key (verify's `trust` null). explain.ts says what the report means.
7
+ *
8
+ * The verification runs in a worker started from this same script (a blob: URL, which inherits the page's policy), so a
9
+ * large bundle doesn't freeze the page; in the page itself where the browser refuses the worker.
10
+ */
11
+ import { MEANING, asJson, asText, cliExit, nextSteps, sh, summarize, verdict } from "./explain.js";
12
+ import { VERSION, clean, readZip, verify } from "./index.js";
13
+ /** The report, and a summary of the runs when nothing failed (what a failed bundle says can't be relied on). */
14
+ async function work(job) {
15
+ const { report, code } = await verify(job.bundle, job.trust, job.names);
16
+ const summary = !report.failures.length && report.checks.some((c) => c.check === "signatures")
17
+ ? summarize(await readZip(job.bundle), report) : [];
18
+ return { report, code, summary };
19
+ }
20
+ if (typeof document === "undefined") // this script, as the page's worker: a report, or the error that stopped it
21
+ addEventListener("message", async (e) => {
22
+ try {
23
+ postMessage(await work(e.data));
24
+ }
25
+ catch (err) {
26
+ postMessage({ error: String(err?.message ?? err) });
27
+ }
28
+ });
29
+ else
30
+ page();
31
+ function page() {
32
+ const $ = (id) => document.getElementById(id);
33
+ const el = (tag, text = "", cls = "") => {
34
+ const e = document.createElement(tag);
35
+ if (text)
36
+ e.textContent = text;
37
+ if (cls)
38
+ e.className = cls;
39
+ return e;
40
+ };
41
+ let bundle = null, trust = null, turn = 0, worker = null, workers = true, url = "";
42
+ let last = { text: "", json: "", name: "" };
43
+ $("version").textContent = VERSION;
44
+ /** `job` verified in a fresh worker (a newer job ends the one before it), else in the page. */
45
+ function run(job) {
46
+ worker?.terminate();
47
+ if (workers)
48
+ try {
49
+ // this page's own script, by id: a browser extension may add a script element of its own first
50
+ url ||= URL.createObjectURL(new Blob([$("verifier").textContent], { type: "text/javascript" }));
51
+ const w = worker = new Worker(url); // classic: Chrome refuses module workers to file:// pages
52
+ return new Promise((resolve, reject) => {
53
+ w.onmessage = (e) => {
54
+ w.terminate();
55
+ if (e.data.error)
56
+ reject(new Error(e.data.error));
57
+ else
58
+ resolve(e.data);
59
+ };
60
+ w.onerror = (e) => {
61
+ e.preventDefault();
62
+ workers = false;
63
+ resolve(work(job));
64
+ };
65
+ w.postMessage(job);
66
+ });
67
+ }
68
+ catch {
69
+ workers = false;
70
+ }
71
+ // lean: in the page, a bundle of 100k records holds the main thread for seconds; chunk the verifier if browsers
72
+ // without blob: workers matter
73
+ return work(job);
74
+ }
75
+ const read = async (f) => ({ name: f.name, bytes: new Uint8Array(await f.arrayBuffer()) });
76
+ const unb64 = (s) => Uint8Array.from(atob(s), (c) => c.charCodeAt(0));
77
+ const sampleTrust = { name: "sample.trust.json", bytes: unb64(SAMPLES["sample.trust.json"]) };
78
+ async function choose(files) {
79
+ if (trust === sampleTrust)
80
+ trust = null; // the samples' signer is not the one behind the person's own bundle
81
+ for (const f of [...files ?? []]) {
82
+ if (/\.json$/i.test(f.name))
83
+ trust = await read(f);
84
+ else
85
+ bundle = await read(f);
86
+ }
87
+ await show();
88
+ }
89
+ function command(text) {
90
+ const row = el("div", "", "cmd"), copy = el("button", "copy");
91
+ copy.setAttribute("type", "button");
92
+ copy.setAttribute("aria-label", `Copy: ${text}`);
93
+ copy.addEventListener("click", () => navigator.clipboard?.writeText(text));
94
+ row.append(el("code", text), copy);
95
+ return row;
96
+ }
97
+ async function show() {
98
+ $("bundle-name").textContent = bundle?.name ?? "none chosen";
99
+ $("trust-name").textContent = trust?.name ?? "none";
100
+ $("clear-trust").hidden = !trust;
101
+ if (!bundle)
102
+ return;
103
+ const mine = ++turn, names = { bundle: bundle.name, trust: trust?.name ?? null };
104
+ $("result").hidden = true; // the last bundle's verdict never stands under this one's name
105
+ document.body.dataset.state = "verifying";
106
+ $("busy").textContent = "Verifying...";
107
+ $("busy").hidden = false;
108
+ let result;
109
+ try {
110
+ result = await run({ bundle: bundle.bytes, trust: trust?.bytes ?? null,
111
+ names: { bundle: names.bundle, trust: names.trust ?? "TRUST.json" } });
112
+ }
113
+ catch (err) { // never left at "Verifying...": say what stopped it and where else to check
114
+ if (mine !== turn)
115
+ return;
116
+ $("result").hidden = true;
117
+ $("busy").textContent = `Couldn't check this file: ${clean(String(err?.message ?? err))}. The `
118
+ + `command-line verifier may say more: npx -y @cygnuxlabs/tracekit@latest verify ${sh(names.bundle)}`;
119
+ document.body.dataset.state = "error";
120
+ return;
121
+ }
122
+ if (mine !== turn)
123
+ return; // a later choice of files: its report wins
124
+ const { report, code, summary } = result;
125
+ const v = verdict(report, code);
126
+ $("banner").className = `banner ${{ VERIFIED: "ok", FAILED: "bad", UNVERIFIABLE: "warn" }[v.word]}`;
127
+ $("verdict-word").textContent = v.word;
128
+ $("verdict-means").textContent = v.means;
129
+ $("verdict-not").textContent = v.notMeans;
130
+ const steps = nextSteps(report, code, names);
131
+ $("next").hidden = !steps.length;
132
+ $("steps").replaceChildren(...steps.map((s) => {
133
+ const li = el("li", s.text);
134
+ if (s.command)
135
+ li.append(command(s.command));
136
+ return li;
137
+ }));
138
+ $("summary-box").hidden = !summary.length;
139
+ $("summary-title").textContent = code === 0 ? "what the bundle holds"
140
+ : "what the bundle says it holds (not verified: see above)";
141
+ $("summary").replaceChildren(...summary.flatMap(([k, val]) => [el("dt", k), el("dd", val)]));
142
+ $("checks").replaceChildren(...report.checks.map((c) => {
143
+ const d = el("details"), s = el("summary"), line = el("span", "", "line");
144
+ const tone = { pass: "ok", warn: "warn", fail: "bad" }[c.status];
145
+ line.append(el("span", `[${c.status.toUpperCase()}]`, `status ${tone}`), ` ${c.check}` + (c.detail ? ` — ${clean(c.detail)}` : ""));
146
+ s.append(line);
147
+ d.append(s);
148
+ for (const p of c.problems)
149
+ d.append(el("div", clean(p), "problem")); // one by one: a spread overflows the stack
150
+ d.append(el("p", `What this means: ${MEANING[c.check] ?? "see docs/verdicts.md."}`, "means"));
151
+ d.open = c.status !== "pass" && c.problems.length > 0;
152
+ return d;
153
+ }));
154
+ $("integrity").textContent = `Integrity: ${clean(report.integrity)}.`;
155
+ $("assurance").textContent = `Assurance: ${clean(report.assurance)}.`;
156
+ last = { text: asText(report, code), json: JSON.stringify(asJson(report, code), null, 2) + "\n", name: bundle.name };
157
+ $("exit").textContent = last.text.trim().split("\n").at(-1) + cliExit(report);
158
+ $("report").textContent = last.text;
159
+ $("copied").textContent = "";
160
+ $("result").hidden = false;
161
+ $("busy").hidden = true;
162
+ $("banner").scrollIntoView?.({ block: "nearest" });
163
+ document.body.dataset.state = "done";
164
+ }
165
+ $("bundle").addEventListener("change", () => choose($("bundle").files));
166
+ $("trust").addEventListener("change", async () => {
167
+ const f = $("trust").files?.[0];
168
+ if (f)
169
+ trust = await read(f);
170
+ await show();
171
+ });
172
+ $("clear-trust").addEventListener("click", () => {
173
+ trust = null;
174
+ $("trust").value = "";
175
+ return show();
176
+ });
177
+ for (const [id, file] of [["sample-clean", "sample.tkb"], ["sample-tampered", "sample-tampered.tkb"]])
178
+ $(id).addEventListener("click", () => {
179
+ bundle = { name: file, bytes: unb64(SAMPLES[file]) };
180
+ trust = sampleTrust;
181
+ return show();
182
+ });
183
+ $("copy").addEventListener("click", async () => {
184
+ try {
185
+ await navigator.clipboard.writeText(last.text);
186
+ $("copied").textContent = "Copied.";
187
+ }
188
+ catch {
189
+ $("report").parentElement.open = true;
190
+ getSelection()?.selectAllChildren($("report"));
191
+ $("copied").textContent = "Selected: press Ctrl+C (Cmd+C on a Mac) to copy.";
192
+ }
193
+ });
194
+ $("download").addEventListener("click", () => {
195
+ const a = el("a");
196
+ a.href = URL.createObjectURL(new Blob([last.json], { type: "application/json" }));
197
+ a.download = `${last.name.replace(/\.[^.]*$/, "")}.verify.json`;
198
+ a.click();
199
+ setTimeout(() => URL.revokeObjectURL(a.href), 1000);
200
+ });
201
+ const drop = $("drop");
202
+ for (const t of ["dragenter", "dragover"])
203
+ drop.addEventListener(t, (e) => {
204
+ e.preventDefault();
205
+ drop.classList.add("over");
206
+ });
207
+ drop.addEventListener("dragleave", () => drop.classList.remove("over"));
208
+ drop.addEventListener("drop", (e) => {
209
+ e.preventDefault();
210
+ drop.classList.remove("over");
211
+ return choose(e.dataTransfer?.files);
212
+ });
213
+ for (const t of ["dragover", "drop"]) // a file dropped beside the zone isn't opened in place of the page
214
+ document.addEventListener(t, (e) => e.preventDefault());
215
+ }
@@ -1,8 +1,3 @@
1
- /**
2
- * The zip reader of docs/format-v2.md §9: one canonical layout, stored and deflated entries (Web Crypto's sibling
3
- * DecompressionStream), no zip64, under the bundle limits. Names that could escape a directory, symlinks and duplicate
4
- * entries are refused.
5
- */
6
1
  export declare const MAX_ENTRIES = 10000;
7
2
  export declare const MAX_ENTRY: number;
8
3
  export declare const MAX_TOTAL: number;