@crawlcheck/sdk 1.7.0 → 1.8.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 CHANGED
@@ -18,6 +18,12 @@ 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.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
+
23
+ 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.
24
+
25
+ `accepted` says who signed a document. It is not permission to act: a valid receipt can say `block`, `warn` or `require_confirmation`. `guard()` decides that part.
26
+
21
27
  `preflight(domain, action, { agent, template, policy })` returns the signed receipt without deciding for you; `verifyDocument(doc)` verifies any signed resolve answer or decision receipt offline. `resolve(domain)` returns the full signed ResolveV1 answer.
22
28
 
23
29
  ## Give your agent the check as a tool
package/dist/index.d.ts CHANGED
@@ -16,8 +16,50 @@ export interface VerifyCheck {
16
16
  export interface Trust {
17
17
  integrity_valid: boolean;
18
18
  issuer_trusted: boolean | null;
19
- accepted: boolean;
19
+ accepted: boolean; /** conf2: the signing policy accepts this key for this object at its signing time (purpose, retired, compromised). null: not decided. */
20
+ key_policy_valid?: boolean | null; /** conf2: stable reason: ok, ok_bound, key_wrong_purpose, key_not_yet_active, key_retired, key_compromised, key_unknown, no_signing_time, or related_<code> for a key the document leans on. */
21
+ key_policy_code?: string | null;
22
+ }
23
+ /** conf2: the key directory and rotation record as the signing policy reads them (keyPolicyFrom / publishedKeyPolicy). */
24
+ export interface KeyPolicyInput {
25
+ v: 2;
26
+ kids: string[];
27
+ keys: Record<string, {
28
+ purpose: string;
29
+ start: number | null;
30
+ }>;
31
+ retired: Record<string, {
32
+ purpose: string;
33
+ stopped: number | null;
34
+ }>;
35
+ compromised: Record<string, number>;
36
+ rotation: boolean;
37
+ read_at?: string;
38
+ source?: string;
39
+ }
40
+ export interface KeyPolicyResult {
41
+ ok: boolean | null;
42
+ code: string;
43
+ known: boolean;
44
+ purpose: string | null;
45
+ expected: string | null;
46
+ why: string;
20
47
  }
48
+ /** The signing policy as data (https://crawlcheck.io/keys#policy): object purposes, purpose chains, start times. */
49
+ export declare const KEY_POLICY: {
50
+ v: 2;
51
+ object_purpose: Record<string, string>;
52
+ chains: Record<string, string[]>;
53
+ starts: Record<string, string | null>;
54
+ bound: Record<string, string>;
55
+ purpose_aliases: Record<string, string>;
56
+ };
57
+ /** Build the policy input from a key directory you hold and, optionally, the rotation record saved with it. */
58
+ export declare const keyPolicyFrom: (directory: unknown, rotation?: unknown) => Promise<KeyPolicyInput>;
59
+ /** Does the signing policy accept this document's signing key for this object at its signing time? */
60
+ export declare const keyPolicyCheck: (doc: unknown, kp: KeyPolicyInput) => KeyPolicyResult;
61
+ /** Apply the signing policy to a verification: key_published (retired keys included), key_purpose, and the trust fields. */
62
+ export declare const applyKeyPolicy: <T>(res: T, doc: unknown, kp: KeyPolicyInput) => T & Trust;
21
63
  export interface BundleVerification extends Partial<Trust> {
22
64
  verified: boolean;
23
65
  summary: string;
@@ -197,7 +239,11 @@ export interface PreflightOptions {
197
239
  template?: string;
198
240
  policy?: PreflightPolicy; /** 8-128 characters of A-Z a-z 0-9 . _ : - signed into the receipt (replay binding). guard() sends a fresh one when you give none. */
199
241
  nonce?: string; /** The exact operation this decision is for (method + url, or an MCP tool); signed into the receipt. */
200
- operation?: Operation;
242
+ operation?: Operation; /** An MCP server on this domain and the tool-list hash you approved (mcp-lockfile tools_sha256). The decision then reads the live tools, blocks when they differ, and its signed `assessed` block names exactly what it covered. https://crawlcheck.io/spec/mcp-lockfile */
243
+ mcp?: {
244
+ url: string;
245
+ tools_sha256?: string;
246
+ };
201
247
  }
202
248
  /** What a receipt can be scoped to. Send only the keys that apply; CrawlCheck signs them back exactly as sent. https://crawlcheck.io/spec/guard#operation */
203
249
  export interface Operation {
@@ -236,6 +282,8 @@ export interface GuardOptions extends PreflightOptions {
236
282
  onlineKeys?: boolean;
237
283
  /** Pinned CrawlCheck key ids, for offline use with onlineKeys: false. */
238
284
  publishedKids?: string[];
285
+ /** conf2: a pinned key policy (keyPolicyFrom(savedDirectory, savedRotation)), for offline use with onlineKeys: false. Checks key purpose too. */
286
+ keyPolicy?: KeyPolicyInput;
239
287
  /** Testing clock (ms since epoch). Default Date.now(). */
240
288
  now?: number;
241
289
  }
@@ -619,7 +667,10 @@ export declare class CrawlCheck {
619
667
  }): Promise<RevocationSync>;
620
668
  /** The policy engine: a signed decision receipt (allow | warn | require_confirmation | block | unsupported). A policy can only make the decision stricter. */
621
669
  preflight(domain: string, action?: Action, opts?: PreflightOptions): Promise<DecisionReceipt>;
622
- /** Verify a signed document here. With onlineKeys (default), its signing key must also be in the published directory. */
670
+ private kpCache;
671
+ /** conf2: the key directory and rotation record, read now (cached 10 minutes), as the signing policy reads them. */
672
+ publishedKeyPolicy(): Promise<KeyPolicyInput>;
673
+ /** Verify a signed document here. With onlineKeys (default), its signing key must also be in the published directory and the signing policy must accept it. */
623
674
  verifyDocument(doc: unknown, onlineKeys?: boolean): Promise<DocumentVerification & Trust>;
624
675
  /**
625
676
  * Preflight, verify the receipt, check it is for THIS operation, and decide. Proceeds on allow; warn only with allowWarn;
package/dist/index.js CHANGED
@@ -3,6 +3,14 @@ import * as L from "./log.js";
3
3
  import * as S from "./staple.js";
4
4
  import * as M from "./mcp.js";
5
5
  import * as R from "./revocations.js";
6
+ /** The signing policy as data (https://crawlcheck.io/keys#policy): object purposes, purpose chains, start times. */
7
+ export const KEY_POLICY = V.KEY_POLICY;
8
+ /** Build the policy input from a key directory you hold and, optionally, the rotation record saved with it. */
9
+ export const keyPolicyFrom = V.keyPolicyFrom;
10
+ /** Does the signing policy accept this document's signing key for this object at its signing time? */
11
+ export const keyPolicyCheck = V.keyPolicyCheck;
12
+ /** Apply the signing policy to a verification: key_published (retired keys included), key_purpose, and the trust fields. */
13
+ export const applyKeyPolicy = V.applyKeyPolicy;
6
14
  function withTrust(v, kid, published, related = []) {
7
15
  let checks = v.checks;
8
16
  let issuer = null;
@@ -302,11 +310,34 @@ export class CrawlCheck {
302
310
  b.nonce = opts.nonce;
303
311
  if (opts.operation)
304
312
  b.operation = opts.operation;
313
+ if (opts.mcp && opts.mcp.url)
314
+ b.mcp = (opts.mcp.tools_sha256 ? { url: opts.mcp.url, tools_sha256: opts.mcp.tools_sha256 } : { url: opts.mcp.url });
305
315
  return this.post("/api/v1/preflight", b);
306
316
  }
307
- /** Verify a signed document here. With onlineKeys (default), its signing key must also be in the published directory. */
317
+ kpCache = null;
318
+ /** conf2: the key directory and rotation record, read now (cached 10 minutes), as the signing policy reads them. */
319
+ async publishedKeyPolicy() {
320
+ if (this.kpCache && Date.now() - this.kpCache.at < 600e3)
321
+ return this.kpCache.kp;
322
+ const d = await this.req("GET", "/.well-known/http-message-signatures-directory");
323
+ let rot = null;
324
+ try {
325
+ const r = await this.f(this.base + "/.well-known/security-key-rotation.json", { headers: { accept: "application/json" } });
326
+ if (r.ok)
327
+ rot = await r.json();
328
+ }
329
+ catch {
330
+ rot = null;
331
+ }
332
+ const kp = Object.assign(await keyPolicyFrom(d, rot), { read_at: new Date().toISOString(), source: "online" });
333
+ this.kpCache = { at: Date.now(), kp };
334
+ return kp;
335
+ }
336
+ /** Verify a signed document here. With onlineKeys (default), its signing key must also be in the published directory and the signing policy must accept it. */
308
337
  async verifyDocument(doc, onlineKeys = true) {
309
- return withTrust(await V.verifyDataDoc(doc), doc?.signature?.kid, onlineKeys ? await this.publishedKeyIds() : null, V.relatedKids(doc));
338
+ const kp = onlineKeys ? await this.publishedKeyPolicy() : null;
339
+ const v = withTrust(await V.verifyDataDoc(doc), doc?.signature?.kid, kp ? kp.kids : null, V.relatedKids(doc));
340
+ return kp && doc?.signature ? applyKeyPolicy(v, doc, kp) : v;
310
341
  }
311
342
  /**
312
343
  * Preflight, verify the receipt, check it is for THIS operation, and decide. Proceeds on allow; warn only with allowWarn;
@@ -333,11 +364,14 @@ export class CrawlCheck {
333
364
  if (!receipt || typeof receipt !== "object")
334
365
  return no(scope.why);
335
366
  const online = opts.onlineKeys ?? true;
336
- if (!online && !(Array.isArray(opts.publishedKids) && opts.publishedKids.length))
337
- return no(["onlineKeys is false and no publishedKids were pinned: the signer cannot be checked"], receipt);
367
+ if (!online && !opts.keyPolicy && !(Array.isArray(opts.publishedKids) && opts.publishedKids.length))
368
+ return no(["onlineKeys is false and no keyPolicy or publishedKids were pinned: the signer cannot be checked"], receipt);
338
369
  let verification;
339
370
  try {
340
- verification = withTrust(await V.verifyDataDoc(receipt), receipt?.signature?.kid, online ? await this.publishedKeyIds() : opts.publishedKids, V.relatedKids(receipt));
371
+ const kp = online ? await this.publishedKeyPolicy() : opts.keyPolicy || null; // conf2: purpose, retired and compromised keys, not only "published"
372
+ verification = withTrust(await V.verifyDataDoc(receipt), receipt?.signature?.kid, kp ? kp.kids : opts.publishedKids, V.relatedKids(receipt));
373
+ if (kp)
374
+ verification = applyKeyPolicy(verification, receipt, kp);
341
375
  }
342
376
  catch (e) {
343
377
  return no(["the receipt could not be verified: " + (e?.message || e)].concat(scope.why), receipt);
@@ -347,6 +381,8 @@ export class CrawlCheck {
347
381
  why.push("integrity failed: " + verification.checks.filter((c) => c.ok === false).map((c) => c.id).join(", "));
348
382
  if (verification.issuer_trusted !== true)
349
383
  why.push("signed by a key that is not in CrawlCheck's published key directory");
384
+ else if (verification.key_policy_valid === false)
385
+ why.push("the signing policy rejects this key for a decision receipt: " + (verification.checks.find((c) => c.id === "key_purpose") || {}).why);
350
386
  if (why.length)
351
387
  return no(why, receipt, verification);
352
388
  const d = receipt.decision;
package/dist/mcp.js CHANGED
@@ -60,8 +60,12 @@ export function mcpPreflight(cc, opts = {}) {
60
60
  return false;
61
61
  return true;
62
62
  };
63
+ // tscope1: send the server URL and, with a lockfile, its approved tools_sha256, so CrawlCheck reads the live tools from its own
64
+ // vantage and the signed receipt names the endpoint and tool state it covered (a changed list comes back as block).
65
+ const mcpScope = function () { if (!state.url || !/^https:/i.test(state.url))
66
+ 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 }; };
63
67
  const decide = async function (host) {
64
- const g = await cc.guard(host, "connect", { template, policy: opts.policy, agent: opts.agent, confirm: opts.confirm, allowWarn: !!opts.allowWarn, onlineKeys: opts.onlineKeys });
68
+ 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() });
65
69
  state.receipt = g.receipt;
66
70
  state.verification = g.verification;
67
71
  state.decidedAt = Date.now();
package/dist/verify.d.ts CHANGED
@@ -20,6 +20,42 @@ export function verifyThresholdSigners(x: any): Promise<{
20
20
  why: never[];
21
21
  }[]>;
22
22
  export function relatedKids(x: any): any[];
23
+ export function keyPolicyFrom(directory: any, rotation: any): Promise<{
24
+ v: number;
25
+ kids: string[];
26
+ keys: {};
27
+ retired: {};
28
+ compromised: {};
29
+ rotation: boolean;
30
+ }>;
31
+ export function signedAtOf(x: any): number | null;
32
+ export function keyPolicyCheck(x: any, kp: any): {
33
+ ok: false | null;
34
+ code: string | undefined;
35
+ known: boolean;
36
+ purpose: any;
37
+ expected: null;
38
+ why: string | undefined;
39
+ } | {
40
+ ok: boolean;
41
+ code: string;
42
+ known: boolean;
43
+ purpose: any;
44
+ expected: any;
45
+ why: string;
46
+ };
47
+ export function applyKeyPolicy(res: any, x: any, kp: any): any;
48
+ export function publishedKeyPolicy(base: any): Promise<{
49
+ v: number;
50
+ kids: string[];
51
+ keys: {};
52
+ retired: {};
53
+ compromised: {};
54
+ rotation: boolean;
55
+ } & {
56
+ read_at: string;
57
+ source: string;
58
+ }>;
23
59
  export function trustOf(res: any, kid: any, publishedKids: any, related: any): any;
24
60
  export function publishedKeyIds(base: any): Promise<string[]>;
25
61
  export function verifyBundle(b: any, opts: any): Promise<any>;
@@ -28,3 +64,29 @@ export function verifyExport(x: any, opts: any): Promise<any>;
28
64
  export function verifyResolution(x: any, opts: any): Promise<any>;
29
65
  export function verifyDnsTxt(txt: any, opts: any): Promise<any>;
30
66
  export function verifyDataDoc(x: any, opts: any): Promise<any>;
67
+ export namespace KEY_POLICY {
68
+ let v: number;
69
+ let object_purpose: {
70
+ "crawlcheck-decision": string;
71
+ "crawlcheck-log-sth": string;
72
+ "*": string;
73
+ };
74
+ let chains: {
75
+ documents: string[];
76
+ decisions: string[];
77
+ "log heads": string[];
78
+ };
79
+ let starts: {
80
+ requests: null;
81
+ documents: string;
82
+ decisions: string;
83
+ "log heads": string;
84
+ };
85
+ let bound: {
86
+ "crawlcheck-resolve": string;
87
+ "crawlcheck-decision": string;
88
+ };
89
+ let purpose_aliases: {
90
+ "requests and documents": string;
91
+ };
92
+ }
package/dist/verify.js CHANGED
@@ -751,6 +751,152 @@ export function relatedKids(x) {
751
751
  catch (e) { }
752
752
  return out.filter((k, i) => typeof k === "string" && out.indexOf(k) === i);
753
753
  }
754
+ // conf2: the signing policy (https://crawlcheck.io/keys#policy, machine copy spec/key-policy/policy.json) as code. Which key
755
+ // PURPOSE may sign which object at which time, retired keys, compromised keys. Same rule in the Python package.
756
+ export const KEY_POLICY = { v: 2,
757
+ object_purpose: { "crawlcheck-decision": "decisions", "crawlcheck-log-sth": "log heads", "*": "documents" },
758
+ chains: { documents: ["documents", "requests"], decisions: ["decisions", "documents", "requests"], "log heads": ["log heads", "documents", "requests"] },
759
+ starts: { requests: null, documents: "2026-10-11T00:00:00Z", decisions: "2026-11-09T17:00:00Z", "log heads": "2026-11-09T17:00:00Z" },
760
+ bound: { "crawlcheck-resolve": "transparency.sth", "crawlcheck-decision": "resolve" },
761
+ purpose_aliases: { "requests and documents": "requests" } };
762
+ const kpPurpose = (p) => { p = String(p || "requests"); return KEY_POLICY.purpose_aliases[p] || p; };
763
+ const kpMs = (v) => { if (v === null || v === undefined || v === "")
764
+ return null; if (typeof v === "number")
765
+ return v < 1e11 ? v * 1000 : v; const t = Date.parse(String(v)); return isFinite(t) ? t : null; };
766
+ // The key directory (and, when you have it, the rotation record) as the input every key-policy check reads.
767
+ export async function keyPolicyFrom(directory, rotation) {
768
+ const keys = {}, retired = {}, compromised = {};
769
+ for (const k of (directory && directory.keys) || []) {
770
+ if (!k || !k.x)
771
+ continue;
772
+ const kid = b64u(await sha256(enc.encode(JSON.stringify({ crv: k.crv || "Ed25519", kty: k.kty || "OKP", x: k.x }))));
773
+ const purpose = kpPurpose(k.crawlcheck_purpose);
774
+ keys[kid] = { purpose, start: kpMs(k.crawlcheck_signs_from) ?? kpMs(k.nbf) ?? kpMs(KEY_POLICY.starts[purpose]) };
775
+ }
776
+ for (const r of (rotation && rotation.retired) || [])
777
+ if (r && r.thumbprint)
778
+ retired[r.thumbprint] = { purpose: kpPurpose(r.purpose), stopped: kpMs(r.stopped_signing_at) ?? kpMs(r.retired_at) };
779
+ for (const c of (rotation && rotation.compromised) || [])
780
+ if (c && c.thumbprint)
781
+ compromised[c.thumbprint] = kpMs(c.compromised_since) ?? 0;
782
+ return { v: 2, kids: Object.keys(keys), keys, retired, compromised, rotation: !!rotation };
783
+ }
784
+ export function signedAtOf(x) {
785
+ const s = x && x.signature, c = [s && s.signed_at, x && x.decided_at, x && x.answered_at, x && x.sealed_at, x && x.generated_at, x && x.issued_at, x && x.at];
786
+ for (const v of c) {
787
+ const t = kpMs(v);
788
+ if (t !== null)
789
+ return t;
790
+ }
791
+ return null;
792
+ }
793
+ function kpBoundKid(x) {
794
+ const path = KEY_POLICY.bound[x && x.kind];
795
+ if (!path)
796
+ return null;
797
+ const o = path.split(".").reduce((a, p) => (a && typeof a === "object" ? a[p] : undefined), x);
798
+ return (o && o.signature && o.signature.kid) || (o && typeof o.kid === "string" ? o.kid : null);
799
+ }
800
+ // One key against the policy at time t. Returns { known, ok, purpose, why } (ok null: could not be decided).
801
+ function kpKey(kid, t, kp) {
802
+ if (!kid)
803
+ return { known: false, ok: false, code: "key_unknown", why: "no signing key named" };
804
+ if (kp.compromised[kid] !== undefined)
805
+ return { known: false, ok: false, code: "key_compromised", why: "key " + kid + " is listed as compromised since " + new Date(kp.compromised[kid]).toISOString() + ": its signature alone proves nothing (" + (t !== null && t >= kp.compromised[kid] ? "signed after that moment" : "only an outside anchor still proves documents signed before it") + ")" };
806
+ const k = kp.keys[kid];
807
+ if (k) {
808
+ if (t !== null && k.start !== null && t < k.start)
809
+ return { known: true, ok: false, purpose: k.purpose, code: "key_not_yet_active", why: "key " + kid + " (" + k.purpose + ") signs nothing before " + new Date(k.start).toISOString() };
810
+ return { known: true, ok: true, purpose: k.purpose, start: k.start };
811
+ }
812
+ const r = kp.retired[kid];
813
+ if (r) {
814
+ if (t === null)
815
+ return { known: true, ok: null, purpose: r.purpose, code: "no_signing_time", why: "retired key, and the document carries no signing time" };
816
+ return r.stopped !== null && t <= r.stopped ? { known: true, ok: true, purpose: r.purpose, retired: true, why: "retired key, signed before it stopped signing (" + new Date(r.stopped).toISOString() + ")" } : { known: false, ok: false, code: "key_retired", why: "key " + kid + " stopped signing at " + (r.stopped !== null ? new Date(r.stopped).toISOString() : "an unknown time") + "; this was signed after" };
817
+ }
818
+ return { known: false, ok: false, code: "key_unknown", why: "key " + kid + " is not in the published key directory" + (kp.rotation ? " or the rotation record" : "") };
819
+ }
820
+ function kpExpected(objPurpose, t, kp) {
821
+ for (const p of KEY_POLICY.chains[objPurpose]) {
822
+ if (p === "requests")
823
+ return p;
824
+ if (Object.keys(kp.keys).some((kid) => kp.keys[kid].purpose === p && kp.compromised[kid] === undefined && (t === null || kp.keys[kid].start === null || kp.keys[kid].start <= t)))
825
+ return p;
826
+ }
827
+ return "requests";
828
+ }
829
+ // Does the policy accept this signature on this document? { ok: true | false | null, code, purpose, expected, why }
830
+ // code (stable): ok, ok_bound, key_wrong_purpose, key_not_yet_active, key_retired, key_compromised, key_unknown, no_signing_time;
831
+ // applyKeyPolicy adds related_<code> when the key a document leans on (its answer, its log head) fails.
832
+ export function keyPolicyCheck(x, kp) {
833
+ const kid = x && x.signature && x.signature.kid, t = signedAtOf(x);
834
+ const objPurpose = KEY_POLICY.object_purpose[x && x.kind] || KEY_POLICY.object_purpose["*"];
835
+ const k = kpKey(kid, t, kp);
836
+ if (k.ok !== true)
837
+ return { ok: k.ok, code: k.code, known: k.known, purpose: k.purpose || null, expected: null, why: k.why };
838
+ if (t === null)
839
+ return { ok: null, code: "no_signing_time", known: true, purpose: k.purpose, expected: null, why: "the document carries no signing time, so the key's purpose cannot be checked against the policy" };
840
+ const expected = kpExpected(objPurpose, t, kp), chain = KEY_POLICY.chains[objPurpose], at = new Date(t).toISOString();
841
+ if (k.purpose === expected)
842
+ return { ok: true, code: "ok", known: true, purpose: k.purpose, expected, why: "a " + k.purpose + " key signs " + objPurpose + " at " + at + (k.retired ? " (" + k.why + ")" : "") };
843
+ const later = chain.indexOf(k.purpose) > chain.indexOf(expected);
844
+ const bound = kpBoundKid(x), decisionKeyLive = objPurpose === "decisions" && expected === "decisions";
845
+ if (later && bound && bound === kid && !decisionKeyLive)
846
+ return { ok: true, code: "ok_bound", known: true, purpose: k.purpose, expected, why: "signed by the " + k.purpose + " key that signed the object it is bound to (" + KEY_POLICY.bound[x.kind] + "), as the policy allows" };
847
+ return { ok: false, code: "key_wrong_purpose", known: true, purpose: k.purpose, expected, why: "a " + k.purpose + " key does not sign " + objPurpose + " at " + at + ": the " + expected + " key does" + (chain.indexOf(k.purpose) < 0 ? " (" + k.purpose + " keys never sign them)" : "") };
848
+ }
849
+ // Apply the policy to a verification result: key_published (retired keys included), key_purpose, and the trust fields.
850
+ export function applyKeyPolicy(res, x, kp) {
851
+ if (!res || !kp)
852
+ return res;
853
+ const main = keyPolicyCheck(x, kp), t = signedAtOf(x), rel = [];
854
+ let relCode = null;
855
+ if (x && x.kind === "crawlcheck-decision" && x.resolve && x.resolve.kid) {
856
+ const r = kpKey(x.resolve.kid, t, kp);
857
+ if (r.ok !== true) {
858
+ rel.push("the Resolve answer it names: " + r.why);
859
+ relCode = relCode || "related_" + (r.code || "key_unknown");
860
+ }
861
+ }
862
+ const sth = x && x.transparency && x.transparency.sth;
863
+ if (sth && sth.signature) {
864
+ const r = keyPolicyCheck(sth, kp);
865
+ if (r.ok === false) {
866
+ rel.push("its log head: " + r.why);
867
+ relCode = relCode || "related_" + r.code;
868
+ }
869
+ }
870
+ const kpub = { id: "key_published", ok: main.known && !rel.length, why: main.known && !rel.length ? "key " + (x.signature && x.signature.kid) + " is " + (kp.keys[x.signature && x.signature.kid] ? "in the published key directory" : "a retired key listed in the rotation record") + ((x && (x.kind === "crawlcheck-decision" || sth)) ? ", and so is every key it leans on" : "") : (main.known ? "it leans on a key the policy rejects: " + rel.join("; ") : main.why) + ": reject this document" };
871
+ const kpur = { id: "key_purpose", ok: rel.length ? false : main.ok, code: rel.length && main.ok !== false ? relCode : main.code, why: rel.length ? rel.join("; ") : main.why };
872
+ let checks = (res.checks || []).filter((c) => c.id !== "key_published" && c.id !== "key_purpose").concat([kpub, kpur]);
873
+ const ran = checks.filter((c) => c.ok !== null), failed = checks.filter((c) => c.ok === false);
874
+ const own = checks.filter((c) => c.id !== "key_published" && c.id !== "key_purpose");
875
+ const integrity_valid = own.some((c) => c.ok !== null) && own.every((c) => c.ok !== false);
876
+ const unavailable = checks.filter((c) => c.ok === null).map((c) => c.id);
877
+ const accepted = integrity_valid && kpub.ok === true && kpur.ok !== false;
878
+ return Object.assign({}, res, { checks, verified: ran.length > 0 && failed.length === 0, summary: failed.length ? failed.length + " check(s) FAILED" : ran.length + " of " + checks.length + " checks ran and passed",
879
+ integrity_valid, issuer_trusted: kpub.ok, key_policy_valid: kpur.ok, key_policy_code: kpur.code, accepted, accepted_strict: accepted && unavailable.length === 0, assurance: unavailable.length ? "partial" : "complete", unavailable });
880
+ }
881
+ // The directory and the rotation record, read now (online). The rotation record is optional: without it retired and
882
+ // compromised keys are unknown, and only keys still in the directory are accepted.
883
+ export async function publishedKeyPolicy(base) {
884
+ const b = String(base || "https://crawlcheck.io").replace(/\/+$/, "");
885
+ const r = await fetch(b + "/.well-known/http-message-signatures-directory");
886
+ if (!r.ok)
887
+ throw new Error("key directory answered HTTP " + r.status);
888
+ const d = await r.json();
889
+ let rot = null;
890
+ try {
891
+ const q = await fetch(b + "/.well-known/security-key-rotation.json");
892
+ if (q.ok)
893
+ rot = await q.json();
894
+ }
895
+ catch (e) {
896
+ rot = null;
897
+ }
898
+ return Object.assign(await keyPolicyFrom(d, rot), { read_at: new Date().toISOString(), source: "online" });
899
+ }
754
900
  export function trustOf(res, kid, publishedKids, related) {
755
901
  let checks = Array.isArray(res && res.checks) ? res.checks : [];
756
902
  let issuer = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crawlcheck/sdk",
3
- "version": "1.7.0",
3
+ "version": "1.8.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",