@crawlcheck/sdk 1.8.0 → 1.9.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/README.md +2 -0
- package/dist/log.d.ts +1 -0
- package/dist/log.js +28 -5
- package/package.json +1 -1
- package/test/split-view.test.js +14 -0
package/README.md
CHANGED
|
@@ -18,6 +18,8 @@ const { proceed, receipt } = await cc.guard("example.com", "read", { agent: "MyA
|
|
|
18
18
|
|
|
19
19
|
Every verification returns `integrity_valid`, `issuer_trusted` and `accepted`. Act on `accepted`: it needs the signing key to be one CrawlCheck publishes, so a document someone signed with their own key is `verified` offline but never `accepted`. The client methods (`verifyDocument`, `verifyOffline`, `verifyReceiptOffline`, `guard`) check the published key directory for you.
|
|
20
20
|
|
|
21
|
+
Since 1.9.0 `logCheck` reads the chain of signed heads 200 per request (`/api/v1/log/sths`) instead of one per batch, so a first check on a client holding no heads takes one or two requests; an older server is still walked one head per request.
|
|
22
|
+
|
|
21
23
|
Since 1.8.0 an MCP connect decision names the server and, with a lockfile, its approved `tools_sha256` (`preflight(domain, "connect", { mcp: { url, tools_sha256 } })`; `withPreflight` sends it for you). CrawlCheck reads the live tools from its own network: a changed tool list comes back as `block`, and the signed receipt's `assessed` block says exactly which endpoint and tool state it covered.
|
|
22
24
|
|
|
23
25
|
Since 1.8.0 they also apply the signing policy (https://crawlcheck.io/keys#policy): which key purpose may sign which object at which time (decision receipts move to the decision key at 2026-11-09 17:00 UTC), retired keys (valid only for documents signed before they stopped) and compromised keys (never valid on their signature alone). The result carries `key_policy_valid`, and `accepted` needs it. Offline, build the same input from copies you saved: `keyPolicyFrom(savedDirectory, savedRotationRecord)`, then `applyKeyPolicy(result, doc, kp)` or `guard(domain, action, { onlineKeys: false, keyPolicy })`. A saved copy cannot know a key retired or compromised after you saved it.
|
package/dist/log.d.ts
CHANGED
|
@@ -110,6 +110,7 @@ export function verifyWitnessToken(token: any, sthSha256: any, jwks: any): Promi
|
|
|
110
110
|
* holds (at most opts.maxWalk batches), observing every link, so any head this client was shown that the witness's
|
|
111
111
|
* chain does not contain surfaces as a fork. Heads newer than the witnessed one are listed as unwitnessed.
|
|
112
112
|
*/
|
|
113
|
+
export function walkHeads(get: any, hi: any, lo: any): Promise<any[]>;
|
|
113
114
|
export function logCheck(store: any, get: any, opts?: {}): Promise<{
|
|
114
115
|
consistent: boolean;
|
|
115
116
|
forks: any;
|
package/dist/log.js
CHANGED
|
@@ -146,6 +146,32 @@ export async function verifyWitnessToken(token, sthSha256, jwks) {
|
|
|
146
146
|
* holds (at most opts.maxWalk batches), observing every link, so any head this client was shown that the witness's
|
|
147
147
|
* chain does not contain surfaces as a fork. Heads newer than the witnessed one are listed as unwitnessed.
|
|
148
148
|
*/
|
|
149
|
+
// Signed heads for batches hi down to lo, newest first. Since 1.9.0 one request per 200 batches (/api/v1/log/sths);
|
|
150
|
+
// a server without that route is walked one head per request as before.
|
|
151
|
+
export async function walkHeads(get, hi, lo) {
|
|
152
|
+
const out = [];
|
|
153
|
+
let k = hi;
|
|
154
|
+
while (k >= lo) {
|
|
155
|
+
const n = Math.min(200, k - lo + 1);
|
|
156
|
+
const d = await get("/api/v1/log/sths?to=" + k + "&n=" + n).catch(() => null);
|
|
157
|
+
if (!d || !Array.isArray(d.sths))
|
|
158
|
+
break;
|
|
159
|
+
const got = {};
|
|
160
|
+
for (const s of d.sths)
|
|
161
|
+
if (s && Number.isInteger(s.batch))
|
|
162
|
+
got[s.batch] = s;
|
|
163
|
+
for (let b = k; b > k - n; b--)
|
|
164
|
+
if (got[b])
|
|
165
|
+
out.push(got[b]);
|
|
166
|
+
k -= n;
|
|
167
|
+
}
|
|
168
|
+
for (; k >= lo; k--) {
|
|
169
|
+
const d = await get("/api/v1/log/sth/" + k).catch(() => null);
|
|
170
|
+
if (d && d.sth)
|
|
171
|
+
out.push(d.sth);
|
|
172
|
+
}
|
|
173
|
+
return out;
|
|
174
|
+
}
|
|
149
175
|
export async function logCheck(store, get, opts = {}) {
|
|
150
176
|
const before = store.forks.length, o = { publishedKids: opts.publishedKids };
|
|
151
177
|
const log = await get("/api/v1/log");
|
|
@@ -166,11 +192,8 @@ export async function logCheck(store, get, opts = {}) {
|
|
|
166
192
|
store.witnessed = { batch: w.batch, sha256: sth.sha256, at: witnessed.at, run_id: witnessed.run_id };
|
|
167
193
|
const held = Object.keys(store.heads).map(Number).filter((k) => k < w.batch);
|
|
168
194
|
const lo = held.length ? Math.max(Math.min(...held), w.batch - (opts.maxWalk || 200)) : w.batch;
|
|
169
|
-
for (
|
|
170
|
-
|
|
171
|
-
if (d && d.sth)
|
|
172
|
-
await logObserve(store, d.sth, o);
|
|
173
|
-
}
|
|
195
|
+
for (const s of await walkHeads(get, w.batch - 1, lo))
|
|
196
|
+
await logObserve(store, s, o);
|
|
174
197
|
}
|
|
175
198
|
const wb = store.witnessed ? store.witnessed.batch : -1, now = Date.now(), maxAge = (opts.maxUnwitnessedMinutes || 120) * 60000;
|
|
176
199
|
const unwitnessed = Object.keys(store.heads).map(Number).filter((k) => k > wb).sort((a, b) => a - b).map((k) => ({ batch: k, sha256: store.heads[k].sha256, created_at: store.heads[k].created_at }));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crawlcheck/sdk",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.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",
|
package/test/split-view.test.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Worker's own log code in a test harness) must be detected offline, and the live log must check out consistent.
|
|
3
3
|
import test from "node:test"; import assert from "node:assert/strict"; import fs from "node:fs";
|
|
4
4
|
import { CrawlCheck, newLogStore, logObserve, logGossip, logHeads, verifyWitnessToken, verifyDocument } from "../dist/index.js";
|
|
5
|
+
import { walkHeads } from "../src/log.js";
|
|
5
6
|
const F = JSON.parse(fs.readFileSync(new URL("./fixtures/split-view.json", import.meta.url), "utf8"));
|
|
6
7
|
|
|
7
8
|
test("a forked log is detected by gossip: two signed heads for one batch", async () => {
|
|
@@ -26,6 +27,19 @@ test("the witness token binds one head: valid for it, refused for the forked hea
|
|
|
26
27
|
assert.equal((await verifyWitnessToken(F.witness.token, F.A[2].sha256, F.jwks)).ok, true);
|
|
27
28
|
assert.equal((await verifyWitnessToken(F.witness.token, F.B[2].sha256, F.jwks)).ok, false);
|
|
28
29
|
});
|
|
30
|
+
test("1.9.0: the chain walk reads 200 heads per request, and falls back to one per batch on an older server", async () => {
|
|
31
|
+
const calls = [];
|
|
32
|
+
const batched = async (p) => { calls.push(p); const m = /sths\?to=(\d+)&n=(\d+)/.exec(p); if (!m) throw new Error("404");
|
|
33
|
+
const to = +m[1], n = +m[2]; return { sths: Array.from({ length: n }, (_, i) => ({ kind: "crawlcheck-log-sth", batch: to - i })) }; };
|
|
34
|
+
let r = await walkHeads(batched, 449, 0);
|
|
35
|
+
assert.equal(r.length, 450); assert.deepEqual(r.slice(0, 2).map((x) => x.batch), [449, 448]); assert.equal(r[449].batch, 0);
|
|
36
|
+
assert.deepEqual(calls, ["/api/v1/log/sths?to=449&n=200", "/api/v1/log/sths?to=249&n=200", "/api/v1/log/sths?to=49&n=50"]);
|
|
37
|
+
calls.length = 0;
|
|
38
|
+
const old = async (p) => { calls.push(p); const m = /sth\/(\d+)$/.exec(p); if (!m) throw new Error("404"); return { sth: { kind: "crawlcheck-log-sth", batch: +m[1] } }; };
|
|
39
|
+
r = await walkHeads(old, 4, 2);
|
|
40
|
+
assert.deepEqual(r.map((x) => x.batch), [4, 3, 2]); assert.equal(calls.length, 4, "one refused batched call, then one per head");
|
|
41
|
+
assert.deepEqual(await walkHeads(batched, 3, 4), []);
|
|
42
|
+
});
|
|
29
43
|
test("live: the production log is consistent and GitHub's witness co-signature verifies", async () => {
|
|
30
44
|
const c = new CrawlCheck(); await c.resolve("github.com");
|
|
31
45
|
const r = await c.logCheck({ report: false });
|