@crawlcheck/sdk 1.0.9 → 1.2.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/dist/index.d.ts +81 -10
- package/dist/index.js +159 -13
- package/dist/mcp.js +2 -1
- package/package.json +3 -2
- package/test/fixtures/guard-acceptance.json +5467 -0
- package/test/guard-acceptance.test.js +56 -0
package/dist/index.d.ts
CHANGED
|
@@ -195,8 +195,20 @@ export interface PreflightPolicy {
|
|
|
195
195
|
export interface PreflightOptions {
|
|
196
196
|
agent?: string;
|
|
197
197
|
template?: string;
|
|
198
|
-
policy?: PreflightPolicy;
|
|
198
|
+
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
|
+
nonce?: string; /** The exact operation this decision is for (method + url, or an MCP tool); signed into the receipt. */
|
|
200
|
+
operation?: Operation;
|
|
199
201
|
}
|
|
202
|
+
/** 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
|
+
export interface Operation {
|
|
204
|
+
method?: "GET" | "HEAD" | "POST" | "PUT" | "PATCH" | "DELETE" | "OPTIONS";
|
|
205
|
+
url?: string;
|
|
206
|
+
tool?: string;
|
|
207
|
+
tool_schema_sha256?: string;
|
|
208
|
+
scopes?: string[];
|
|
209
|
+
}
|
|
210
|
+
/** The operation for calling an MCP tool: its name, the sha256 of its definition (as in the lockfile) and the scopes you will use. */
|
|
211
|
+
export declare function toolOperation(tool: McpTool, scopes?: string[]): Promise<Operation>;
|
|
200
212
|
/** A signed crawlcheck-decision receipt. Verify it with verifyDocument (or let guard() do it). */
|
|
201
213
|
export interface DecisionReceipt {
|
|
202
214
|
kind: "crawlcheck-decision";
|
|
@@ -220,8 +232,61 @@ export interface GuardOptions extends PreflightOptions {
|
|
|
220
232
|
allowWarn?: boolean;
|
|
221
233
|
/** Called on "require_confirmation"; proceed only if it resolves true. Without it, require_confirmation does not proceed. */
|
|
222
234
|
confirm?: (receipt: DecisionReceipt) => boolean | Promise<boolean>;
|
|
223
|
-
/**
|
|
235
|
+
/** Read the published key directory (default true). With false, pass publishedKids: a guard never accepts a receipt without checking who signed it. */
|
|
224
236
|
onlineKeys?: boolean;
|
|
237
|
+
/** Pinned CrawlCheck key ids, for offline use with onlineKeys: false. */
|
|
238
|
+
publishedKids?: string[];
|
|
239
|
+
/** Testing clock (ms since epoch). Default Date.now(). */
|
|
240
|
+
now?: number;
|
|
241
|
+
}
|
|
242
|
+
/** The longest a receipt may stay valid, per action (seconds). A receipt that claims longer is refused. Published at https://crawlcheck.io/spec/guard */
|
|
243
|
+
export declare const ACTION_MAX_LIFETIME_S: Record<string, number>;
|
|
244
|
+
/** The most clock skew a receipt may claim (seconds). */
|
|
245
|
+
export declare const MAX_CLOCK_SKEW_S = 300;
|
|
246
|
+
export interface DecisionWant {
|
|
247
|
+
domain: string;
|
|
248
|
+
action: Action | string;
|
|
249
|
+
nonce?: string | null;
|
|
250
|
+
now?: number;
|
|
251
|
+
operation?: Operation | null;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Is this receipt about the operation you are about to perform? Checks the scope only (kind, domain, action, nonce,
|
|
255
|
+
* time window, per-action lifetime, known decision, resolve binding); the signature and issuer are checked by
|
|
256
|
+
* verifyDocument. guard() runs both. https://crawlcheck.io/spec/guard
|
|
257
|
+
*/
|
|
258
|
+
export declare function checkDecision(receipt: unknown, want: DecisionWant): {
|
|
259
|
+
ok: boolean;
|
|
260
|
+
why: string[];
|
|
261
|
+
};
|
|
262
|
+
export interface GuardResult {
|
|
263
|
+
proceed: boolean;
|
|
264
|
+
receipt: DecisionReceipt | null;
|
|
265
|
+
verification: (DocumentVerification & Trust) | null;
|
|
266
|
+
header: Record<string, string> | null;
|
|
267
|
+
why: string[];
|
|
268
|
+
split_view?: LogFork[];
|
|
269
|
+
}
|
|
270
|
+
export interface GuardedFetchOptions extends GuardOptions {
|
|
271
|
+
action?: Action;
|
|
272
|
+
init?: RequestInit;
|
|
273
|
+
maxHops?: number;
|
|
274
|
+
fetch?: typeof fetch;
|
|
275
|
+
}
|
|
276
|
+
export interface GuardedHop {
|
|
277
|
+
url: string;
|
|
278
|
+
domain: string;
|
|
279
|
+
proceed: boolean;
|
|
280
|
+
decision: string | null;
|
|
281
|
+
why: string[];
|
|
282
|
+
status?: number;
|
|
283
|
+
}
|
|
284
|
+
export interface GuardedFetchResult {
|
|
285
|
+
proceed: boolean;
|
|
286
|
+
response: Response | null;
|
|
287
|
+
url: string;
|
|
288
|
+
hops: GuardedHop[];
|
|
289
|
+
why: string[];
|
|
225
290
|
}
|
|
226
291
|
export interface LogHead {
|
|
227
292
|
kind: "crawlcheck-log-sth";
|
|
@@ -499,14 +564,20 @@ export declare class CrawlCheck {
|
|
|
499
564
|
preflight(domain: string, action?: Action, opts?: PreflightOptions): Promise<DecisionReceipt>;
|
|
500
565
|
/** Verify a signed document here. With onlineKeys (default), its signing key must also be in the published directory. */
|
|
501
566
|
verifyDocument(doc: unknown, onlineKeys?: boolean): Promise<DocumentVerification & Trust>;
|
|
502
|
-
/**
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
567
|
+
/**
|
|
568
|
+
* Preflight, verify the receipt, check it is for THIS operation, and decide. Proceeds on allow; warn only with allowWarn;
|
|
569
|
+
* require_confirmation only if confirm() says yes; block, unsupported and every failure never (fail closed: an error,
|
|
570
|
+
* an unreachable CrawlCheck, a malformed, foreign, expired, replayed or out-of-scope receipt all return proceed false
|
|
571
|
+
* with why[]). Passes the acceptance suite at https://crawlcheck.io/spec/guard
|
|
572
|
+
*/
|
|
573
|
+
guard(domain: string, action?: Action, opts?: GuardOptions): Promise<GuardResult>;
|
|
574
|
+
/**
|
|
575
|
+
* fetch() with the check where the action runs: guard() before the first request, and again before every redirect hop
|
|
576
|
+
* to a new domain. The site is contacted only after its domain proceeds; a refused hop is never requested. Redirects
|
|
577
|
+
* are followed by hand (GET/HEAD only), to http(s) only, at most maxHops (10). The receipt header is sent only to the
|
|
578
|
+
* domain it names. In a browser a cross-origin redirect hides its target, so it is refused rather than followed blind.
|
|
579
|
+
*/
|
|
580
|
+
guardedFetch(url: string, opts?: GuardedFetchOptions): Promise<GuardedFetchResult>;
|
|
510
581
|
/** 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. */
|
|
511
582
|
/** Make a signed lockfile for an MCP server (CrawlCheck reads tools/list; no tool is called). Save it; check with checkLock before every connect. */
|
|
512
583
|
mcpLock(url: string): Promise<McpLock & JsonObject>;
|
package/dist/index.js
CHANGED
|
@@ -38,6 +38,7 @@ export function normalizeDomain(v) {
|
|
|
38
38
|
}
|
|
39
39
|
/** { "CrawlCheck-Receipt": "v=1; sha256=..." } for a decision receipt. Send it only to the domain the receipt names, before expires_at. */
|
|
40
40
|
export function receiptHeader(receipt) { return { [RECEIPT_HEADER]: "v=1; sha256=" + receipt.sha256 }; }
|
|
41
|
+
function randomNonce() { const b = new Uint8Array(16); globalThis.crypto.getRandomValues(b); return Array.from(b, (x) => x.toString(16).padStart(2, "0")).join(""); }
|
|
41
42
|
async function sha256Hex(s) {
|
|
42
43
|
const b = await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(s));
|
|
43
44
|
return Array.from(new Uint8Array(b), (x) => x.toString(16).padStart(2, "0")).join("");
|
|
@@ -64,6 +65,55 @@ export async function checkLock(lock, tools) {
|
|
|
64
65
|
const same = lock?.tools_sha256 === now.tools_sha256 && !added.length && !removed.length && !modified.length;
|
|
65
66
|
return { verdict: same ? "unchanged" : "changed", decision: same ? "connect" : "block", approved_tools_sha256: lock?.tools_sha256 ?? "", current_tools_sha256: now.tools_sha256, added, removed, modified };
|
|
66
67
|
}
|
|
68
|
+
/** The operation for calling an MCP tool: its name, the sha256 of its definition (as in the lockfile) and the scopes you will use. */
|
|
69
|
+
export async function toolOperation(tool, scopes) { const o = { tool: String(tool?.name ?? ""), tool_schema_sha256: await lockToolHash(tool) }; if (scopes && scopes.length)
|
|
70
|
+
o.scopes = scopes.slice(); return o; }
|
|
71
|
+
/** The longest a receipt may stay valid, per action (seconds). A receipt that claims longer is refused. Published at https://crawlcheck.io/spec/guard */
|
|
72
|
+
export const ACTION_MAX_LIFETIME_S = { read: 86400, cite: 86400, connect: 3600, transact: 300, administer: 300 };
|
|
73
|
+
/** The most clock skew a receipt may claim (seconds). */
|
|
74
|
+
export const MAX_CLOCK_SKEW_S = 300;
|
|
75
|
+
const DECISIONS = ["allow", "warn", "require_confirmation", "block", "unsupported"];
|
|
76
|
+
/**
|
|
77
|
+
* Is this receipt about the operation you are about to perform? Checks the scope only (kind, domain, action, nonce,
|
|
78
|
+
* time window, per-action lifetime, known decision, resolve binding); the signature and issuer are checked by
|
|
79
|
+
* verifyDocument. guard() runs both. https://crawlcheck.io/spec/guard
|
|
80
|
+
*/
|
|
81
|
+
export function checkDecision(receipt, want) {
|
|
82
|
+
const why = [], r = receipt;
|
|
83
|
+
if (!r || typeof r !== "object" || Array.isArray(r))
|
|
84
|
+
return { ok: false, why: ["no receipt: CrawlCheck sent " + (r === null || r === undefined ? "nothing" : typeof r)] };
|
|
85
|
+
if (r.kind !== "crawlcheck-decision" || r.v !== 1)
|
|
86
|
+
why.push("not a v1 decision receipt (kind " + r.kind + ", v " + r.v + ")");
|
|
87
|
+
const wd = normalizeDomain(want.domain);
|
|
88
|
+
if (!wd || normalizeDomain(r.domain) !== wd)
|
|
89
|
+
why.push("the receipt is for " + r.domain + ", not " + want.domain);
|
|
90
|
+
if (String(r.action) !== String(want.action))
|
|
91
|
+
why.push("the receipt is for action " + r.action + ", not " + want.action);
|
|
92
|
+
if (want.operation && canon(r.operation ?? null) !== canon(want.operation))
|
|
93
|
+
why.push(r.operation ? "the receipt is for another operation (" + canon(r.operation).slice(0, 120) + ")" : "the receipt is not scoped to an operation, and this request named one");
|
|
94
|
+
if (want.nonce && r.nonce !== want.nonce)
|
|
95
|
+
why.push(r.nonce ? "the receipt carries another request's nonce (replayed)" : "the receipt does not carry the nonce this request sent");
|
|
96
|
+
const now = want.now ?? Date.now(), skew = Math.min(Math.max(Number(r.clock_skew_s) || 0, 0), MAX_CLOCK_SKEW_S) * 1000;
|
|
97
|
+
const at = Date.parse(r.decided_at), exp = Date.parse(r.expires_at);
|
|
98
|
+
if (!isFinite(at) || !isFinite(exp))
|
|
99
|
+
why.push("decided_at or expires_at missing");
|
|
100
|
+
else {
|
|
101
|
+
if (now > exp + skew)
|
|
102
|
+
why.push("expired at " + r.expires_at);
|
|
103
|
+
if (at > now + skew)
|
|
104
|
+
why.push("decided in the future (" + r.decided_at + "): check the clock");
|
|
105
|
+
const max = ACTION_MAX_LIFETIME_S[String(want.action)];
|
|
106
|
+
if (max === undefined)
|
|
107
|
+
why.push("unknown action " + want.action);
|
|
108
|
+
else if (exp - at > max * 1000 + 1000)
|
|
109
|
+
why.push("valid for " + Math.round((exp - at) / 1000) + " s; a " + want.action + " receipt may last at most " + max + " s");
|
|
110
|
+
}
|
|
111
|
+
if (DECISIONS.indexOf(r.decision) < 0)
|
|
112
|
+
why.push("unknown decision " + r.decision);
|
|
113
|
+
if (!r.resolve || !/^[0-9a-f]{64}$/.test(String(r.resolve.sha256 || "")))
|
|
114
|
+
why.push("the receipt names no resolve answer");
|
|
115
|
+
return { ok: why.length === 0, why };
|
|
116
|
+
}
|
|
67
117
|
export const LOG_ID = L.LOG_ID;
|
|
68
118
|
/** A new, empty transparency-log store (plain JSON: persist it between runs). */
|
|
69
119
|
export const newLogStore = L.newLogStore;
|
|
@@ -199,29 +249,125 @@ export class CrawlCheck {
|
|
|
199
249
|
b.template = opts.template;
|
|
200
250
|
if (opts.policy)
|
|
201
251
|
b.policy = opts.policy;
|
|
252
|
+
if (opts.nonce)
|
|
253
|
+
b.nonce = opts.nonce;
|
|
254
|
+
if (opts.operation)
|
|
255
|
+
b.operation = opts.operation;
|
|
202
256
|
return this.post("/api/v1/preflight", b);
|
|
203
257
|
}
|
|
204
258
|
/** Verify a signed document here. With onlineKeys (default), its signing key must also be in the published directory. */
|
|
205
259
|
async verifyDocument(doc, onlineKeys = true) {
|
|
206
260
|
return withTrust(await V.verifyDataDoc(doc), doc?.signature?.kid, onlineKeys ? await this.publishedKeyIds() : null);
|
|
207
261
|
}
|
|
208
|
-
/**
|
|
262
|
+
/**
|
|
263
|
+
* Preflight, verify the receipt, check it is for THIS operation, and decide. Proceeds on allow; warn only with allowWarn;
|
|
264
|
+
* require_confirmation only if confirm() says yes; block, unsupported and every failure never (fail closed: an error,
|
|
265
|
+
* an unreachable CrawlCheck, a malformed, foreign, expired, replayed or out-of-scope receipt all return proceed false
|
|
266
|
+
* with why[]). Passes the acceptance suite at https://crawlcheck.io/spec/guard
|
|
267
|
+
*/
|
|
209
268
|
async guard(domain, action = "read", opts = {}) {
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
269
|
+
const nonce = opts.nonce || randomNonce();
|
|
270
|
+
const no = (why, receipt = null, verification = null) => ({ proceed: false, receipt, verification, header: null, why });
|
|
271
|
+
let receipt;
|
|
272
|
+
try {
|
|
273
|
+
receipt = await this.preflight(domain, action, Object.assign({}, opts, { nonce }));
|
|
274
|
+
}
|
|
275
|
+
catch (e) {
|
|
276
|
+
return no(["CrawlCheck did not give a decision: " + (e?.message || e)]);
|
|
214
277
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
if (!
|
|
219
|
-
return
|
|
278
|
+
if (this.log.forks.length)
|
|
279
|
+
return Object.assign(no(["the transparency log has shown this client two histories (split view)"], receipt), { split_view: this.log.forks.slice() });
|
|
280
|
+
const scope = checkDecision(receipt, { domain, action, nonce, now: opts.now, operation: opts.operation });
|
|
281
|
+
if (!receipt || typeof receipt !== "object")
|
|
282
|
+
return no(scope.why);
|
|
283
|
+
const online = opts.onlineKeys ?? true;
|
|
284
|
+
if (!online && !(Array.isArray(opts.publishedKids) && opts.publishedKids.length))
|
|
285
|
+
return no(["onlineKeys is false and no publishedKids were pinned: the signer cannot be checked"], receipt);
|
|
286
|
+
let verification;
|
|
287
|
+
try {
|
|
288
|
+
verification = withTrust(await V.verifyDataDoc(receipt), receipt?.signature?.kid, online ? await this.publishedKeyIds() : opts.publishedKids);
|
|
289
|
+
}
|
|
290
|
+
catch (e) {
|
|
291
|
+
return no(["the receipt could not be verified: " + (e?.message || e)].concat(scope.why), receipt);
|
|
292
|
+
}
|
|
293
|
+
const why = scope.why.slice();
|
|
294
|
+
if (!verification.integrity_valid)
|
|
295
|
+
why.push("integrity failed: " + verification.checks.filter((c) => c.ok === false).map((c) => c.id).join(", "));
|
|
296
|
+
if (verification.issuer_trusted !== true)
|
|
297
|
+
why.push("signed by a key that is not in CrawlCheck's published key directory");
|
|
298
|
+
if (why.length)
|
|
299
|
+
return no(why, receipt, verification);
|
|
220
300
|
const d = receipt.decision;
|
|
221
301
|
let proceed = d === "allow" || (d === "warn" && !!opts.allowWarn);
|
|
222
|
-
if (d === "require_confirmation"
|
|
223
|
-
|
|
224
|
-
|
|
302
|
+
if (d === "require_confirmation") {
|
|
303
|
+
if (opts.confirm) {
|
|
304
|
+
try {
|
|
305
|
+
proceed = (await opts.confirm(receipt)) === true;
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
proceed = false;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (!proceed)
|
|
313
|
+
why.push(d === "warn" ? "decision is warn and allowWarn is not set" : d === "require_confirmation" ? (opts.confirm ? "a human did not confirm" : "decision is require_confirmation and no confirm() was given") : "decision is " + d);
|
|
314
|
+
return { proceed, receipt, verification, header: proceed ? receiptHeader(receipt) : null, why };
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* fetch() with the check where the action runs: guard() before the first request, and again before every redirect hop
|
|
318
|
+
* to a new domain. The site is contacted only after its domain proceeds; a refused hop is never requested. Redirects
|
|
319
|
+
* are followed by hand (GET/HEAD only), to http(s) only, at most maxHops (10). The receipt header is sent only to the
|
|
320
|
+
* domain it names. In a browser a cross-origin redirect hides its target, so it is refused rather than followed blind.
|
|
321
|
+
*/
|
|
322
|
+
async guardedFetch(url, opts = {}) {
|
|
323
|
+
const action = opts.action ?? "read", max = opts.maxHops ?? 10, f = opts.fetch ? opts.fetch.bind(globalThis) : this.f;
|
|
324
|
+
const init = Object.assign({}, opts.init || {}), method = String(init.method || "GET").toUpperCase();
|
|
325
|
+
const hops = [], seen = new Map();
|
|
326
|
+
let cur = url;
|
|
327
|
+
for (let n = 0; n <= max; n++) {
|
|
328
|
+
let u;
|
|
329
|
+
try {
|
|
330
|
+
u = new URL(cur);
|
|
331
|
+
}
|
|
332
|
+
catch {
|
|
333
|
+
return { proceed: false, response: null, url: cur, hops, why: ["not a URL: " + cur] };
|
|
334
|
+
}
|
|
335
|
+
if (u.protocol !== "https:" && u.protocol !== "http:")
|
|
336
|
+
return { proceed: false, response: null, url: cur, hops, why: ["refused a " + u.protocol + " URL"] };
|
|
337
|
+
const dom = normalizeDomain(u.hostname);
|
|
338
|
+
if (!dom)
|
|
339
|
+
return { proceed: false, response: null, url: cur, hops, why: ["not a public domain name: " + u.hostname] };
|
|
340
|
+
const op = { method: method, url: cur }; // op1: each hop's receipt names that exact request
|
|
341
|
+
let g = seen.get(method + " " + cur);
|
|
342
|
+
if (!g) {
|
|
343
|
+
g = await this.guard(dom, action, Object.assign({}, opts, { operation: op }));
|
|
344
|
+
seen.set(method + " " + cur, g);
|
|
345
|
+
}
|
|
346
|
+
const hop = { url: cur, domain: dom, proceed: g.proceed, decision: g.receipt ? String(g.receipt.decision) : null, why: g.why };
|
|
347
|
+
hops.push(hop);
|
|
348
|
+
if (!g.proceed)
|
|
349
|
+
return { proceed: false, response: null, url: cur, hops, why: [dom + ": " + (g.why.join("; ") || "refused")] };
|
|
350
|
+
const h = new Headers(init.headers || {});
|
|
351
|
+
if (g.header)
|
|
352
|
+
for (const [k, v] of Object.entries(g.header))
|
|
353
|
+
h.set(k, v);
|
|
354
|
+
const res = await f(cur, Object.assign({}, init, { headers: h, redirect: "manual" }));
|
|
355
|
+
hop.status = res.status;
|
|
356
|
+
if (res.type === "opaqueredirect")
|
|
357
|
+
return { proceed: false, response: null, url: cur, hops, why: ["the runtime hid the redirect target, so it cannot be checked"] };
|
|
358
|
+
const loc = res.headers.get("location");
|
|
359
|
+
if (![301, 302, 303, 307, 308].includes(res.status) || !loc)
|
|
360
|
+
return { proceed: true, response: res, url: cur, hops, why: [] };
|
|
361
|
+
if (method !== "GET" && method !== "HEAD")
|
|
362
|
+
return { proceed: true, response: res, url: cur, hops, why: ["redirect not followed for " + method] };
|
|
363
|
+
try {
|
|
364
|
+
cur = new URL(loc, cur).toString();
|
|
365
|
+
}
|
|
366
|
+
catch {
|
|
367
|
+
return { proceed: false, response: null, url: cur, hops, why: ["unreadable redirect target " + loc] };
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return { proceed: false, response: null, url: cur, hops, why: ["more than " + max + " redirects"] };
|
|
225
371
|
}
|
|
226
372
|
/** 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. */
|
|
227
373
|
/** Make a signed lockfile for an MCP server (CrawlCheck reads tools/list; no tool is called). Save it; check with checkLock before every connect. */
|
package/dist/mcp.js
CHANGED
|
@@ -64,7 +64,8 @@ export function mcpPreflight(cc, opts = {}) {
|
|
|
64
64
|
state.decidedAt = Date.now();
|
|
65
65
|
if (!g.proceed) {
|
|
66
66
|
const d = g.receipt && g.receipt.decision;
|
|
67
|
-
const
|
|
67
|
+
const decisionOnly = (g.why || []).every(function (w) { return /^decision is|a human did not confirm|no confirm\(\) was given|allowWarn is not set/.test(w); });
|
|
68
|
+
const why = g.split_view ? "the transparency log showed two histories; no answer is trusted" : !g.verification ? ((g.why || []).join("; ") || "no decision") : !g.verification.verified ? "the decision receipt did not verify" : g.verification.accepted === false ? "the receipt was signed with a key CrawlCheck does not publish" : !decisionOnly ? g.why.join("; ") : !g.verification.verified ? "the decision receipt did not verify" : g.verification.accepted === false ? "the receipt was signed with a key CrawlCheck does not publish" : d === "require_confirmation" ? "a human must confirm and none did" : d === "warn" ? "warn without allowWarn" : d === "unsupported" ? "nothing vouches for a connection to " + host + " (" + ((g.receipt.steps || [])[0] || {}).why + ")" : "decision " + d + (g.receipt.steps && g.receipt.steps[0] ? " (" + g.receipt.steps[0].why + ")" : "");
|
|
68
69
|
throw new McpPreflightRefused("connect", why, { receipt: g.receipt, verification: g.verification });
|
|
69
70
|
}
|
|
70
71
|
return g;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crawlcheck/sdk",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.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",
|
|
@@ -42,5 +42,6 @@
|
|
|
42
42
|
],
|
|
43
43
|
"scripts": {
|
|
44
44
|
"test": "node --test test/*.test.js"
|
|
45
|
-
}
|
|
45
|
+
},
|
|
46
|
+
"license": "MIT"
|
|
46
47
|
}
|