@crawlcheck/sdk 1.0.2 → 1.0.3

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
@@ -16,6 +16,8 @@ const { proceed, receipt } = await cc.guard("example.com", "read", { agent: "MyA
16
16
  // The receipt is a signed crawlcheck-decision; guard() verified it here and checked its key against the published directory.
17
17
  ```
18
18
 
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
+
19
21
  `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.
20
22
 
21
23
  ## Read a record
package/dist/index.d.ts CHANGED
@@ -7,7 +7,17 @@ export interface VerifyCheck {
7
7
  why: string;
8
8
  [k: string]: unknown;
9
9
  }
10
- export interface BundleVerification {
10
+ /**
11
+ * Three separate answers. integrity_valid: hashes and the signature agree with the document. issuer_trusted: the
12
+ * signing key is one CrawlCheck publishes (null offline: not decided). accepted: both - the only field to act on.
13
+ * A document someone else signed with their own key is verified: true offline and accepted: false.
14
+ */
15
+ export interface Trust {
16
+ integrity_valid: boolean;
17
+ issuer_trusted: boolean | null;
18
+ accepted: boolean;
19
+ }
20
+ export interface BundleVerification extends Partial<Trust> {
11
21
  verified: boolean;
12
22
  summary: string;
13
23
  report: {
@@ -19,7 +29,7 @@ export interface BundleVerification {
19
29
  experiences: number | null;
20
30
  note: string;
21
31
  }
22
- export interface ReceiptVerification {
32
+ export interface ReceiptVerification extends Partial<Trust> {
23
33
  verified: boolean;
24
34
  summary: string;
25
35
  receipt: {
@@ -36,7 +46,7 @@ export interface ReceiptVerification {
36
46
  export declare const verifyBundle: (b: unknown) => Promise<BundleVerification>;
37
47
  /** Verify a remediation receipt with no call to CrawlCheck. */
38
48
  export declare const verifyReceipt: (rc: unknown) => Promise<ReceiptVerification>;
39
- export interface DocumentVerification {
49
+ export interface DocumentVerification extends Partial<Trust> {
40
50
  verified: boolean;
41
51
  summary: string;
42
52
  document: {
@@ -47,7 +57,7 @@ export interface DocumentVerification {
47
57
  checks: VerifyCheck[];
48
58
  }
49
59
  /** Verify a signed resolve answer, preflight decision receipt, data inventory, export part or deletion record offline. */
50
- export declare const verifyDocument: (doc: unknown) => Promise<DocumentVerification>;
60
+ export declare const verifyDocument: (doc: unknown) => Promise<DocumentVerification & Trust>;
51
61
  /** Verify a signed evidence export (and every bundle and receipt inside it) offline. */
52
62
  export declare const verifyExport: (x: unknown) => Promise<{
53
63
  verified: boolean;
@@ -188,12 +198,12 @@ export declare class CrawlCheck {
188
198
  /** The policy engine: a signed decision receipt (allow | warn | require_confirmation | block | unsupported). A policy can only make the decision stricter. */
189
199
  preflight(domain: string, action?: Action, opts?: PreflightOptions): Promise<DecisionReceipt>;
190
200
  /** Verify a signed document here. With onlineKeys (default), its signing key must also be in the published directory. */
191
- verifyDocument(doc: unknown, onlineKeys?: boolean): Promise<DocumentVerification>;
201
+ verifyDocument(doc: unknown, onlineKeys?: boolean): Promise<DocumentVerification & Trust>;
192
202
  /** Preflight, verify the receipt, and decide. Proceeds on allow; warn only with allowWarn; require_confirmation only if confirm() says yes; block and unsupported never. */
193
203
  guard(domain: string, action?: Action, opts?: GuardOptions): Promise<{
194
204
  proceed: boolean;
195
205
  receipt: DecisionReceipt;
196
- verification: DocumentVerification;
206
+ verification: DocumentVerification & Trust;
197
207
  }>;
198
208
  /** Signed change feed (what changed since a time, optionally for one domain). */
199
209
  changes(q?: QueryOf<"/api/v1/changes">): Promise<JsonObject>;
@@ -219,11 +229,11 @@ export declare class CrawlCheck {
219
229
  /** The evidence bundle (anonymous: subject and leaves withheld; with a licence for the domain: complete). */
220
230
  bundle(id: string): Promise<EvidenceBundleV1>;
221
231
  /** Download the bundle and verify it here, with the offline verifier: no trust in the API's own answer. */
222
- verifyOffline(id: string): Promise<BundleVerification>;
232
+ verifyOffline(id: string): Promise<BundleVerification & Trust>;
223
233
  /** A signed remediation receipt by its id (rc1:…). */
224
234
  receipt(id: string): Promise<RemediationReceiptV1>;
225
235
  /** Fetch a receipt and verify its hash and signature here. key_published stays null offline: compare the key id with publishedKeyIds(). */
226
- verifyReceiptOffline(id: string): Promise<ReceiptVerification>;
236
+ verifyReceiptOffline(id: string): Promise<ReceiptVerification & Trust>;
227
237
  /** The observer's published Ed25519 key ids (RFC 7638 thumbprints), to close the one check a verifier cannot do offline. */
228
238
  publishedKeyIds(): Promise<string[]>;
229
239
  receipts(domain: string): Promise<JsonObject>;
package/dist/index.js CHANGED
@@ -1,10 +1,24 @@
1
1
  import * as V from "./verify.js";
2
+ function withTrust(v, kid, published) {
3
+ let checks = v.checks;
4
+ let issuer = null;
5
+ if (published) {
6
+ issuer = !!kid && published.includes(kid);
7
+ const kc = { id: "key_published", ok: issuer, why: issuer ? "key id " + kid + " is in the published key directory" : "key id " + (kid || "?") + " is NOT in the published key directory - reject this document" };
8
+ checks = checks.some((c) => c.id === "key_published") ? checks.map((c) => c.id === "key_published" ? kc : c) : checks.concat([kc]);
9
+ }
10
+ const ran = checks.filter((c) => c.ok !== null), failed = checks.filter((c) => c.ok === false);
11
+ const verified = ran.length > 0 && failed.length === 0;
12
+ const integrity_valid = checks.filter((c) => c.id !== "key_published").every((c) => c.ok !== false);
13
+ return Object.assign({}, v, { checks, verified, summary: failed.length ? failed.length + " check(s) FAILED" : ran.length + " of " + checks.length + " checks ran and passed",
14
+ integrity_valid, issuer_trusted: issuer, accepted: verified && issuer === true });
15
+ }
2
16
  /** Verify an evidence bundle with no call to CrawlCheck (the same code as /crawlcheck-verify.mjs). */
3
17
  export const verifyBundle = V.verifyBundle;
4
18
  /** Verify a remediation receipt with no call to CrawlCheck. */
5
19
  export const verifyReceipt = V.verifyReceipt;
6
20
  /** Verify a signed resolve answer, preflight decision receipt, data inventory, export part or deletion record offline. */
7
- export const verifyDocument = V.verifyDataDoc;
21
+ export const verifyDocument = async (doc) => withTrust(await V.verifyDataDoc(doc), doc?.signature?.kid, null);
8
22
  /** Verify a signed evidence export (and every bundle and receipt inside it) offline. */
9
23
  export const verifyExport = V.verifyExport;
10
24
  /** Verify a signed dispute resolution offline (re-runs the published decision table over the recorded readings). */
@@ -67,19 +81,14 @@ export class CrawlCheck {
67
81
  }
68
82
  /** Verify a signed document here. With onlineKeys (default), its signing key must also be in the published directory. */
69
83
  async verifyDocument(doc, onlineKeys = true) {
70
- const v = await verifyDocument(doc);
71
- if (!onlineKeys)
72
- return v;
73
- const kid = doc?.signature?.kid;
74
- const ok = !!kid && (await this.publishedKeyIds()).includes(kid);
75
- const checks = v.checks.map((c) => c.id === "key_published" ? { id: c.id, ok, why: ok ? "key id " + kid + " is in the published key directory" : "key id " + kid + " is NOT in the published key directory" } : c);
76
- const ran = checks.filter((c) => c.ok !== null), failed = checks.filter((c) => c.ok === false);
77
- return { verified: ran.length > 0 && failed.length === 0, summary: failed.length ? failed.length + " check(s) FAILED" : ran.length + " of " + checks.length + " checks ran and passed", document: v.document, checks };
84
+ return withTrust(await V.verifyDataDoc(doc), doc?.signature?.kid, onlineKeys ? await this.publishedKeyIds() : null);
78
85
  }
79
86
  /** Preflight, verify the receipt, and decide. Proceeds on allow; warn only with allowWarn; require_confirmation only if confirm() says yes; block and unsupported never. */
80
87
  async guard(domain, action = "read", opts = {}) {
81
88
  const receipt = await this.preflight(domain, action, opts);
82
89
  const verification = await this.verifyDocument(receipt, opts.onlineKeys ?? true);
90
+ if (!verification.accepted && (opts.onlineKeys ?? true))
91
+ return { proceed: false, receipt, verification };
83
92
  if (!verification.verified)
84
93
  return { proceed: false, receipt, verification };
85
94
  const d = receipt.decision;
@@ -112,11 +121,11 @@ export class CrawlCheck {
112
121
  /** The evidence bundle (anonymous: subject and leaves withheld; with a licence for the domain: complete). */
113
122
  bundle(id) { return this.get("/api/bundle", { id, download: "0" }); }
114
123
  /** Download the bundle and verify it here, with the offline verifier: no trust in the API's own answer. */
115
- async verifyOffline(id) { return verifyBundle(await this.bundle(id)); }
124
+ async verifyOffline(id) { const b = await this.bundle(id); return withTrust(await verifyBundle(b), b?.manifest?.signature?.kid, await this.publishedKeyIds()); }
116
125
  /** A signed remediation receipt by its id (rc1:…). */
117
126
  async receipt(id) { const r = await this.get("/api/receipt", { id }); return r.receipt; }
118
127
  /** Fetch a receipt and verify its hash and signature here. key_published stays null offline: compare the key id with publishedKeyIds(). */
119
- async verifyReceiptOffline(id) { return verifyReceipt(await this.receipt(id)); }
128
+ async verifyReceiptOffline(id) { const rc = await this.receipt(id); return withTrust(await verifyReceipt(rc), rc?.signature?.kid, await this.publishedKeyIds()); }
120
129
  /** The observer's published Ed25519 key ids (RFC 7638 thumbprints), to close the one check a verifier cannot do offline. */
121
130
  async publishedKeyIds() {
122
131
  const d = await this.req("GET", "/.well-known/http-message-signatures-directory");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crawlcheck/sdk",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
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
@@ -59,5 +59,12 @@ test("live preflight: the decision receipt verifies offline, its key is publishe
59
59
  assert.equal(g.verification.verified, true);
60
60
  assert.equal(g.proceed, g.receipt.decision === "allow");
61
61
  const r = await c.resolve("example.com");
62
- assert.equal((await verifyDocument(r)).verified, true);
62
+ const off = await verifyDocument(r);
63
+ assert.equal(off.verified, true); assert.equal(off.issuer_trusted, null); assert.equal(off.accepted, false, "offline is never accepted");
64
+ assert.equal((await c.verifyDocument(r)).accepted, true);
65
+ const ids = await c.publishedKeyIds();
66
+ const ix = await (await fetch(c.base + "/conformance/index.json")).json();
67
+ const foreign = await (await fetch(ix.fixtures.find((x) => x.name === "receipt-foreign-key").file)).json();
68
+ assert.equal((await verifyReceipt(foreign)).verified, true, "self-consistent offline");
69
+ assert.ok(!ids.includes(foreign.signature.kid), "but not a CrawlCheck key, so never accepted");
63
70
  });