@crawlcheck/sdk 1.0.3 → 1.0.4

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/index.d.ts CHANGED
@@ -115,6 +115,29 @@ export type BodyOf<P extends keyof paths> = Op<P, "post"> extends {
115
115
  };
116
116
  } ? B : JsonObject;
117
117
  export type Action = "read" | "cite" | "connect" | "transact" | "administer";
118
+ /** The header an agent sends with its request so the site can see it checked CrawlCheck first. */
119
+ export declare const RECEIPT_HEADER = "CrawlCheck-Receipt";
120
+ /** The domain the way CrawlCheck hashes and stores it: lowercased, no scheme, no leading www., no path or port. "" when not a domain. */
121
+ export declare function normalizeDomain(v: string): string;
122
+ /** { "CrawlCheck-Receipt": "v=1; sha256=..." } for a decision receipt. Send it only to the domain the receipt names, before expires_at. */
123
+ export declare function receiptHeader(receipt: {
124
+ sha256: string;
125
+ }): Record<string, string>;
126
+ export interface PrivateLookup {
127
+ found: boolean;
128
+ ready: boolean;
129
+ in_bucket: number | null;
130
+ answer: JsonObject | null;
131
+ verification: (DocumentVerification & Trust) | null;
132
+ accepted: boolean;
133
+ why?: string;
134
+ }
135
+ export interface ReceiptCheck {
136
+ accept: boolean;
137
+ why: string;
138
+ check: JsonObject | null;
139
+ verification: (DocumentVerification & Trust) | null;
140
+ }
118
141
  export type Decision = "allow" | "warn" | "require_confirmation" | "block" | "unsupported";
119
142
  export interface PreflightPolicy {
120
143
  max_age_hours?: number;
@@ -204,7 +227,12 @@ export declare class CrawlCheck {
204
227
  proceed: boolean;
205
228
  receipt: DecisionReceipt;
206
229
  verification: DocumentVerification & Trust;
230
+ header: Record<string, string> | null;
207
231
  }>;
232
+ /** Look a domain up without sending it: only the first prefixLen hex characters of sha256(domain) leave this process. found false with ready true means no public measurement. */
233
+ resolvePrivate(domain: string, prefixLen?: number): Promise<PrivateLookup>;
234
+ /** For sites: check a CrawlCheck-Receipt header received with a request to host. accept only when the receipt is held, names host, has not expired, decided allow or warn, and verifies with a published key. */
235
+ checkReceiptHeader(headerValue: string, host: string): Promise<ReceiptCheck>;
208
236
  /** Signed change feed (what changed since a time, optionally for one domain). */
209
237
  changes(q?: QueryOf<"/api/v1/changes">): Promise<JsonObject>;
210
238
  /** Report what happened after acting. Outcomes are kept apart from the signed record and never rewrite it. */
package/dist/index.js CHANGED
@@ -26,6 +26,19 @@ export const verifyResolution = V.verifyResolution;
26
26
  export const canon = V.canon;
27
27
  export const merkleRootHex = V.merkleRootHex;
28
28
  export const otsInspect = V.otsInspect;
29
+ /** The header an agent sends with its request so the site can see it checked CrawlCheck first. */
30
+ export const RECEIPT_HEADER = "CrawlCheck-Receipt";
31
+ /** The domain the way CrawlCheck hashes and stores it: lowercased, no scheme, no leading www., no path or port. "" when not a domain. */
32
+ export function normalizeDomain(v) {
33
+ const d = String(v ?? "").trim().toLowerCase().replace(/^https?:\/\//, "").replace(/^www\./, "").replace(/[\/?#].*$/, "").replace(/:\d+$/, "");
34
+ return /^[a-z0-9-]+(\.[a-z0-9-]+)+$/.test(d) ? d : "";
35
+ }
36
+ /** { "CrawlCheck-Receipt": "v=1; sha256=..." } for a decision receipt. Send it only to the domain the receipt names, before expires_at. */
37
+ export function receiptHeader(receipt) { return { [RECEIPT_HEADER]: "v=1; sha256=" + receipt.sha256 }; }
38
+ async function sha256Hex(s) {
39
+ const b = await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(s));
40
+ return Array.from(new Uint8Array(b), (x) => x.toString(16).padStart(2, "0")).join("");
41
+ }
29
42
  export class CrawlCheckError extends Error {
30
43
  status;
31
44
  body;
@@ -88,14 +101,55 @@ export class CrawlCheck {
88
101
  const receipt = await this.preflight(domain, action, opts);
89
102
  const verification = await this.verifyDocument(receipt, opts.onlineKeys ?? true);
90
103
  if (!verification.accepted && (opts.onlineKeys ?? true))
91
- return { proceed: false, receipt, verification };
104
+ return { proceed: false, receipt, verification, header: null };
92
105
  if (!verification.verified)
93
- return { proceed: false, receipt, verification };
106
+ return { proceed: false, receipt, verification, header: null };
94
107
  const d = receipt.decision;
95
108
  let proceed = d === "allow" || (d === "warn" && !!opts.allowWarn);
96
109
  if (d === "require_confirmation" && opts.confirm)
97
110
  proceed = !!(await opts.confirm(receipt));
98
- return { proceed, receipt, verification };
111
+ return { proceed, receipt, verification, header: proceed ? receiptHeader(receipt) : null };
112
+ }
113
+ /** Look a domain up without sending it: only the first prefixLen hex characters of sha256(domain) leave this process. found false with ready true means no public measurement. */
114
+ async resolvePrivate(domain, prefixLen = 2) {
115
+ const d = normalizeDomain(domain);
116
+ if (!d)
117
+ throw new Error("not a domain name: " + domain);
118
+ if (prefixLen < 2 || prefixLen > 8)
119
+ throw new Error("prefixLen must be 2 to 8");
120
+ const h = await sha256Hex(d);
121
+ let r;
122
+ try {
123
+ r = await this.req("GET", "/api/v1/resolve-prefix/" + h.slice(0, prefixLen));
124
+ }
125
+ catch (e) {
126
+ if (e instanceof CrawlCheckError && e.status === 503)
127
+ return { found: false, ready: false, in_bucket: null, answer: null, verification: null, accepted: false, why: "the prefix index is not built yet" };
128
+ throw e;
129
+ }
130
+ const hit = (r.answers || []).find((a) => a.domain_sha256 === h);
131
+ if (!hit)
132
+ return { found: false, ready: true, in_bucket: r.in_bucket ?? null, answer: null, verification: null, accepted: false };
133
+ const verification = await this.verifyDocument(hit.answer);
134
+ return { found: true, ready: true, in_bucket: r.in_bucket ?? null, answer: hit.answer, verification, accepted: !!verification.accepted && normalizeDomain(hit.answer.domain) === d };
135
+ }
136
+ /** For sites: check a CrawlCheck-Receipt header received with a request to host. accept only when the receipt is held, names host, has not expired, decided allow or warn, and verifies with a published key. */
137
+ async checkReceiptHeader(headerValue, host) {
138
+ const m = String(headerValue || "").match(/sha256=([0-9a-fA-F]{64})/) || String(headerValue || "").trim().match(/^([0-9a-fA-F]{64})$/);
139
+ if (!m)
140
+ return { accept: false, why: "no receipt digest in the header", check: null, verification: null };
141
+ const sha = m[1].toLowerCase(), h = normalizeDomain(host);
142
+ let r;
143
+ try {
144
+ r = await this.req("GET", "/api/v1/decisions/" + sha, { host: h });
145
+ }
146
+ catch (e) {
147
+ if (e instanceof CrawlCheckError && e.status === 404)
148
+ return { accept: false, why: "CrawlCheck holds no receipt with that digest", check: e.body, verification: null };
149
+ throw e;
150
+ }
151
+ const rc = r.receipt || {}, verification = await this.verifyDocument(rc);
152
+ return { accept: !!r.accept && !!verification.accepted && rc.sha256 === sha && normalizeDomain(rc.domain) === h, why: r.reason, check: r, verification };
99
153
  }
100
154
  /** Signed change feed (what changed since a time, optionally for one domain). */
101
155
  changes(q = {}) { return this.get("/api/v1/changes", q); }
package/dist/verify.d.ts CHANGED
@@ -4,88 +4,15 @@ export function otsInspect(u8: any): Promise<{
4
4
  digest: string;
5
5
  attestations: any[];
6
6
  }>;
7
- export function verifyBundle(b: any): Promise<{
8
- verified: boolean;
9
- summary: string;
10
- report: any;
11
- checks: any[];
12
- experiences: any;
13
- note: string;
14
- }>;
15
- export function verifyReceipt(rc: any): Promise<{
16
- verified: boolean;
17
- summary: string;
18
- receipt: {
19
- id: any;
20
- domain: any;
21
- verdict: any;
22
- before: any;
23
- after: any;
24
- };
25
- checks: any[];
26
- note: string;
27
- }>;
28
- export function verifyExport(x: any): Promise<{
29
- verified: boolean;
30
- summary: string;
31
- export: {
32
- domain: any;
33
- window: any;
34
- records: number;
35
- receipts: number;
36
- generated_at: any;
37
- };
38
- checks: any[];
39
- records: ({
40
- id: any;
41
- verified: null;
42
- failed: never[];
43
- why: any;
44
- scanned_at?: undefined;
45
- ran?: undefined;
46
- } | {
47
- id: any;
48
- scanned_at: any;
49
- verified: boolean | null;
50
- failed: any[];
51
- ran: number;
52
- why: string | null;
53
- })[];
54
- receipts: {
55
- id: any;
56
- verified: boolean | null;
57
- failed: any[];
58
- why: string | null;
59
- }[];
60
- note: string;
61
- }>;
62
7
  export function dspVerdict(m: any): {
63
8
  verdict: string;
64
9
  effect: string;
65
10
  rule: string;
66
11
  };
67
- export function verifyResolution(x: any): Promise<{
68
- verified: boolean;
69
- summary: string;
70
- resolution: {
71
- id: any;
72
- dispute: any;
73
- domain: any;
74
- code: any;
75
- verdict: any;
76
- effect: any;
77
- observers: any;
78
- };
79
- checks: any[];
80
- note: string;
81
- }>;
82
- export function verifyDataDoc(x: any): Promise<{
83
- verified: boolean;
84
- summary: string;
85
- document: {
86
- kind: any;
87
- domain: any;
88
- sha256: any;
89
- };
90
- checks: any[];
91
- }>;
12
+ export function trustOf(res: any, kid: any, publishedKids: any): any;
13
+ export function publishedKeyIds(base: any): Promise<string[]>;
14
+ export function verifyBundle(b: any, opts: any): Promise<any>;
15
+ export function verifyReceipt(rc: any, opts: any): Promise<any>;
16
+ export function verifyExport(x: any, opts: any): Promise<any>;
17
+ export function verifyResolution(x: any, opts: any): Promise<any>;
18
+ export function verifyDataDoc(x: any, opts: any): Promise<any>;
package/dist/verify.js CHANGED
@@ -20,6 +20,14 @@
20
20
  // decision_root (manifest v3) the sealed root over the decisions recomputes from the decision leaves (licence bundles)
21
21
  // lineage_root (manifest v3) the sealed root over the extracted business facts recomputes from their leaves (licence bundles)
22
22
  // A check that cannot run (a withheld subject, an unsealed day) is null with the reason, never a pass.
23
+ // Every result also separates the questions a single "verified" used to answer at once (vt1, 2026-10-06):
24
+ // integrity_valid no check over the bytes failed (hashes, signature, proof paths)
25
+ // issuer_trusted the signing key is in CrawlCheck's published key directory: true / false, or null offline
26
+ // accepted integrity_valid AND issuer_trusted === true. Act on this, never on verified alone.
27
+ // accepted_strict accepted AND every check ran (legacy and pre-manifest records stay false: less was sealed)
28
+ // assurance "complete" or "partial", with unavailable[] naming the checks that could not run
29
+ // Offline, issuer_trusted is null and accepted is false. Close it with --online (fetches the key directory) or
30
+ // pass the published key ids yourself: verifyReceipt(doc, { publishedKids: [...] }).
23
31
  // What it cannot do offline: read a Bitcoin block header. It prints the block height and the merkle root the
24
32
  // proof commits to; compare that value with any Bitcoin node or block explorer.
25
33
  const MEG_SIG_PREFIX = "crawlcheck-meg-manifest-v1\n";
@@ -136,7 +144,7 @@ export async function otsInspect(u8) {
136
144
  return { digest: hex(digest), attestations };
137
145
  }
138
146
  // ── the bundle ────────────────────────────────────────────────────────────────
139
- export async function verifyBundle(b) {
147
+ async function verifyBundle0(b) {
140
148
  if (!subtle)
141
149
  throw new Error("this runtime has no WebCrypto (crypto.subtle)");
142
150
  const checks = [];
@@ -274,7 +282,7 @@ export async function verifyBundle(b) {
274
282
  // verification verdict, and is signed with the observer's Ed25519 key over sha256(canonical JSON of everything but
275
283
  // receipt_id and signature). Change any field and receipt_hash fails; swap the key and key_id fails.
276
284
  const RCPT_SIG_PREFIX = "crawlcheck-receipt-v1\n";
277
- export async function verifyReceipt(rc) {
285
+ async function verifyReceipt0(rc) {
278
286
  if (!subtle)
279
287
  throw new Error("this runtime has no WebCrypto (crypto.subtle)");
280
288
  const checks = [];
@@ -322,7 +330,7 @@ export async function verifyReceipt(rc) {
322
330
  // export_sha256 and signature); then every bundle and every receipt inside it is verified exactly as it would be on
323
331
  // its own. One altered byte anywhere fails export_hash; one altered bundle fails its own checks.
324
332
  const EXPORT_SIG_PREFIX = "crawlcheck-evidence-export-v1\n";
325
- export async function verifyExport(x) {
333
+ async function verifyExport0(x) {
326
334
  if (!subtle)
327
335
  throw new Error("this runtime has no WebCrypto (crypto.subtle)");
328
336
  const checks = [];
@@ -432,7 +440,7 @@ export function dspVerdict(m) {
432
440
  return { verdict: "fixed_since", effect: "stands_for_record", rule: "the rule holds over the original bytes, and every network now finds the condition gone: correct when made, fixed since" };
433
441
  return { verdict: "no_longer_observed", effect: "stands_for_record", rule: "every network now finds the condition gone, and the original bytes could not be re-checked, so whether it was ever wrong cannot be decided" };
434
442
  }
435
- export async function verifyResolution(x) {
443
+ async function verifyResolution0(x) {
436
444
  if (!subtle)
437
445
  throw new Error("this runtime has no WebCrypto (crypto.subtle)");
438
446
  const checks = [];
@@ -483,13 +491,13 @@ export async function verifyResolution(x) {
483
491
  // Signed over sha256(canonical JSON of every field except sha256 and signature) with the prefix below. A deletion
484
492
  // record carries counts per family and the digest of the sorted deleted-key list, never the data.
485
493
  const DATA_SIG_PREFIX = "crawlcheck-data-v1\n";
486
- export async function verifyDataDoc(x) {
494
+ async function verifyDataDoc0(x) {
487
495
  if (!subtle)
488
496
  throw new Error("this runtime has no WebCrypto (crypto.subtle)");
489
497
  const checks = [];
490
498
  const C = (id, ok, why) => checks.push({ id, ok, why });
491
- if (!x || !/^crawlcheck-(data-inventory|data-export-part|deletion-record|resolve|decision)$/.test(String(x.kind)) || !x.signature)
492
- throw new Error("not a CrawlCheck data inventory, export part, deletion record, resolve answer or decision receipt");
499
+ if (!x || !/^crawlcheck-(data-inventory|data-export-part|deletion-record|resolve|decision|snapshot)$/.test(String(x.kind)) || !x.signature)
500
+ throw new Error("not a CrawlCheck data inventory, export part, deletion record, resolve answer, decision receipt or snapshot manifest");
493
501
  const body = {};
494
502
  for (const k of Object.keys(x))
495
503
  if (k !== "sha256" && k !== "signature")
@@ -533,6 +541,10 @@ export async function verifyDataDoc(x) {
533
541
  C("resolve_bound", /^[0-9a-f]{64}$/.test(String(r.sha256 || "")) && r.kid === x.signature.kid, /^[0-9a-f]{64}$/.test(String(r.sha256 || "")) ? "the decision names the resolve answer it was made from (" + String(r.sha256).slice(0, 16) + "…), signed by the same key" : "the decision does not name a resolve answer digest");
534
542
  C("decision_known", ["allow", "warn", "require_confirmation", "block", "unsupported"].indexOf(x.decision) >= 0, "decision is " + x.decision);
535
543
  }
544
+ if (x.kind === "crawlcheck-snapshot") {
545
+ const ps = Array.isArray(x.parts) ? x.parts : [];
546
+ C("parts", ps.length > 0 && ps.every((p) => /^part-\d{4}\.ndjson\.gz$/.test(String(p.path)) && /^[0-9a-f]{64}$/.test(String(p.sha256)) && p.records > 0) && ps.reduce((a, p) => a + p.records, 0) === x.records, ps.reduce((a, p) => a + (p.records || 0), 0) === x.records ? ps.length + " parts, " + x.records + " records; check each downloaded part with sha256sum against its sha256, then each line with this tool" : "the parts' record counts do not add up to records (" + x.records + ")");
547
+ }
536
548
  if (x.kind === "crawlcheck-deletion-record")
537
549
  C("counts", (x.deleted || []).reduce((a, f) => a + (f.keys || 0), 0) === x.deleted_total, "the per-family counts add up to deleted_total (" + x.deleted_total + ")");
538
550
  const ran = checks.filter((c) => c.ok !== null), failed = checks.filter((c) => c.ok === false);
@@ -541,6 +553,43 @@ export async function verifyDataDoc(x) {
541
553
  // ── command line ─────────────────────────────────────────────────────────────
542
554
  // Run as a script under ANY file name (a browser saves a second download as "crawlcheck-verify (1).mjs"); imported, it
543
555
  // stays a library. The old test matched the file name only, so a renamed copy exited 0 having checked nothing.
556
+ // vt1: integrity, issuer trust and completeness, reported apart. Same semantics as @crawlcheck/sdk and the Python package.
557
+ export function trustOf(res, kid, publishedKids) {
558
+ let checks = Array.isArray(res && res.checks) ? res.checks : [];
559
+ let issuer = null;
560
+ if (Array.isArray(publishedKids)) {
561
+ issuer = !!kid && publishedKids.indexOf(kid) > -1;
562
+ const kc = { id: "key_published", ok: issuer, why: issuer ? "key id " + kid + " is in the published key directory" : !kid ? "this document carries no signature, so its issuer cannot be authenticated" : "key id " + kid + " is NOT in the published key directory: reject this document" };
563
+ checks = checks.some((c) => c.id === "key_published") ? checks.map((c) => c.id === "key_published" ? kc : c) : checks.concat([kc]);
564
+ }
565
+ const ran = checks.filter((c) => c.ok !== null), failed = checks.filter((c) => c.ok === false);
566
+ const unavailable = checks.filter((c) => c.ok === null && c.id !== "key_published").map((c) => c.id);
567
+ const verified = ran.length > 0 && failed.length === 0;
568
+ const integrity_valid = ran.some((c) => c.id !== "key_published") && checks.filter((c) => c.id !== "key_published").every((c) => c.ok !== false);
569
+ const accepted = integrity_valid && issuer === true;
570
+ return Object.assign({}, res, { checks, verified, summary: failed.length ? failed.length + " check(s) FAILED" : ran.length + " of " + checks.length + " checks ran and passed",
571
+ integrity_valid, issuer_trusted: issuer, accepted, accepted_strict: accepted && unavailable.length === 0,
572
+ assurance: unavailable.length ? "partial" : "complete", unavailable });
573
+ }
574
+ // The published Ed25519 key ids (RFC 7638 thumbprints) from the observer's key directory.
575
+ export async function publishedKeyIds(base) {
576
+ const r = await fetch(String(base || "https://crawlcheck.io").replace(/\/+$/, "") + "/.well-known/http-message-signatures-directory");
577
+ if (!r.ok)
578
+ throw new Error("key directory answered HTTP " + r.status);
579
+ const d = await r.json(), out = [];
580
+ for (const k of (d && d.keys) || []) {
581
+ if (!k || !k.x)
582
+ continue;
583
+ out.push(b64u(await sha256(enc.encode(JSON.stringify({ crv: k.crv, kty: k.kty, x: k.x })))));
584
+ }
585
+ return out;
586
+ }
587
+ const kidOf = (o) => (o && o.signature && o.signature.kid) || (o && o.manifest && o.manifest.signature && o.manifest.signature.kid) || undefined;
588
+ export async function verifyBundle(b, opts) { return trustOf(await verifyBundle0(b), b && b.manifest && b.manifest.signature && b.manifest.signature.kid, opts && opts.publishedKids); }
589
+ export async function verifyReceipt(rc, opts) { return trustOf(await verifyReceipt0(rc), rc && rc.signature && rc.signature.kid, opts && opts.publishedKids); }
590
+ export async function verifyExport(x, opts) { return trustOf(await verifyExport0(x), kidOf(x), opts && opts.publishedKids); }
591
+ export async function verifyResolution(x, opts) { return trustOf(await verifyResolution0(x), kidOf(x), opts && opts.publishedKids); }
592
+ export async function verifyDataDoc(x, opts) { return trustOf(await verifyDataDoc0(x), kidOf(x), opts && opts.publishedKids); }
544
593
  const isMain = typeof process !== "undefined" && !!(process.argv && process.argv[1]) && await (async () => { try {
545
594
  const { pathToFileURL } = await import("node:url");
546
595
  const { realpathSync } = await import("node:fs");
@@ -550,9 +599,10 @@ catch (e) {
550
599
  return false;
551
600
  } })();
552
601
  if (isMain) {
553
- const f = process.argv[2];
602
+ const online = process.argv.includes("--online");
603
+ const f = process.argv.slice(2).filter((a) => a !== "--online")[0];
554
604
  if (!f) {
555
- console.log("usage: node crawlcheck-verify.mjs <bundle.json | receipt.json | export.json | resolution.json | deletion.json | answer.json | decision.json>");
605
+ console.log("usage: node crawlcheck-verify.mjs <bundle.json | receipt.json | export.json | resolution.json | deletion.json | answer.json | decision.json | manifest.json>");
556
606
  process.exit(2);
557
607
  }
558
608
  const { readFile } = await import("node:fs/promises");
@@ -560,8 +610,18 @@ if (isMain) {
560
610
  const isRc = doc && (doc.kind === "crawlcheck-remediation-receipt" || (doc.receipt && doc.receipt.kind === "crawlcheck-remediation-receipt"));
561
611
  const isEx = doc && doc.kind === "crawlcheck-evidence-export";
562
612
  const isDr = doc && doc.kind === "crawlcheck-dispute-resolution";
563
- const isDt = doc && /^crawlcheck-(data-inventory|data-export-part|deletion-record|resolve|decision)$/.test(String(doc.kind));
564
- const res = isDt ? await verifyDataDoc(doc) : isDr ? await verifyResolution(doc) : isEx ? await verifyExport(doc) : isRc ? await verifyReceipt(doc.kind ? doc : doc.receipt) : await verifyBundle(doc);
613
+ const isDt = doc && /^crawlcheck-(data-inventory|data-export-part|deletion-record|resolve|decision|snapshot)$/.test(String(doc.kind));
614
+ let kids;
615
+ if (online) {
616
+ try {
617
+ kids = await publishedKeyIds();
618
+ }
619
+ catch (e) {
620
+ console.log("could not read the published key directory: " + e.message);
621
+ }
622
+ }
623
+ const vo = { publishedKids: kids };
624
+ const res = isDt ? await verifyDataDoc(doc, vo) : isDr ? await verifyResolution(doc, vo) : isEx ? await verifyExport(doc, vo) : isRc ? await verifyReceipt(doc.kind ? doc : doc.receipt, vo) : await verifyBundle(doc, vo);
565
625
  const mark = (ok) => ok === true ? "PASS" : ok === false ? "FAIL" : " -- ";
566
626
  if (isDt)
567
627
  console.log("CrawlCheck " + doc.kind.replace("crawlcheck-", "").replace(/-/g, " ") + ": " + (doc.domain || "?") + " " + String(doc.sha256 || "").slice(0, 16) + "…");
@@ -582,5 +642,7 @@ if (isMain) {
582
642
  for (const r of res.receipts)
583
643
  console.log(mark(r.verified) + " receipt " + String(r.id || "?").slice(0, 20) + (r.failed && r.failed.length ? " failed: " + r.failed.join(", ") : ""));
584
644
  console.log(res.summary);
645
+ console.log("integrity " + (res.integrity_valid ? "valid" : "NOT valid") + " · issuer " + (res.issuer_trusted === true ? "trusted (key published)" : res.issuer_trusted === false ? "NOT trusted (key not published)" : "not checked offline: run with --online") + " · assurance " + res.assurance + (res.unavailable.length ? " (" + res.unavailable.join(", ") + " could not run)" : ""));
646
+ console.log(res.accepted ? (res.accepted_strict ? "ACCEPTED" : "ACCEPTED, partial assurance") : "NOT ACCEPTED" + (res.integrity_valid && res.issuer_trusted === null ? ": issuer unknown offline" : ""));
585
647
  process.exit(res.checks.some((c) => c.ok === false) ? 1 : 0);
586
648
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crawlcheck/sdk",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
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/sdk.test.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // Runs against the live API. npm test
2
2
  import test from "node:test"; import assert from "node:assert/strict";
3
- import { CrawlCheck, verifyBundle, verifyReceipt, CrawlCheckError, verifyDocument } from "../dist/index.js";
3
+ import { CrawlCheck, verifyBundle, verifyReceipt, CrawlCheckError, verifyDocument, normalizeDomain, receiptHeader } from "../dist/index.js";
4
4
  const c = new CrawlCheck();
5
5
 
6
6
  test("conformance: every published fixture gives its expected output through this package's verifier", async () => {
@@ -68,3 +68,21 @@ test("live preflight: the decision receipt verifies offline, its key is publishe
68
68
  assert.equal((await verifyReceipt(foreign)).verified, true, "self-consistent offline");
69
69
  assert.ok(!ids.includes(foreign.signature.kid), "but not a CrawlCheck key, so never accepted");
70
70
  });
71
+
72
+ test("receipt header: a site accepts the receipt for its own host only", async () => {
73
+ const rc = await c.preflight("github.com", "read", { agent: "crawlcheck-sdk-test" });
74
+ const hv = receiptHeader(rc)["CrawlCheck-Receipt"];
75
+ assert.match(hv, /^v=1; sha256=[0-9a-f]{64}$/);
76
+ const ok = await c.checkReceiptHeader(hv, "https://www.github.com/x");
77
+ assert.equal(ok.accept, rc.decision === "allow" || rc.decision === "warn", JSON.stringify(ok.why));
78
+ assert.equal((await c.checkReceiptHeader(hv, "gitlab.com")).accept, false);
79
+ assert.equal((await c.checkReceiptHeader("v=1; sha256=" + "c".repeat(64), "github.com")).accept, false);
80
+ });
81
+
82
+ test("private lookup sends only a hash prefix and verifies what it finds", async () => {
83
+ const r = await c.resolvePrivate("github.com");
84
+ assert.ok(r.ready === true || r.ready === false);
85
+ if (r.found) { assert.equal(r.accepted, true); assert.equal(r.answer.domain, "github.com"); }
86
+ assert.equal(normalizeDomain("HTTPS://WWW.Example.com:443/a?b"), "example.com");
87
+ });
88
+