@crawlcheck/sdk 1.13.1 → 1.14.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.
@@ -0,0 +1,28 @@
1
+ import { CrawlCheck } from "@crawlcheck/sdk";
2
+
3
+ // Agent process: the SDK verifies preflight and attaches the receipt to the guarded GET.
4
+ export async function readWithReceipt(url) {
5
+ const result = await new CrawlCheck().guardedFetch(url, { action: "read" });
6
+ if (!result.proceed || !result.response?.ok) {
7
+ throw new Error("Read did not complete: " + (result.why?.join(", ") || result.response?.status || "refused"));
8
+ }
9
+ return result.response;
10
+ }
11
+
12
+ // Destination process: pass a configured host for this site, independent of incoming headers.
13
+ export async function checkIncomingReceipt(request, siteHost) {
14
+ const header = request.headers.get("CrawlCheck-Receipt");
15
+ if (!header) return false;
16
+ try {
17
+ const result = await new CrawlCheck().checkReceiptHeader(header, siteHost);
18
+ return result.accept;
19
+ } catch {
20
+ return false;
21
+ }
22
+ }
23
+
24
+ if (process.argv[1] && import.meta.url === new URL(process.argv[1], "file:").href) {
25
+ if (!process.argv[2]) throw new Error("Usage: node sdk/examples/receipt-roundtrip.mjs https://your-pilot-site.example/page");
26
+ const response = await readWithReceipt(process.argv[2]);
27
+ console.log("Guarded read completed:", response.status);
28
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crawlcheck/sdk",
3
- "version": "1.13.1",
3
+ "version": "1.14.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",
@@ -3,6 +3,7 @@
3
3
  import fs from "node:fs";
4
4
  import * as C from "../../dist/continuity.js";
5
5
  import { canon } from "../../dist/verify.js";
6
+ import { jcsV1 } from "../../dist/jcs.js";
6
7
  import { lockFromTools, keyPolicyFrom } from "../../dist/index.js";
7
8
  const rd = (f) => JSON.parse(fs.readFileSync(new URL("./" + f, import.meta.url), "utf8"));
8
9
  const NOW = Date.parse("2026-11-07T16:00:00Z");
@@ -21,7 +22,7 @@ const annChanged = clone(v1); annChanged[2].annotations = { readOnlyHint: true }
21
22
  const added = clone(v1).concat([{ name: "export", description: "Export all records.", inputSchema: { type: "object" } }]);
22
23
  const removed = clone(v1).slice(0, 2);
23
24
  const S = "https://mcp.example.com/mcp";
24
- const cases = [
25
+ let cases = [
25
26
  ["unchanged", lock, v1, {}],
26
27
  ["legitimate_update_description", lock, descChanged, {}],
27
28
  ["schema_changed", lock, schemaChanged, {}],
@@ -40,7 +41,45 @@ const cases = [
40
41
  ["expired_exception", lock, descChanged, { exceptions: [{ tool: "search", class: "description_changed", by: "ops@example.com", reason: "old", until: "2026-11-01T00:00:00Z" }] }],
41
42
  ["operator_policy_blocks_additions", lock, added, { policy: { id: "acme-strict", effects: { tool_added: "deny" } } }],
42
43
  ["baseline_trust_named", lock, v1, { baselineTrust: "operator_signed" }]
43
- ].map(([name, L, tools, o]) => ({ name, lock: L, tools, opts: Object.assign({ now: NOW, server: S }, o) }));
44
+ ];
45
+ // 1.14.0: a lockfile that carries jcs_v1 is compared on jcs-v1, over every tool; a v1 comparison past 300 tools is marked incomplete.
46
+ const jcsLock = Object.assign(clone(lock), { jcs_v1: await jcsV1(v1) }), jcsNoParts = clone(jcsLock); delete jcsNoParts.parts;
47
+ const noDesc = clone(v1); delete noDesc[2].description;
48
+ const emptyDesc = clone(noDesc); emptyDesc[2].description = "";
49
+ const lockNoDesc = Object.assign(clone(lock), { jcs_v1: await jcsV1(noDesc) });
50
+ { const lk2 = await lockFromTools(noDesc); lockNoDesc.tools_sha256 = lk2.tools_sha256; lockNoDesc.tools = lk2.tools; lockNoDesc.parts = await C.lockParts(noDesc); }
51
+ const titled = clone(v1); titled[0].title = "Search everything";
52
+ const outSchema = clone(v1); outSchema[2].outputSchema = { type: "object", properties: { state: { type: "string" } } };
53
+ const metaChanged = clone(v1); metaChanged[1]._meta = { "io.example/rev": 2 };
54
+ const unnamed = clone(v1).concat([{ description: "no name" }]);
55
+ const dup = clone(v1).concat([clone(v1[0])]); dup[3].description = "Search the catalogue (copy).";
56
+ const many = Array.from({ length: 301 }, (_, i) => ({ name: "t" + String(i).padStart(3, "0"), description: "Tool " + i + ".", inputSchema: { type: "object" } }));
57
+ const lkMany = await lockFromTools(many);
58
+ const lockMany = { kind: "crawlcheck-mcp-lock", v: 1, server: { url: S }, approved_at: "2026-10-20T00:00:00Z", tools_sha256: lkMany.tools_sha256, tools: lkMany.tools };
59
+ const lockManyJcs = Object.assign(clone(lockMany), { jcs_v1: await jcsV1(many) });
60
+ const many2 = clone(many); many2[300].description = "Tool 300, rewritten.";
61
+ cases.push(
62
+ ["jcs_unchanged", jcsLock, v1, {}],
63
+ ["jcs_description_changed", jcsLock, descChanged, {}],
64
+ ["jcs_schema_changed", jcsLock, schemaChanged, {}],
65
+ ["jcs_annotations_changed", jcsLock, annChanged, {}],
66
+ ["jcs_title_changed", jcsLock, titled, {}],
67
+ ["jcs_meta_changed_no_parts", jcsNoParts, metaChanged, {}],
68
+ ["jcs_output_schema_added", jcsLock, outSchema, {}],
69
+ ["jcs_contract_changed_no_parts", jcsNoParts, descChanged, {}],
70
+ ["jcs_tool_added", jcsLock, added, {}],
71
+ ["jcs_tool_removed", jcsLock, removed, {}],
72
+ ["jcs_truncated", jcsLock, v1.slice(0, 2), { complete: false }],
73
+ ["jcs_unnamed_tool", jcsLock, unnamed, {}],
74
+ ["jcs_duplicate_names", jcsLock, dup, {}],
75
+ ["jcs_empty_description_is_absent", lockNoDesc, emptyDesc, {}],
76
+ ["lk_v1_forced_empty_description", lockNoDesc, emptyDesc, { digest: "lk-v1" }],
77
+ ["lk_v1_over_300_tools", lockMany, many, {}],
78
+ ["jcs_over_300_tools", lockManyJcs, many, {}],
79
+ ["jcs_over_300_tools_change_past_300", lockManyJcs, many2, {}],
80
+ ["lk_v1_over_300_tools_change_past_300", lockMany, many2, {}]
81
+ );
82
+ cases = cases.map(([name, L, tools, o]) => ({ name, lock: L, tools, opts: Object.assign({ now: NOW, server: S }, o) }));
44
83
  for (const c of cases) c.expect = await C.continuityCheck(c.lock, c.tools, c.opts);
45
84
  const failures = [];
46
85
  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) }); }