@crawlcheck/sdk 1.12.0 → 1.13.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/dist/mcp.js CHANGED
@@ -17,7 +17,7 @@
17
17
  // request is never sent.
18
18
  import { normalizeDomain } from "./index.js";
19
19
  import { checkLock, toolOperation } from "./index.js";
20
- import { continuityCheck, continuityFailure, continuityClassOf } from "./continuity.js";
20
+ import { continuityCheck, continuityFailure, continuityClassOf, verifyLockBaseline, baselineRefusal } from "./continuity.js";
21
21
  export class McpPreflightRefused extends Error {
22
22
  constructor(stage, why, detail) { super("MCP preflight refused " + stage + ": " + why); this.name = "McpPreflightRefused"; this.stage = stage; this.why = why; this.receipt = detail && detail.receipt || null; this.lock = detail && detail.lock || null; this.verification = detail && detail.verification || null; }
23
23
  }
@@ -62,6 +62,36 @@ export function mcpPreflight(cc, opts = {}) {
62
62
  catch (e) { }
63
63
  } return rec; };
64
64
  const coOpts = function (extra) { return Object.assign({ mode: CO.mode, policy: CO.policy, exceptions: CO.exceptions, server: state.url }, extra || {}); };
65
+ // 1.13.0 signed baseline: the lockfile's signature is checked once, now, before anything is compared against it. Options
66
+ // (in opts.continuity, or top level without continuity): operatorKeys, keyPolicy, publishedKids, trustedKids,
67
+ // requireSignedBaseline. A bad or forged signature (or an unsigned lockfile when one is required) refuses the connect
68
+ // outside observe; observe records baseline_untrusted and goes on.
69
+ let baseLock = opts.lock || null, baseRefused = null;
70
+ state.baseline = null;
71
+ state.ready = !opts.lock ? Promise.resolve(null) : (async () => {
72
+ const bo = Object.assign({}, opts, CO || {}), req = bo.requireSignedBaseline ?? !!(bo.operatorKeys && bo.operatorKeys.length);
73
+ let b;
74
+ try {
75
+ b = await verifyLockBaseline(opts.lock, bo);
76
+ }
77
+ catch (e) {
78
+ b = { trust: "invalid", signer: null, kid: null, why: "the baseline could not be checked: " + (e && e.message || e), parts_used: false, lock: opts.lock };
79
+ }
80
+ state.baseline = { trust: b.trust, signer: b.signer, kid: b.kid, why: b.why, parts_used: b.parts_used };
81
+ baseLock = b.lock || opts.lock;
82
+ const bad = baselineRefusal(b, req);
83
+ if (bad && CO) {
84
+ const rec = await keep(await continuityFailure("baseline_untrusted", coOpts({ detail: String(bad).slice(0, 300) })));
85
+ if (rec.outcome !== "proceed")
86
+ baseRefused = new McpPreflightRefused("connect", rec.why.join("; ") + ": " + bad, { lock: rec });
87
+ }
88
+ else if (bad)
89
+ baseRefused = new McpPreflightRefused("connect", "the approved lockfile is not trusted: " + bad);
90
+ return state.baseline;
91
+ })();
92
+ state.ready.catch(function () { });
93
+ const baseOk = async function () { await state.ready; if (baseRefused)
94
+ throw baseRefused; };
65
95
  const fresh = function () {
66
96
  if (!state.receipt)
67
97
  return false;
@@ -110,6 +140,7 @@ export function mcpPreflight(cc, opts = {}) {
110
140
  state,
111
141
  /** Before initialize. url: the server URL (a stdio/local server has none and is not decided; pass opts.local = "allow" | "refuse", default allow). */
112
142
  async beforeConnect(url) {
143
+ await baseOk();
113
144
  if (!url) {
114
145
  if (opts.local === "refuse")
115
146
  throw new McpPreflightRefused("connect", "a local (stdio) server cannot be resolved and opts.local is refuse");
@@ -127,8 +158,9 @@ export function mcpPreflight(cc, opts = {}) {
127
158
  },
128
159
  /** After initialize, with the tools the server lists. Refuses (and the wrapper closes the client) when they differ from the approved lockfile. */
129
160
  async afterConnect(tools, meta) {
161
+ await baseOk();
130
162
  if (CO && opts.lock) { // 1.11.0: classify, apply the operator's policy, keep the record
131
- const rec = await keep(await continuityCheck(opts.lock, tools, coOpts({ complete: !(meta && meta.nextCursor) })));
163
+ const rec = await keep(await continuityCheck(baseLock, tools, coOpts({ complete: !(meta && meta.nextCursor), baselineTrust: state.baseline && state.baseline.trust })));
132
164
  state.lockCheck = rec;
133
165
  state.approved = null;
134
166
  state.pausedTools = new Set(rec.paused_tools);
@@ -154,7 +186,7 @@ export function mcpPreflight(cc, opts = {}) {
154
186
  state.approved = null;
155
187
  return null;
156
188
  }
157
- const lc = await checkLock(opts.lock, tools || []);
189
+ const lc = await checkLock(baseLock, tools || []);
158
190
  state.lockCheck = lc;
159
191
  state.approved = new Set((opts.lock.tools || []).map(function (t) { return t.name; }));
160
192
  if (lc.decision !== "connect")
@@ -163,6 +195,7 @@ export function mcpPreflight(cc, opts = {}) {
163
195
  },
164
196
  /** Before tools/call. */
165
197
  async beforeToolCall(name, annotations, tool) {
198
+ await baseOk();
166
199
  if (state.host && !fresh())
167
200
  await decide(state.host);
168
201
  if (state.pausedTools && state.pausedTools.has(name)) { // 1.11.0: a changed or new tool waits for a person; the others stay usable
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crawlcheck/sdk",
3
- "version": "1.12.0",
3
+ "version": "1.13.0",
4
4
  "description": "Typed client for the CrawlCheck API (generated from its OpenAPI document) and the zero-dependency offline verifier for evidence bundles and remediation receipts.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -0,0 +1,93 @@
1
+ // Signed baseline (1.13.0): the lockfile's signature is checked once, when a gate is created, before anything is compared
2
+ // against it. The cases are in the shared golden file (baseline_cases); the Python SDK must reproduce each one exactly.
3
+ import test from "node:test"; import assert from "node:assert/strict";
4
+ import fs from "node:fs";
5
+ import { verifyLockBaseline, signLock, newOperatorKey, keyPolicyFrom, continuityGate, openContinuityGate, ContinuityRefused, withPreflight, McpPreflightRefused, CONTINUITY_FAILURE_CLASSES, CONTINUITY_DEFAULT_POLICY, canon } from "../dist/index.js";
6
+ const G = JSON.parse(fs.readFileSync(new URL("./fixtures/continuity.json", import.meta.url), "utf8"));
7
+ const keyDir = G.key_directory;
8
+ const sha = async (s) => Buffer.from(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(s))).toString("hex");
9
+ const B = (n) => G.baseline_cases.find((c) => c.name === n);
10
+ const quiet = { log: false };
11
+ const opts = async (o) => { const x = Object.assign({}, o); if (x.keyPolicyDirectory) { delete x.keyPolicyDirectory; x.keyPolicy = await keyPolicyFrom(keyDir); } return x; };
12
+
13
+ for (const c of G.baseline_cases) test("golden baseline: " + c.name, async () => {
14
+ const r = await verifyLockBaseline(c.lock, await opts(c.opts)); const { lock, ...rest } = r;
15
+ assert.deepEqual(rest, c.expect); assert.equal(await sha(canon(lock)), c.used_sha256);
16
+ });
17
+ test("golden: signLock is deterministic and matches the fixture", async () => {
18
+ assert.deepEqual(await signLock(G.lock, G.operator.privateKey), G.operator.signed_lock);
19
+ });
20
+ test("baseline_untrusted is the 14th failure class, deny by default, and an integrity class", async () => {
21
+ assert.equal(CONTINUITY_FAILURE_CLASSES.length, 14); assert.equal(CONTINUITY_FAILURE_CLASSES[13], "baseline_untrusted");
22
+ assert.equal(CONTINUITY_DEFAULT_POLICY.effects.baseline_untrusted, "deny");
23
+ for (const m of ["review", "enforce"]) assert.equal(G.failures.find((f) => f.cls === "baseline_untrusted" && f.opts.mode === m).expect.outcome, "deny");
24
+ assert.equal(G.failures.find((f) => f.cls === "baseline_untrusted" && f.opts.mode === "observe").expect.outcome, "proceed");
25
+ });
26
+
27
+ test("gate, enforce: a lockfile edited after CrawlCheck signed it fails at startup", async () => {
28
+ const L = B("edited_after_signing").lock;
29
+ await assert.rejects(() => openContinuityGate(L, Object.assign({ mode: "enforce" }, quiet)), (e) => e instanceof ContinuityRefused && /baseline_untrusted|not trusted/.test(e.message) && e.record.events[0].class === "baseline_untrusted");
30
+ const g = continuityGate(L, Object.assign({ mode: "enforce" }, quiet)); await g.ready;
31
+ assert.equal(g.baseline.trust, "invalid"); assert.equal(g.records.length, 1);
32
+ await assert.rejects(() => g.check(G.tools_v1), ContinuityRefused);
33
+ });
34
+ test("gate, enforce: a lockfile re-signed with a foreign key fails at startup", async () => {
35
+ await assert.rejects(() => openContinuityGate(B("forged_foreign_key").lock, Object.assign({ mode: "enforce" }, quiet)), /not a CrawlCheck key/);
36
+ });
37
+ test("gate, observe: an untrusted baseline is recorded and passes", async () => {
38
+ const g = await openContinuityGate(B("edited_after_signing").lock, Object.assign({ mode: "observe" }, quiet));
39
+ assert.equal(g.records[0].events[0].class, "baseline_untrusted"); assert.equal(g.records[0].outcome, "proceed");
40
+ const r = await g.check([]); assert.equal(r.outcome, "proceed"); assert.equal(r.baseline.trust, "invalid");
41
+ });
42
+ test("gate: a CrawlCheck-signed lockfile is verified and named in every record", async () => {
43
+ const L = B("crawlcheck_signed").lock;
44
+ const g = await openContinuityGate(L, Object.assign({ mode: "enforce" }, quiet));
45
+ assert.equal(g.baseline.trust, "verified"); assert.equal(g.records.length, 0);
46
+ const r = await g.check([]); assert.equal(r.baseline.trust, "verified"); assert.equal(r.baseline.per_part, true);
47
+ });
48
+ test("gate: parts added after signing are not used for the comparison", async () => {
49
+ const g = await openContinuityGate(B("parts_added_after_signing").lock, Object.assign({ mode: "enforce" }, quiet));
50
+ assert.equal(g.baseline.trust, "verified"); assert.equal(g.baseline.parts_used, false);
51
+ const r = await g.check([]); assert.equal(r.baseline.per_part, false);
52
+ });
53
+ test("gate: unsigned passes unless a signed baseline is required; operator keys require one by default", async () => {
54
+ const g = await openContinuityGate(G.lock, Object.assign({ mode: "enforce" }, quiet)); assert.equal(g.baseline.trust, "unsigned");
55
+ await assert.rejects(() => openContinuityGate(G.lock, Object.assign({ mode: "enforce", requireSignedBaseline: true }, quiet)), /unsigned and a signed one is required/);
56
+ await assert.rejects(() => openContinuityGate(G.lock, Object.assign({ mode: "enforce", operatorKeys: [G.operator.publicKey] }, quiet)), /unsigned/);
57
+ await openContinuityGate(G.lock, Object.assign({ mode: "enforce", operatorKeys: [G.operator.publicKey], requireSignedBaseline: false }, quiet));
58
+ });
59
+ test("gate: an operator-signed lockfile with the operator key is operator_signed; with another key it is refused", async () => {
60
+ const g = await openContinuityGate(G.operator.signed_lock, Object.assign({ mode: "enforce", operatorKeys: [G.operator.publicKey] }, quiet));
61
+ assert.equal(g.baseline.trust, "operator_signed"); const r = await g.check(G.tools_v1); assert.equal(r.outcome, "proceed"); assert.equal(r.baseline.trust, "operator_signed");
62
+ await assert.rejects(() => openContinuityGate(G.operator.signed_lock, Object.assign({ mode: "enforce", operatorKeys: [G.operator.other_publicKey] }, quiet)), /not one of the operator keys/);
63
+ });
64
+ test("gate: an attacker who edits the operator-signed baseline to approve a new tool is refused at startup", async () => {
65
+ const L = structuredClone(G.operator.signed_lock); L.tools.push({ name: "export", sha256: "e".repeat(64) });
66
+ await assert.rejects(() => openContinuityGate(L, Object.assign({ mode: "enforce", operatorKeys: [G.operator.publicKey] }, quiet)), /changed after the operator signed it/);
67
+ const R = structuredClone(L); delete R.operator_signature; // stripping the signature instead: unsigned, refused because a key is configured
68
+ await assert.rejects(() => openContinuityGate(R, Object.assign({ mode: "enforce", operatorKeys: [G.operator.publicKey] }, quiet)), /unsigned/);
69
+ });
70
+ test("verified at startup, not per call: check() does not verify again", async () => {
71
+ let calls = 0; const g = continuityGate(B("crawlcheck_signed").lock, Object.assign({ mode: "enforce" }, quiet)); await g.ready;
72
+ const orig = crypto.subtle.verify; crypto.subtle.verify = async (...a) => { calls++; return orig.apply(crypto.subtle, a); };
73
+ try { for (let i = 0; i < 3; i++) await g.check([]); } finally { crypto.subtle.verify = orig; }
74
+ assert.equal(calls, 0);
75
+ });
76
+ test("newOperatorKey + signLock round trip", async () => {
77
+ const k = await newOperatorKey(); const s = await signLock(G.lock, k.privateKey);
78
+ assert.equal((await verifyLockBaseline(s, { operatorKeys: [k.publicKey] })).trust, "operator_signed");
79
+ assert.equal(s.operator_signature.kid, k.kid);
80
+ });
81
+ function cc() { return { async guard(host, action) { return { proceed: true, receipt: { kind: "crawlcheck-decision", decision: "allow", expires_at: new Date(Date.now() + 3600e3).toISOString() }, verification: { verified: true }, why: [] }; } }; }
82
+ test("withPreflight: an untrusted baseline refuses the connect before a byte is sent", async () => {
83
+ const sent = []; const c = { async connect() { sent.push("initialize"); }, async listTools() { return { tools: G.tools_v1 }; }, async callTool() { sent.push("call"); } };
84
+ withPreflight(c, cc(), { lock: B("edited_after_signing").lock, continuity: { mode: "enforce" } });
85
+ await assert.rejects(() => c.connect({ url: "https://mcp.example.com/mcp" }), (e) => e instanceof McpPreflightRefused && /baseline/.test(e.message));
86
+ assert.deepEqual(sent, []);
87
+ const c2 = { async connect() { sent.push("initialize"); }, async listTools() { return { tools: G.tools_v1 }; }, async callTool() { } };
88
+ withPreflight(c2, cc(), { lock: B("forged_foreign_key").lock }); // without continuity too
89
+ await assert.rejects(() => c2.connect({ url: "https://mcp.example.com/mcp" }), /lockfile is not trusted/);
90
+ const c3 = { async connect() { sent.push("initialize"); }, async listTools() { return { tools: G.tools_v1 }; }, async callTool() { } };
91
+ withPreflight(c3, cc(), { lock: G.operator.signed_lock, continuity: { mode: "enforce", operatorKeys: [G.operator.publicKey] }, toolReceipts: "none" });
92
+ await c3.connect({ url: "https://mcp.example.com/mcp" }); assert.equal(c3.preflight.state.baseline.trust, "operator_signed");
93
+ });
@@ -2,7 +2,9 @@
2
2
  // Both SDKs must reproduce every expected field, including record_sha256, byte for byte.
3
3
  import fs from "node:fs";
4
4
  import * as C from "../../dist/continuity.js";
5
- import { lockFromTools } from "../../dist/index.js";
5
+ import { canon } from "../../dist/verify.js";
6
+ import { lockFromTools, keyPolicyFrom } from "../../dist/index.js";
7
+ const rd = (f) => JSON.parse(fs.readFileSync(new URL("./" + f, import.meta.url), "utf8"));
6
8
  const NOW = Date.parse("2026-11-07T16:00:00Z");
7
9
  const v1 = [
8
10
  { name: "search", description: "Search the catalogue.", inputSchema: { type: "object", properties: { q: { type: "string" } }, required: ["q"] }, annotations: { readOnlyHint: true } },
@@ -36,11 +38,58 @@ const cases = [
36
38
  ["review_mode_change", lock, schemaChanged, { mode: "review" }],
37
39
  ["approved_exception", lock, descChanged, { exceptions: [{ tool: "search", class: "description_changed", by: "ops@example.com", reason: "vendor release 2.4 reviewed", until: "2026-12-01T00:00:00Z" }] }],
38
40
  ["expired_exception", lock, descChanged, { exceptions: [{ tool: "search", class: "description_changed", by: "ops@example.com", reason: "old", until: "2026-11-01T00:00:00Z" }] }],
39
- ["operator_policy_blocks_additions", lock, added, { policy: { id: "acme-strict", effects: { tool_added: "deny" } } }]
41
+ ["operator_policy_blocks_additions", lock, added, { policy: { id: "acme-strict", effects: { tool_added: "deny" } } }],
42
+ ["baseline_trust_named", lock, v1, { baselineTrust: "operator_signed" }]
40
43
  ].map(([name, L, tools, o]) => ({ name, lock: L, tools, opts: Object.assign({ now: NOW, server: S }, o) }));
41
44
  for (const c of cases) c.expect = await C.continuityCheck(c.lock, c.tools, c.opts);
42
45
  const failures = [];
43
46
  for (const cls of C.CONTINUITY_FAILURE_CLASSES) for (const mode of ["observe", "review", "enforce"]) { const o = { now: NOW, server: S, mode }; failures.push({ cls, opts: o, expect: await C.continuityFailure(cls, o) }); }
47
+ // 1.13.0 signed baseline. Real CrawlCheck-signed lockfiles (GET /api/v1/mcp/lock?url=https://crawlcheck.io/mcp on 2026-11-07,
48
+ // one before and one after the signed lockfile gained parts), the key directory read the same day, and a TEST operator key
49
+ // derived from a public seed (never use it for anything real).
50
+ const signed = rd("lock-signed.json"), signedNoParts = rd("lock-signed-noparts.json"), keyDir = rd("key-directory-20261107.json");
51
+ const b64e = (u8) => Buffer.from(u8).toString("base64url");
52
+ const seedKey = async (label) => {
53
+ const seed = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(label)));
54
+ const pk = await crypto.subtle.importKey("pkcs8", Buffer.concat([Buffer.from("302e020100300506032b657004220420", "hex"), Buffer.from(seed)]), { name: "Ed25519" }, true, ["sign"]);
55
+ const j = await crypto.subtle.exportKey("jwk", pk); return { seed_hex: Buffer.from(seed).toString("hex"), privateKey: { kty: "OKP", crv: "Ed25519", x: j.x, d: j.d }, publicKey: { kty: "OKP", crv: "Ed25519", x: j.x }, key: pk };
56
+ };
57
+ const opKey = await seedKey("crawlcheck continuity fixture: TEST operator key, public, never use"), otherKey = await seedKey("crawlcheck continuity fixture: TEST other operator key"), forger = await seedKey("crawlcheck continuity fixture: TEST forger key");
58
+ const sha = async (s) => Buffer.from(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(s))).toString("hex");
59
+ const forged = clone(signed); forged.tools[0].sha256 = "f".repeat(64);
60
+ { const body = {}; for (const k of Object.keys(forged)) if (k !== "sha256" && k !== "signature") body[k] = forged[k];
61
+ const d = await sha(canon(body)), msg = "crawlcheck-data-v1\n" + d, kid = b64e(new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify({ crv: "Ed25519", kty: "OKP", x: forger.publicKey.x })))));
62
+ forged.sha256 = d; forged.signature = Object.assign({}, forged.signature, { kid, message: msg, sig: b64e(new Uint8Array(await crypto.subtle.sign({ name: "Ed25519" }, forger.key, new TextEncoder().encode(msg)))), key: Object.assign({}, forged.signature.key, { kid, jwk: forger.publicKey }) }); }
63
+ const opSigned = await C.signLock(lock, opKey.privateKey), opOverCc = await C.signLock(signed, opKey.privateKey);
64
+ const edited = (L, f) => { const x = clone(L); f(x); return x; };
65
+ const OK = [opKey.publicKey];
66
+ const baselines = [
67
+ ["crawlcheck_signed", signed, {}],
68
+ ["crawlcheck_signed_key_policy", signed, { keyPolicyDirectory: true }],
69
+ ["crawlcheck_signed_other_trusted_kids", signed, { trustedKids: ["not-a-crawlcheck-key"] }],
70
+ ["edited_after_signing", edited(signed, (x) => { x.tools[0].sha256 = "0".repeat(64); }), {}],
71
+ ["parts_edited_after_signing", edited(signed, (x) => { x.parts[Object.keys(x.parts)[0]].description_sha256 = "0".repeat(64); }), {}],
72
+ ["parts_added_after_signing", Object.assign(clone(signedNoParts), { parts: { best_source: { description_sha256: "0".repeat(64), schema_sha256: "0".repeat(64), annotations_sha256: "0".repeat(64) } } }), {}],
73
+ ["forged_foreign_key", forged, {}],
74
+ ["foreign_scheme", Object.assign(clone(lock), { signature: { kid: "x", message: "something-else\n00", sig: "AA" } }), {}],
75
+ ["operator_signed", opSigned, { operatorKeys: OK }],
76
+ ["operator_signed_key_as_x", opSigned, { operatorKeys: [opKey.publicKey.x] }],
77
+ ["operator_wrong_key", opSigned, { operatorKeys: [otherKey.publicKey] }],
78
+ ["operator_edited_after_signing", edited(opSigned, (x) => { x.tools.pop(); }), { operatorKeys: OK }],
79
+ ["operator_bad_signature", edited(opSigned, (x) => { x.operator_signature.sig = opOverCc.operator_signature.sig; }), { operatorKeys: OK }],
80
+ ["operator_no_key_given", opSigned, {}],
81
+ ["operator_over_crawlcheck_no_key_given", opOverCc, {}],
82
+ ["operator_over_crawlcheck", opOverCc, { operatorKeys: OK }],
83
+ ["unsigned", lock, {}],
84
+ ["not_a_lockfile", { tools_sha256: "x" }, {}]
85
+ ];
86
+ const baselineCases = [];
87
+ for (const [name, L, o] of baselines) {
88
+ const oo = Object.assign({}, o); if (oo.keyPolicyDirectory) { delete oo.keyPolicyDirectory; oo.keyPolicy = await keyPolicyFrom(keyDir); }
89
+ const r = await C.verifyLockBaseline(L, oo); const { lock: used, ...expect } = r;
90
+ baselineCases.push({ name, lock: L, opts: o, expect, used_sha256: await sha(canon(used)) });
91
+ }
92
+ const operator = { warning: "TEST KEY derived from a public seed. Never use it for anything real.", seed_hex: opKey.seed_hex, privateKey: opKey.privateKey, publicKey: opKey.publicKey, other_publicKey: otherKey.publicKey, signed_lock: opSigned };
44
93
  // acceptance: the split for one receipt
45
94
  const F = { kind_ok: true, domain_matches: true, action_matches: true, times_present: true, not_expired: true, not_future: true, lifetime_ok: true, decision_known: true, resolve_bound: true };
46
95
  const V = { integrity_valid: true, issuer_trusted: true, key_policy_valid: true, unavailable: [] };
@@ -63,6 +112,7 @@ const acc = [
63
112
  ["baseline_changed_in_receipt", R("allow", { assessed: { expected_tools_sha256: "b".repeat(64), tools_sha256: "c".repeat(64), matches: false } }), V, F, {}],
64
113
  ["partial_assessment", R("allow", { resolve: { sha256: "a".repeat(64), status: "partial" } }), Object.assign({}, V, { unavailable: ["ots_attestations"] }), F, {}]
65
114
  ].map(([name, receipt, verification, fields, opts]) => ({ name, receipt, verification, scope: { fields }, opts, expect: C.acceptanceOf(receipt, verification, { fields }, opts) }));
66
- const out = { v: 1, generated_by: "sdk/test/fixtures/continuity-gen.mjs", now: NOW, default_policy_sha256: (await C.continuityPolicy()).sha256, lock, tools_v1: v1, cases, failures, acceptance: acc };
115
+ const out = { v: 1, generated_by: "sdk/test/fixtures/continuity-gen.mjs", now: NOW, default_policy_sha256: (await C.continuityPolicy()).sha256, lock, tools_v1: v1, cases, failures, acceptance: acc, baseline_cases: baselineCases, operator, key_directory: keyDir };
67
116
  fs.writeFileSync(new URL("./continuity.json", import.meta.url), JSON.stringify(out, null, 1) + "\n");
68
- console.log("cases", cases.length, "failures", failures.length, "acceptance", acc.length);
117
+ console.log("cases", cases.length, "failures", failures.length, "acceptance", acc.length, "baseline", baselineCases.length);
118
+ for (const b of baselineCases) console.log(" ", b.name.padEnd(40), b.expect.trust.padEnd(16), b.expect.why.slice(0, 90));