@crawlcheck/sdk 1.12.0 → 1.13.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/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;
@@ -76,11 +106,8 @@ export function mcpPreflight(cc, opts = {}) {
76
106
  // vantage and the signed receipt names the endpoint and tool state it covered (a changed list comes back as block).
77
107
  const mcpScope = function () { if (!state.url || !/^https:/i.test(state.url))
78
108
  return undefined; const h = opts.lock && typeof opts.lock.tools_sha256 === "string" ? opts.lock.tools_sha256 : null; return h ? { url: state.url, tools_sha256: h } : { url: state.url }; };
79
- const decide = async function (host) {
109
+ const checkConnect = async function (host) {
80
110
  const g = await cc.guard(host, "connect", { template, policy: opts.policy, agent: opts.agent, confirm: opts.confirm, allowWarn: !!opts.allowWarn, onlineKeys: opts.onlineKeys, mcp: mcpScope() });
81
- state.receipt = g.receipt;
82
- state.verification = g.verification;
83
- state.decidedAt = Date.now();
84
111
  if (!g.proceed && CO) {
85
112
  const rec = await keep(await continuityFailure(continuityClassOf(g), coOpts({ detail: (g.why || []).join("; ").slice(0, 300) })));
86
113
  if (rec.outcome === "proceed")
@@ -106,10 +133,23 @@ export function mcpPreflight(cc, opts = {}) {
106
133
  }
107
134
  return g;
108
135
  };
136
+ const decide = async function (host) {
137
+ const url = state.url;
138
+ state.receipt = null;
139
+ state.verification = null;
140
+ const result = await checkConnect(host);
141
+ if (state.url !== url) throw new McpPreflightRefused("connect", "the server URL changed during preflight");
142
+ // A refusal may include a valid-looking receipt. Only a completed gate can cache it.
143
+ state.receipt = result.receipt;
144
+ state.verification = result.verification;
145
+ state.decidedAt = Date.now();
146
+ return result;
147
+ };
109
148
  return {
110
149
  state,
111
150
  /** Before initialize. url: the server URL (a stdio/local server has none and is not decided; pass opts.local = "allow" | "refuse", default allow). */
112
151
  async beforeConnect(url) {
152
+ await baseOk();
113
153
  if (!url) {
114
154
  if (opts.local === "refuse")
115
155
  throw new McpPreflightRefused("connect", "a local (stdio) server cannot be resolved and opts.local is refuse");
@@ -127,8 +167,9 @@ export function mcpPreflight(cc, opts = {}) {
127
167
  },
128
168
  /** After initialize, with the tools the server lists. Refuses (and the wrapper closes the client) when they differ from the approved lockfile. */
129
169
  async afterConnect(tools, meta) {
170
+ await baseOk();
130
171
  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) })));
172
+ const rec = await keep(await continuityCheck(baseLock, tools, coOpts({ complete: !(meta && meta.nextCursor), baselineTrust: state.baseline && state.baseline.trust })));
132
173
  state.lockCheck = rec;
133
174
  state.approved = null;
134
175
  state.pausedTools = new Set(rec.paused_tools);
@@ -154,7 +195,7 @@ export function mcpPreflight(cc, opts = {}) {
154
195
  state.approved = null;
155
196
  return null;
156
197
  }
157
- const lc = await checkLock(opts.lock, tools || []);
198
+ const lc = await checkLock(baseLock, tools || []);
158
199
  state.lockCheck = lc;
159
200
  state.approved = new Set((opts.lock.tools || []).map(function (t) { return t.name; }));
160
201
  if (lc.decision !== "connect")
@@ -163,6 +204,7 @@ export function mcpPreflight(cc, opts = {}) {
163
204
  },
164
205
  /** Before tools/call. */
165
206
  async beforeToolCall(name, annotations, tool) {
207
+ await baseOk();
166
208
  if (state.host && !fresh())
167
209
  await decide(state.host);
168
210
  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.1",
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));