@usekaval/kaval
Advanced tools
| /** | ||
| * `check-decision/1.1.0` — THE PUBLISHED DECISION TABLE, EXECUTABLE. | ||
| * | ||
| * A signed receipt exists to be an exhibit. Until this module shipped, a holder could check the | ||
| * SIGNATURE offline but had to run Kaval's server code to re-derive the VERDICT — so a skeptic could | ||
| * fairly answer "to check your verdict I must run your software", which is exactly the objection an | ||
| * appeal packet exists to remove. The table below was already published as documentation; this file | ||
| * is that same table as code, and it is the code the issuer runs. | ||
| * | ||
| * THIS IS THE SINGLE SOURCE OF TRUTH, NOT A COPY OF ONE. | ||
| * `apps/server/src/check/decide.ts` re-exports these bindings; it does not restate them. A | ||
| * reimplementation that drifted would accept verdicts the server never reaches, which is worse than | ||
| * publishing nothing at all, so `apps/server/test/check-decision-shared-source.test.ts` fails if the | ||
| * server's `decideCheck` is ever anything but the function object exported here — and fails again if | ||
| * the receipt-issuing pipeline ever routes around it. | ||
| * | ||
| * COMPATIBILITY SURFACE. Because verifiers a customer runs now execute this table, changing a row | ||
| * changes what those verifiers accept: a receipt issued under a new table would be REFUSED by every | ||
| * deployed verifier that still knows the old one. That is a BREAKING CHANGE, deliberately made | ||
| * expensive — for an audit-grade artifact the decision table *should* be hard to change quietly. The | ||
| * procedure is: bump `CHECK_DECISION_RULE_VERSION`, publish the new table, and keep accepting the | ||
| * old one for receipts that name it. Nothing here may change under the existing version string. | ||
| * | ||
| * ZERO DEPENDENCIES, ON PURPOSE. The enums below are restated rather than imported from | ||
| * `@kaval/contracts`: this package is mirrored into the public Apache-2.0 thin client, where nothing | ||
| * from the private repo exists. `apps/server/src/check/contracts.ts` asserts at COMPILE TIME that | ||
| * its own `Materiality` / `FactStatus` / `FreshnessFailure` are the same sets in both directions, so | ||
| * adding an internal value without publishing it here fails the build rather than a receipt. | ||
| * | ||
| * | # | condition | verdict | | ||
| * |---|----------------------------------------------------------------------------------|---------| | ||
| * | 1 | any high/critical fact is `changed` | BLOCK | | ||
| * | 2 | any uncontested critical fact is `unknown` | BLOCK | | ||
| * | 3 | any contested fact is `unknown` | REVIEW | | ||
| * | 4 | any other changed/unknown/timeout/stale-pending fact, or compile uncertainty | REVIEW | | ||
| * | 5 | every material fact `holds` on a fresh basis | ALLOW | | ||
| * | ||
| * Top-down, FIRST MATCH WINS. Row 4 is vacuously true for an empty fact list; a check that | ||
| * identified no facts at all must therefore set `compilationUncertain` (row 3) rather than rely on | ||
| * the table to fail closed for it. | ||
| */ | ||
| /** Published with every new receipt so its verdict stays re-derivable from the fact list. */ | ||
| export declare const CHECK_DECISION_RULE_VERSION = "check-decision/1.1.0"; | ||
| export declare const CHECK_DECISION_RULE_VERSIONS: readonly ["check-decision/1.0.0", "check-decision/1.1.0"]; | ||
| export type CheckDecisionRuleVersion = (typeof CHECK_DECISION_RULE_VERSIONS)[number]; | ||
| /** The entire taxonomy. Eight codes, no synonyms, no free text. */ | ||
| export declare const CHECK_REASON_CODES: readonly ["ALL_FACTS_HOLD", "FACT_CHANGED", "FACT_EXPIRED", "FACT_UNKNOWN", "SOURCE_UPDATED_PENDING_REVIEW", "SOURCE_UNREACHABLE", "NEW_FACT_UNVERIFIED", "COMPILATION_UNCERTAIN"]; | ||
| export type CheckReasonCode = (typeof CHECK_REASON_CODES)[number]; | ||
| export declare const CHECK_VERDICTS: readonly ["ALLOW", "REVIEW", "BLOCK"]; | ||
| export type CheckVerdict = (typeof CHECK_VERDICTS)[number]; | ||
| /** How a fact's status was obtained. Mirrors the receipt's per-fact `method`. */ | ||
| export declare const CHECK_FACT_METHODS: readonly ["state", "live", "timeout"]; | ||
| export type CheckFactMethod = (typeof CHECK_FACT_METHODS)[number]; | ||
| /** The public, three-valued projection of an internal claim assessment. */ | ||
| export declare const CHECK_FACT_STATES: readonly ["holds", "changed", "unknown"]; | ||
| export type CheckFactState = (typeof CHECK_FACT_STATES)[number]; | ||
| export declare const CHECK_MATERIALITIES: readonly ["low", "medium", "high", "critical"]; | ||
| export type CheckMateriality = (typeof CHECK_MATERIALITIES)[number]; | ||
| /** Why a stored fact state could not be served, published as part of the receipt. */ | ||
| export declare const CHECK_FRESHNESS_FAILURES: readonly ["stale", "dormant", "basis_superseded", "source_unreachable", "ttl_expired"]; | ||
| export type CheckFreshnessFailure = (typeof CHECK_FRESHNESS_FAILURES)[number]; | ||
| export interface CheckDecisionFact { | ||
| materiality: CheckMateriality; | ||
| status: CheckFactState; | ||
| /** How the status was obtained. `timeout` means the live path ran out of `max_wait_ms`. */ | ||
| method: CheckFactMethod; | ||
| /** Internal temporal state when known — the only thing that distinguishes EXPIRED from CHANGED. */ | ||
| temporalState?: string | null | undefined; | ||
| /** Why a stored row could not be served. Drives SOURCE_UNREACHABLE. */ | ||
| freshnessFailure?: CheckFreshnessFailure | undefined; | ||
| /** A known fact whose basis moved and whose re-evaluation has not finished yet. */ | ||
| stalePending?: boolean | undefined; | ||
| /** True when no stored state existed and the live path did not establish one. */ | ||
| novel?: boolean | undefined; | ||
| /** True when at least one recorded source contests the conclusion's backing. */ | ||
| contested?: boolean | undefined; | ||
| } | ||
| export interface CheckDecisionOptions { | ||
| /** Claim extraction failed or produced nothing usable for the submitted action. */ | ||
| compilationUncertain?: boolean | undefined; | ||
| /** Omit for the latest table. Receipt projection sets the version named by the artifact. */ | ||
| ruleVersion?: CheckDecisionRuleVersion | undefined; | ||
| } | ||
| export interface CheckDecision { | ||
| verdict: CheckVerdict; | ||
| reason_codes: CheckReasonCode[]; | ||
| decision_rule_version: string; | ||
| } | ||
| export declare function decideCheck(facts: readonly CheckDecisionFact[], options?: CheckDecisionOptions): CheckDecision; | ||
| /** A receipt that predates the discriminators cannot be re-derived; it is refused, never guessed. */ | ||
| export declare class ReceiptNotSelfDerivableError extends Error { | ||
| readonly reason: string; | ||
| constructor(reason: string); | ||
| } | ||
| /** | ||
| * Project a signed receipt back onto `decideCheck`'s inputs — the whole product claim, executable. | ||
| * | ||
| * Feed the result to `decideCheck` and it must reproduce the receipt's own `decision` and | ||
| * `reason_codes` exactly. Nothing else may be consulted: not a database, not the response body, and | ||
| * emphatically not `receipt.reason_codes` itself (reading the answer off the answer would make the | ||
| * round trip vacuous). Anyone holding the JSON can do this with the published decision table. | ||
| * | ||
| * The parameter is `unknown` because a verifier's input is a document someone handed it, not a value | ||
| * a type system already vouched for. Every field the table reads is checked here, and the ONLY | ||
| * permitted outcomes are a complete decision input or a throw. | ||
| * | ||
| * FAILS CLOSED on an under-specified receipt. A document missing the discriminators would re-derive | ||
| * a *more permissive* verdict than was issued — a stale-pending REVIEW would read as ALLOW — so it | ||
| * is refused outright. Silently producing the wrong verdict is the one outcome an auditor cannot | ||
| * detect, which makes it the one outcome this function must never have. The same reasoning covers a | ||
| * value outside a published enum: an unrecognised `materiality` is not "not blocking", it is a | ||
| * document this table cannot answer for. | ||
| */ | ||
| export declare function checkDecisionInputFromReceipt(receipt: unknown): { | ||
| facts: CheckDecisionFact[]; | ||
| options: CheckDecisionOptions; | ||
| }; | ||
| /** | ||
| * The one-call form: hand it a receipt, get back the verdict its own facts imply. | ||
| * | ||
| * This never reads `receipt.decision` or `receipt.reason_codes`. Comparing the answer to those two | ||
| * fields is the caller's job — and is what `verifyReceipt(..., { derive_verdict: true })` does. | ||
| */ | ||
| export declare function deriveCheckDecision(receipt: unknown): CheckDecision; |
| /** | ||
| * `check-decision/1.1.0` — THE PUBLISHED DECISION TABLE, EXECUTABLE. | ||
| * | ||
| * A signed receipt exists to be an exhibit. Until this module shipped, a holder could check the | ||
| * SIGNATURE offline but had to run Kaval's server code to re-derive the VERDICT — so a skeptic could | ||
| * fairly answer "to check your verdict I must run your software", which is exactly the objection an | ||
| * appeal packet exists to remove. The table below was already published as documentation; this file | ||
| * is that same table as code, and it is the code the issuer runs. | ||
| * | ||
| * THIS IS THE SINGLE SOURCE OF TRUTH, NOT A COPY OF ONE. | ||
| * `apps/server/src/check/decide.ts` re-exports these bindings; it does not restate them. A | ||
| * reimplementation that drifted would accept verdicts the server never reaches, which is worse than | ||
| * publishing nothing at all, so `apps/server/test/check-decision-shared-source.test.ts` fails if the | ||
| * server's `decideCheck` is ever anything but the function object exported here — and fails again if | ||
| * the receipt-issuing pipeline ever routes around it. | ||
| * | ||
| * COMPATIBILITY SURFACE. Because verifiers a customer runs now execute this table, changing a row | ||
| * changes what those verifiers accept: a receipt issued under a new table would be REFUSED by every | ||
| * deployed verifier that still knows the old one. That is a BREAKING CHANGE, deliberately made | ||
| * expensive — for an audit-grade artifact the decision table *should* be hard to change quietly. The | ||
| * procedure is: bump `CHECK_DECISION_RULE_VERSION`, publish the new table, and keep accepting the | ||
| * old one for receipts that name it. Nothing here may change under the existing version string. | ||
| * | ||
| * ZERO DEPENDENCIES, ON PURPOSE. The enums below are restated rather than imported from | ||
| * `@kaval/contracts`: this package is mirrored into the public Apache-2.0 thin client, where nothing | ||
| * from the private repo exists. `apps/server/src/check/contracts.ts` asserts at COMPILE TIME that | ||
| * its own `Materiality` / `FactStatus` / `FreshnessFailure` are the same sets in both directions, so | ||
| * adding an internal value without publishing it here fails the build rather than a receipt. | ||
| * | ||
| * | # | condition | verdict | | ||
| * |---|----------------------------------------------------------------------------------|---------| | ||
| * | 1 | any high/critical fact is `changed` | BLOCK | | ||
| * | 2 | any uncontested critical fact is `unknown` | BLOCK | | ||
| * | 3 | any contested fact is `unknown` | REVIEW | | ||
| * | 4 | any other changed/unknown/timeout/stale-pending fact, or compile uncertainty | REVIEW | | ||
| * | 5 | every material fact `holds` on a fresh basis | ALLOW | | ||
| * | ||
| * Top-down, FIRST MATCH WINS. Row 4 is vacuously true for an empty fact list; a check that | ||
| * identified no facts at all must therefore set `compilationUncertain` (row 3) rather than rely on | ||
| * the table to fail closed for it. | ||
| */ | ||
| /** Published with every new receipt so its verdict stays re-derivable from the fact list. */ | ||
| export const CHECK_DECISION_RULE_VERSION = "check-decision/1.1.0"; | ||
| export const CHECK_DECISION_RULE_VERSIONS = [ | ||
| "check-decision/1.0.0", | ||
| CHECK_DECISION_RULE_VERSION, | ||
| ]; | ||
| /** The entire taxonomy. Eight codes, no synonyms, no free text. */ | ||
| export const CHECK_REASON_CODES = [ | ||
| "ALL_FACTS_HOLD", | ||
| "FACT_CHANGED", | ||
| "FACT_EXPIRED", | ||
| "FACT_UNKNOWN", | ||
| "SOURCE_UPDATED_PENDING_REVIEW", | ||
| "SOURCE_UNREACHABLE", | ||
| "NEW_FACT_UNVERIFIED", | ||
| "COMPILATION_UNCERTAIN", | ||
| ]; | ||
| export const CHECK_VERDICTS = ["ALLOW", "REVIEW", "BLOCK"]; | ||
| /** How a fact's status was obtained. Mirrors the receipt's per-fact `method`. */ | ||
| export const CHECK_FACT_METHODS = ["state", "live", "timeout"]; | ||
| /** The public, three-valued projection of an internal claim assessment. */ | ||
| export const CHECK_FACT_STATES = ["holds", "changed", "unknown"]; | ||
| export const CHECK_MATERIALITIES = [ | ||
| "low", | ||
| "medium", | ||
| "high", | ||
| "critical", | ||
| ]; | ||
| /** Why a stored fact state could not be served, published as part of the receipt. */ | ||
| export const CHECK_FRESHNESS_FAILURES = [ | ||
| "stale", | ||
| "dormant", | ||
| "basis_superseded", | ||
| "source_unreachable", | ||
| "ttl_expired", | ||
| ]; | ||
| function isBlockingMateriality(materiality) { | ||
| return materiality === "high" || materiality === "critical"; | ||
| } | ||
| /** | ||
| * Reason codes are evidence, not commentary: every code emitted must be traceable to a fact row (or | ||
| * to compilation) in the same receipt. Order is stable — table order, then fact order — so two | ||
| * identical decision inputs produce the same reason-code order. Receipt IDs and times remain unique. | ||
| */ | ||
| function reasonCodes(facts, verdict, compilationUncertain) { | ||
| if (verdict === "ALLOW") | ||
| return ["ALL_FACTS_HOLD"]; | ||
| const codes = new Set(); | ||
| if (compilationUncertain) | ||
| codes.add("COMPILATION_UNCERTAIN"); | ||
| for (const fact of facts) { | ||
| if (fact.status === "changed") { | ||
| codes.add(fact.temporalState === "expired" ? "FACT_EXPIRED" : "FACT_CHANGED"); | ||
| continue; | ||
| } | ||
| if (fact.stalePending === true) { | ||
| codes.add("SOURCE_UPDATED_PENDING_REVIEW"); | ||
| continue; | ||
| } | ||
| if (fact.status === "unknown") { | ||
| if (fact.freshnessFailure === "source_unreachable") | ||
| codes.add("SOURCE_UNREACHABLE"); | ||
| else if (fact.novel === true || fact.method === "timeout") | ||
| codes.add("NEW_FACT_UNVERIFIED"); | ||
| else | ||
| codes.add("FACT_UNKNOWN"); | ||
| } | ||
| } | ||
| // A non-ALLOW verdict always names at least one cause. The only way to reach here with nothing | ||
| // recorded is a `holds` fact that timed out mid-revalidation, which is exactly UNKNOWN-shaped. | ||
| if (codes.size === 0) | ||
| codes.add("FACT_UNKNOWN"); | ||
| return [...codes]; | ||
| } | ||
| function decideCheckForVersion(facts, options, version) { | ||
| const compilationUncertain = options.compilationUncertain === true; | ||
| const verdict = (() => { | ||
| // Row 1 — a material fact that CHANGED is the whole product; it blocks before anything else. | ||
| if (facts.some((fact) => fact.status === "changed" && isBlockingMateriality(fact.materiality))) { | ||
| return "BLOCK"; | ||
| } | ||
| // Version 1.0.0 blocked every critical unknown. Version 1.1.0 stops a named contest at REVIEW. | ||
| if (facts.some((fact) => fact.status === "unknown" && | ||
| fact.materiality === "critical" && | ||
| (version === "check-decision/1.0.0" || fact.contested !== true))) { | ||
| return "BLOCK"; | ||
| } | ||
| // Rows 3 and 4 — a named evidence contest stops at REVIEW, regardless of materiality. | ||
| if (compilationUncertain || | ||
| facts.some((fact) => fact.status !== "holds" || | ||
| fact.method === "timeout" || | ||
| fact.stalePending === true)) { | ||
| return "REVIEW"; | ||
| } | ||
| // Row 4. | ||
| return "ALLOW"; | ||
| })(); | ||
| return { | ||
| verdict, | ||
| reason_codes: reasonCodes(facts, verdict, compilationUncertain), | ||
| decision_rule_version: version, | ||
| }; | ||
| } | ||
| export function decideCheck(facts, options = {}) { | ||
| return decideCheckForVersion(facts, options, options.ruleVersion ?? CHECK_DECISION_RULE_VERSION); | ||
| } | ||
| /* ------------------------------------------------------------------ * | ||
| * Offline re-derivation * | ||
| * ------------------------------------------------------------------ */ | ||
| /** A receipt that predates the discriminators cannot be re-derived; it is refused, never guessed. */ | ||
| export class ReceiptNotSelfDerivableError extends Error { | ||
| reason; | ||
| constructor(reason) { | ||
| super(`this receipt cannot be re-derived offline: ${reason}`); | ||
| this.reason = reason; | ||
| this.name = "ReceiptNotSelfDerivableError"; | ||
| } | ||
| } | ||
| function objectOrNull(value) { | ||
| return value !== null && typeof value === "object" && !Array.isArray(value) | ||
| ? value | ||
| : null; | ||
| } | ||
| function memberOf(values, value) { | ||
| return (typeof value === "string" && values.includes(value)); | ||
| } | ||
| /** | ||
| * Project a signed receipt back onto `decideCheck`'s inputs — the whole product claim, executable. | ||
| * | ||
| * Feed the result to `decideCheck` and it must reproduce the receipt's own `decision` and | ||
| * `reason_codes` exactly. Nothing else may be consulted: not a database, not the response body, and | ||
| * emphatically not `receipt.reason_codes` itself (reading the answer off the answer would make the | ||
| * round trip vacuous). Anyone holding the JSON can do this with the published decision table. | ||
| * | ||
| * The parameter is `unknown` because a verifier's input is a document someone handed it, not a value | ||
| * a type system already vouched for. Every field the table reads is checked here, and the ONLY | ||
| * permitted outcomes are a complete decision input or a throw. | ||
| * | ||
| * FAILS CLOSED on an under-specified receipt. A document missing the discriminators would re-derive | ||
| * a *more permissive* verdict than was issued — a stale-pending REVIEW would read as ALLOW — so it | ||
| * is refused outright. Silently producing the wrong verdict is the one outcome an auditor cannot | ||
| * detect, which makes it the one outcome this function must never have. The same reasoning covers a | ||
| * value outside a published enum: an unrecognised `materiality` is not "not blocking", it is a | ||
| * document this table cannot answer for. | ||
| */ | ||
| export function checkDecisionInputFromReceipt(receipt) { | ||
| const document = objectOrNull(receipt); | ||
| if (document === null) { | ||
| throw new ReceiptNotSelfDerivableError("it is not a JSON object"); | ||
| } | ||
| if (typeof document["compilation_uncertain"] !== "boolean") { | ||
| throw new ReceiptNotSelfDerivableError("it does not state whether compilation was uncertain"); | ||
| } | ||
| const factList = document["facts"]; | ||
| if (!Array.isArray(factList)) { | ||
| throw new ReceiptNotSelfDerivableError("it does not carry a fact list"); | ||
| } | ||
| const ruleVersion = document["decision_rule_version"]; | ||
| const facts = factList.map((entry, index) => { | ||
| const fact = objectOrNull(entry); | ||
| if (fact === null) | ||
| throw new ReceiptNotSelfDerivableError(`fact ${index} is not a JSON object`); | ||
| if (typeof fact["stale_pending"] !== "boolean" || | ||
| typeof fact["novel"] !== "boolean") { | ||
| throw new ReceiptNotSelfDerivableError(`fact ${index} omits the stale-pending/novel discriminators`); | ||
| } | ||
| // `temporal_state` is what separates FACT_EXPIRED from FACT_CHANGED, so an ABSENT one would | ||
| // silently downgrade the code rather than fail. `null` is a legitimate value (the state was | ||
| // never known); `undefined` is a document that cannot answer the question. | ||
| if (fact["temporal_state"] === undefined) { | ||
| throw new ReceiptNotSelfDerivableError(`fact ${index} omits its temporal state`); | ||
| } | ||
| // An unrecognised failure reason must not degrade to "no failure": that is exactly how a | ||
| // SOURCE_UNREACHABLE would quietly re-derive as the milder FACT_UNKNOWN. | ||
| const freshnessFailure = fact["freshness_failure"]; | ||
| if (freshnessFailure !== null && | ||
| !memberOf(CHECK_FRESHNESS_FAILURES, freshnessFailure)) { | ||
| throw new ReceiptNotSelfDerivableError(`fact ${index} carries a freshness failure this contract does not define`); | ||
| } | ||
| // The two inputs rows 1-4 branch on. A `materiality` or `state` this table does not define | ||
| // would fall through every row into the permissive direction, so it is refused instead. | ||
| const materiality = fact["materiality"]; | ||
| if (!memberOf(CHECK_MATERIALITIES, materiality)) { | ||
| throw new ReceiptNotSelfDerivableError(`fact ${index} carries a materiality this contract does not define`); | ||
| } | ||
| const state = fact["state"]; | ||
| if (!memberOf(CHECK_FACT_STATES, state)) { | ||
| throw new ReceiptNotSelfDerivableError(`fact ${index} carries a state this contract does not define`); | ||
| } | ||
| const method = fact["method"]; | ||
| if (!memberOf(CHECK_FACT_METHODS, method)) { | ||
| throw new ReceiptNotSelfDerivableError(`fact ${index} carries a method this contract does not define`); | ||
| } | ||
| const temporalState = fact["temporal_state"]; | ||
| if (temporalState !== null && typeof temporalState !== "string") { | ||
| throw new ReceiptNotSelfDerivableError(`fact ${index} carries a non-textual temporal state`); | ||
| } | ||
| // Older receipts did not publish semantic roles. They remain derivable as non-contested. | ||
| const basis = Array.isArray(fact["basis"]) ? fact["basis"] : []; | ||
| const contested = basis.some((entry) => objectOrNull(entry)?.["role"] === "contesting"); | ||
| return { | ||
| materiality, | ||
| status: state, | ||
| method, | ||
| temporalState, | ||
| stalePending: fact["stale_pending"], | ||
| novel: fact["novel"], | ||
| ...(contested ? { contested: true } : {}), | ||
| ...(freshnessFailure === null ? {} : { freshnessFailure }), | ||
| }; | ||
| }); | ||
| if (!memberOf(CHECK_DECISION_RULE_VERSIONS, ruleVersion)) { | ||
| throw new ReceiptNotSelfDerivableError(typeof ruleVersion === "string" | ||
| ? `it names unsupported decision rule ${ruleVersion}` | ||
| : "it names no decision rule version"); | ||
| } | ||
| return { | ||
| facts, | ||
| options: { | ||
| compilationUncertain: document["compilation_uncertain"], | ||
| ruleVersion, | ||
| }, | ||
| }; | ||
| } | ||
| /** | ||
| * The one-call form: hand it a receipt, get back the verdict its own facts imply. | ||
| * | ||
| * This never reads `receipt.decision` or `receipt.reason_codes`. Comparing the answer to those two | ||
| * fields is the caller's job — and is what `verifyReceipt(..., { derive_verdict: true })` does. | ||
| */ | ||
| export function deriveCheckDecision(receipt) { | ||
| const { facts, options } = checkDecisionInputFromReceipt(receipt); | ||
| return decideCheck(facts, options); | ||
| } |
@@ -18,2 +18,4 @@ #!/usr/bin/env node | ||
| --require-fresh Exit non-zero unless freshness is "fresh" | ||
| --derive-verdict Also re-derive a check receipt's ALLOW/REVIEW/BLOCK from its own | ||
| facts using the published decision table, and require it to match | ||
| --allow-http-loopback Permit http://localhost/127.0.0.0/8/::1 for local development | ||
@@ -24,3 +26,4 @@ --compact Emit compact JSON | ||
| Exit status 0 means the signature is valid and the key is trusted. Freshness is reported | ||
| separately unless --require-fresh is supplied. | ||
| separately unless --require-fresh is supplied. With --derive-verdict the verdict must also | ||
| re-derive from the receipt's own facts, and the "decision" block explains any mismatch. | ||
| `; | ||
@@ -42,2 +45,3 @@ function argumentError(message) { | ||
| requireFresh: false, | ||
| deriveVerdict: false, | ||
| allowHttpLoopback: false, | ||
@@ -50,2 +54,4 @@ compact: false, | ||
| result.requireFresh = true; | ||
| else if (flag === "--derive-verdict") | ||
| result.deriveVerdict = true; | ||
| else if (flag === "--allow-http-loopback") | ||
@@ -131,2 +137,3 @@ result.allowHttpLoopback = true; | ||
| ...(args.at ? { at: args.at } : {}), | ||
| ...(args.deriveVerdict ? { derive_verdict: true } : {}), | ||
| }); | ||
@@ -133,0 +140,0 @@ print(result, args.compact); |
@@ -6,6 +6,14 @@ /** | ||
| * no `node:https`, no sockets. It reads a receipt and a key document you already hold and answers | ||
| * three separate questions — is the Ed25519 signature over the exact canonical bytes, is the key | ||
| * trusted, and is the receipt fresh at the instant you name. That is the whole point of the | ||
| * subpath, and `test/verify/no-network.test.ts` holds the import graph to it. | ||
| * four separate questions — is the Ed25519 signature over the exact canonical bytes, is the key | ||
| * trusted, is the receipt fresh at the instant you name, and (only when you pass | ||
| * `derive_verdict: true`) does the receipt's stated ALLOW/REVIEW/BLOCK actually follow from the | ||
| * facts the receipt itself carries. That is the whole point of the subpath, and | ||
| * `test/verify/no-network.test.ts` holds the import graph to it. | ||
| * | ||
| * The fourth question is what makes a receipt an appeal packet rather than a signed assertion. The | ||
| * decision table it runs is `./decision.js` — `check-decision/1.1.0`, the same table Kaval's issuer | ||
| * executes, carried here so a holder never has to run Kaval's software to check Kaval's verdict. | ||
| * `decision.ts` is a mirrored copy of the issuer's; `test/verify/mirror-pin.test.ts` pins the shared | ||
| * conformance vectors that make the two answer identically. | ||
| * | ||
| * `verifyWebhookSignature` is here for the same reason: authenticating an inbound `fact_state.delta` | ||
@@ -21,6 +29,7 @@ * is a pure HMAC over bytes you were handed, and a receiver that had to reach the network to decide | ||
| export { canonicalUnsignedReceiptBytes, canonicalUnsignedReceiptJson, MAX_JSON_NUMBER_CHARACTERS, parseJsonStrict, stableCanonicalJson, } from "./canonicalize.js"; | ||
| export { CHECK_DECISION_RULE_VERSION, CHECK_DECISION_RULE_VERSIONS, CHECK_FACT_METHODS, CHECK_FACT_STATES, CHECK_FRESHNESS_FAILURES, CHECK_MATERIALITIES, CHECK_REASON_CODES, CHECK_VERDICTS, ReceiptNotSelfDerivableError, checkDecisionInputFromReceipt, decideCheck, deriveCheckDecision, type CheckDecision, type CheckDecisionFact, type CheckDecisionOptions, type CheckDecisionRuleVersion, type CheckFactMethod, type CheckFactState, type CheckFreshnessFailure, type CheckMateriality, type CheckReasonCode, type CheckVerdict, } from "./decision.js"; | ||
| export { parseVerificationKey, verificationKeyFromDocument, } from "./key-document.js"; | ||
| export { isRfc3339Timestamp, parseRfc3339Instant, rfc3339TimestampMilliseconds, rfc3339TimestampNanoseconds, type Rfc3339Instant, } from "./rfc3339.js"; | ||
| export { KAVAL_CANONICALIZATION, type FreshnessStatus, type JsonValue, type KeyLifecycle, type KeyLifecycleStatus, type VerificationKey, type VerificationResult, type VerifyOptions, } from "./types.js"; | ||
| export { KAVAL_CANONICALIZATION, type FreshnessStatus, type JsonValue, type KeyLifecycle, type KeyLifecycleStatus, type VerificationDecision, type VerificationKey, type VerificationResult, type VerificationScope, type VerifyOptions, } from "./types.js"; | ||
| export { extractReceipt, verifyReceipt, verifyReceiptText } from "./verify.js"; | ||
| export { DEFAULT_WEBHOOK_TOLERANCE_SECONDS, verifyWebhookSignature, WEBHOOK_SIGNATURE_VERSION, WEBHOOK_SIGNED_CONTENT, type VerifyWebhookSignatureInput, type WebhookHeaderSource, type WebhookRejectionReason, type WebhookSignatureAccepted, type WebhookSignatureRejected, type WebhookSignatureResult, } from "./webhook.js"; |
+12
-3
@@ -6,6 +6,14 @@ /** | ||
| * no `node:https`, no sockets. It reads a receipt and a key document you already hold and answers | ||
| * three separate questions — is the Ed25519 signature over the exact canonical bytes, is the key | ||
| * trusted, and is the receipt fresh at the instant you name. That is the whole point of the | ||
| * subpath, and `test/verify/no-network.test.ts` holds the import graph to it. | ||
| * four separate questions — is the Ed25519 signature over the exact canonical bytes, is the key | ||
| * trusted, is the receipt fresh at the instant you name, and (only when you pass | ||
| * `derive_verdict: true`) does the receipt's stated ALLOW/REVIEW/BLOCK actually follow from the | ||
| * facts the receipt itself carries. That is the whole point of the subpath, and | ||
| * `test/verify/no-network.test.ts` holds the import graph to it. | ||
| * | ||
| * The fourth question is what makes a receipt an appeal packet rather than a signed assertion. The | ||
| * decision table it runs is `./decision.js` — `check-decision/1.1.0`, the same table Kaval's issuer | ||
| * executes, carried here so a holder never has to run Kaval's software to check Kaval's verdict. | ||
| * `decision.ts` is a mirrored copy of the issuer's; `test/verify/mirror-pin.test.ts` pins the shared | ||
| * conformance vectors that make the two answer identically. | ||
| * | ||
| * `verifyWebhookSignature` is here for the same reason: authenticating an inbound `fact_state.delta` | ||
@@ -21,2 +29,3 @@ * is a pure HMAC over bytes you were handed, and a receiver that had to reach the network to decide | ||
| export { canonicalUnsignedReceiptBytes, canonicalUnsignedReceiptJson, MAX_JSON_NUMBER_CHARACTERS, parseJsonStrict, stableCanonicalJson, } from "./canonicalize.js"; | ||
| export { CHECK_DECISION_RULE_VERSION, CHECK_DECISION_RULE_VERSIONS, CHECK_FACT_METHODS, CHECK_FACT_STATES, CHECK_FRESHNESS_FAILURES, CHECK_MATERIALITIES, CHECK_REASON_CODES, CHECK_VERDICTS, ReceiptNotSelfDerivableError, checkDecisionInputFromReceipt, decideCheck, deriveCheckDecision, } from "./decision.js"; | ||
| export { parseVerificationKey, verificationKeyFromDocument, } from "./key-document.js"; | ||
@@ -23,0 +32,0 @@ export { isRfc3339Timestamp, parseRfc3339Instant, rfc3339TimestampMilliseconds, rfc3339TimestampNanoseconds, } from "./rfc3339.js"; |
@@ -0,1 +1,2 @@ | ||
| import type { CheckDecision } from "./decision.js"; | ||
| export declare const KAVAL_CANONICALIZATION: "kaval-stable-json-v1"; | ||
@@ -27,5 +28,41 @@ export type JsonPrimitive = null | boolean | number | string; | ||
| export type FreshnessStatus = "fresh" | "recheck_due" | "expired" | "not_yet_issued" | "unknown"; | ||
| /** | ||
| * What a verification result actually covers, stated in the result itself. | ||
| * | ||
| * `signature_envelope` — the historical and still the DEFAULT answer: the Ed25519 signature over the | ||
| * canonical unsigned bytes, key trust, and timing. It deliberately says nothing about the verdict. | ||
| * | ||
| * `signature_envelope+decision_table` — the caller passed `derive_verdict: true`, so the result | ||
| * additionally re-derives the receipt's verdict from the receipt's own facts using the published | ||
| * `check-decision` table (see `decision.ts`) and compares it with the verdict the receipt states. | ||
| * | ||
| * Widening this union is additive by construction: a caller that never opts in never observes the | ||
| * second value, so `scope === "signature_envelope"` keeps meaning exactly what it always meant. | ||
| */ | ||
| export type VerificationScope = "signature_envelope" | "signature_envelope+decision_table"; | ||
| /** | ||
| * The verdict half of a verification, present only when `derive_verdict: true` was requested. | ||
| * | ||
| * `derived` is computed from `facts[]` and `compilation_uncertain` ALONE. The receipt's own | ||
| * `decision` / `reason_codes` are reported as `stated` and are never an input — reading the answer | ||
| * off the answer would make the comparison vacuous. `matches` is the comparison, and it is the only | ||
| * field that may move `accepted`. | ||
| */ | ||
| export interface VerificationDecision { | ||
| /** The decision-table version this verifier executes. */ | ||
| supported_rule_version: string; | ||
| /** The decision-table version the receipt names, when it names one. */ | ||
| receipt_rule_version?: string; | ||
| stated?: { | ||
| verdict?: string; | ||
| reason_codes?: string[]; | ||
| }; | ||
| derived?: CheckDecision; | ||
| /** True only when a verdict AND its reason-code set were re-derived and both agree. */ | ||
| matches: boolean; | ||
| error?: string; | ||
| } | ||
| export interface VerificationResult { | ||
| contract_version: "1"; | ||
| scope: "signature_envelope"; | ||
| scope: VerificationScope; | ||
| accepted: boolean; | ||
@@ -69,5 +106,19 @@ format: { | ||
| }; | ||
| /** Present only when `derive_verdict: true` was requested. Additive; see `VerificationScope`. */ | ||
| decision?: VerificationDecision; | ||
| } | ||
| export interface VerifyOptions { | ||
| at?: Date | number | string; | ||
| /** | ||
| * Also re-derive the receipt's verdict from its own facts and require it to match. | ||
| * | ||
| * OFF BY DEFAULT, and that default is a compatibility guarantee, not timidity: turning it on | ||
| * changes `scope`, adds a `decision` block, and can turn a signature-valid result into | ||
| * `accepted: false`. Every caller written before this option existed keeps the exact result shape | ||
| * and the exact acceptance semantics it was written against. | ||
| * | ||
| * Only `POST /v1/check` receipts carry a fact list. A ProofPacket has none, so asking for | ||
| * re-derivation on one fails closed rather than pretending the question was answered. | ||
| */ | ||
| derive_verdict?: boolean; | ||
| } |
@@ -10,6 +10,13 @@ import { type VerificationResult, type VerifyOptions } from "./types.js"; | ||
| * | ||
| * This intentionally validates only the signature envelope. Full ProofPacket schema validation is | ||
| * the issuer/application's job and must not be confused with cryptographic verification. | ||
| * By default this validates only the SIGNATURE ENVELOPE — `scope: "signature_envelope"` — and says | ||
| * nothing about what the receipt concluded. Pass `derive_verdict: true` and it also re-derives the | ||
| * verdict from the receipt's own facts through the published `check-decision` table and requires the | ||
| * two to agree; `scope` then reads `"signature_envelope+decision_table"` and a `decision` block is | ||
| * present. The default is off so that every caller written before that option existed keeps its | ||
| * result shape and its acceptance semantics unchanged. | ||
| * | ||
| * Neither scope is full ProofPacket schema validation: that remains the issuer/application's job and | ||
| * must not be confused with cryptographic verification. | ||
| */ | ||
| export declare function verifyReceipt(receiptValue: unknown, keyDocument: unknown, options?: VerifyOptions): VerificationResult; | ||
| export declare function verifyReceiptText(receiptJson: string, keyDocumentJson: string, options?: VerifyOptions): VerificationResult; |
+145
-20
| import { createHash, createPublicKey, verify as verifySignature, } from "node:crypto"; | ||
| import { canonicalUnsignedReceiptBytes, parseJsonStrict, } from "./canonicalize.js"; | ||
| import { CHECK_DECISION_RULE_VERSION, CHECK_DECISION_RULE_VERSIONS, ReceiptNotSelfDerivableError, deriveCheckDecision, } from "./decision.js"; | ||
| import { decodeCanonicalBase64Url, verificationKeyFromDocument, } from "./key-document.js"; | ||
@@ -125,6 +126,106 @@ import { parseRfc3339Instant } from "./rfc3339.js"; | ||
| } | ||
| function malformedResult(receipt, at, error) { | ||
| /* ------------------------------------------------------------------ * | ||
| * Verdict re-derivation (opt-in; see VerifyOptions.derive_verdict) * | ||
| * ------------------------------------------------------------------ */ | ||
| function scopeFor(derive) { | ||
| return derive ? "signature_envelope+decision_table" : "signature_envelope"; | ||
| } | ||
| function statedDecision(receipt) { | ||
| const codes = receipt["reason_codes"]; | ||
| const stated = { | ||
| ...(typeof receipt["decision"] === "string" | ||
| ? { verdict: receipt["decision"] } | ||
| : {}), | ||
| ...(Array.isArray(codes) && codes.every((code) => typeof code === "string") | ||
| ? { reason_codes: [...codes] } | ||
| : {}), | ||
| }; | ||
| return Object.keys(stated).length === 0 ? undefined : stated; | ||
| } | ||
| /** Set equality: the codes are what carry meaning, their array order is only determinism. */ | ||
| function sameCodes(stated, derived) { | ||
| const left = [...stated].sort(); | ||
| const right = [...derived].sort(); | ||
| return (left.length === right.length && | ||
| left.every((code, index) => code === right[index])); | ||
| } | ||
| /** | ||
| * Re-derive the receipt's verdict from its own facts and compare with what it claims. | ||
| * | ||
| * Every exit that is not a clean match sets `matches: false`, because a verifier that could not | ||
| * answer the question must never read as one that answered it favourably. A receipt decided under a | ||
| * decision-table version this build does not implement is refused for that reason and named: it is | ||
| * not evidence of tampering, and pretending to re-derive it across a rule change would be worse than | ||
| * saying so. | ||
| */ | ||
| function decisionResult(receipt) { | ||
| const supported = { supported_rule_version: CHECK_DECISION_RULE_VERSION }; | ||
| if (receipt === null) { | ||
| return { | ||
| ...supported, | ||
| matches: false, | ||
| error: "receipt is not a JSON object", | ||
| }; | ||
| } | ||
| const ruleVersion = receipt["decision_rule_version"]; | ||
| const stated = statedDecision(receipt); | ||
| const head = { | ||
| ...supported, | ||
| ...(typeof ruleVersion === "string" | ||
| ? { receipt_rule_version: ruleVersion } | ||
| : {}), | ||
| ...(stated === undefined ? {} : { stated }), | ||
| matches: false, | ||
| }; | ||
| if (typeof ruleVersion !== "string") { | ||
| return { ...head, error: "receipt names no decision rule version" }; | ||
| } | ||
| if (!CHECK_DECISION_RULE_VERSIONS.includes(ruleVersion)) { | ||
| return { | ||
| ...head, | ||
| error: `receipt was decided under ${ruleVersion}, which this verifier does not implement`, | ||
| }; | ||
| } | ||
| let derived; | ||
| try { | ||
| derived = deriveCheckDecision(receipt); | ||
| } | ||
| catch (error) { | ||
| return { | ||
| ...head, | ||
| error: error instanceof ReceiptNotSelfDerivableError | ||
| ? error.message | ||
| : `verdict re-derivation failed: ${error.message}`, | ||
| }; | ||
| } | ||
| if (stated?.verdict === undefined || stated.reason_codes === undefined) { | ||
| return { | ||
| ...head, | ||
| derived, | ||
| error: "receipt states no verdict to compare against", | ||
| }; | ||
| } | ||
| if (stated.verdict !== derived.verdict) { | ||
| return { | ||
| ...head, | ||
| derived, | ||
| error: `receipt states ${stated.verdict} but its own facts re-derive as ${derived.verdict}`, | ||
| }; | ||
| } | ||
| if (!sameCodes(stated.reason_codes, derived.reason_codes)) { | ||
| return { | ||
| ...head, | ||
| derived, | ||
| error: `receipt states reason codes [${stated.reason_codes.join(", ")}] but its own facts re-derive [${derived.reason_codes.join(", ")}]`, | ||
| }; | ||
| } | ||
| return { ...head, derived, matches: true }; | ||
| } | ||
| function decisionFields(receipt, derive) { | ||
| return derive ? { decision: decisionResult(receipt) } : {}; | ||
| } | ||
| function malformedResult(receipt, at, error, derive = false) { | ||
| return { | ||
| contract_version: "1", | ||
| scope: "signature_envelope", | ||
| scope: scopeFor(derive), | ||
| accepted: false, | ||
@@ -152,2 +253,3 @@ format: { valid: false, error }, | ||
| : { status: "unknown", evaluated_at: at.iso }, | ||
| ...decisionFields(receipt, derive), | ||
| }; | ||
@@ -158,19 +260,27 @@ } | ||
| * | ||
| * This intentionally validates only the signature envelope. Full ProofPacket schema validation is | ||
| * the issuer/application's job and must not be confused with cryptographic verification. | ||
| * By default this validates only the SIGNATURE ENVELOPE — `scope: "signature_envelope"` — and says | ||
| * nothing about what the receipt concluded. Pass `derive_verdict: true` and it also re-derives the | ||
| * verdict from the receipt's own facts through the published `check-decision` table and requires the | ||
| * two to agree; `scope` then reads `"signature_envelope+decision_table"` and a `decision` block is | ||
| * present. The default is off so that every caller written before that option existed keeps its | ||
| * result shape and its acceptance semantics unchanged. | ||
| * | ||
| * Neither scope is full ProofPacket schema validation: that remains the issuer/application's job and | ||
| * must not be confused with cryptographic verification. | ||
| */ | ||
| export function verifyReceipt(receiptValue, keyDocument, options = {}) { | ||
| const at = evaluatedAt(options.at); | ||
| const derive = options.derive_verdict === true; | ||
| const receipt = record(receiptValue); | ||
| if (!receipt) | ||
| return malformedResult(null, at, "receipt must be a JSON object"); | ||
| return malformedResult(null, at, "receipt must be a JSON object", derive); | ||
| const signature = record(receipt["signature"]); | ||
| if (!signature) | ||
| return malformedResult(receipt, at, "receipt signature is missing"); | ||
| return malformedResult(receipt, at, "receipt signature is missing", derive); | ||
| if (!signatureFieldSetIsValid(signature)) { | ||
| return malformedResult(receipt, at, "receipt signature has an invalid field set"); | ||
| return malformedResult(receipt, at, "receipt signature has an invalid field set", derive); | ||
| } | ||
| if (Object.hasOwn(signature, "signed_at") && | ||
| timestamp(signature["signed_at"]) === null) { | ||
| return malformedResult(receipt, at, "receipt signature signed_at is not an RFC 3339 instant"); | ||
| return malformedResult(receipt, at, "receipt signature signed_at is not an RFC 3339 instant", derive); | ||
| } | ||
@@ -195,11 +305,11 @@ /* | ||
| if (signature["signed_at"] !== receipt["checked_at"]) { | ||
| return malformedResult(receipt, at, "receipt signature signed_at does not match the signed checked_at"); | ||
| return malformedResult(receipt, at, "receipt signature signed_at does not match the signed checked_at", derive); | ||
| } | ||
| } | ||
| if (signature["algorithm"] !== "Ed25519") { | ||
| return malformedResult(receipt, at, "only Ed25519 receipt signatures are supported"); | ||
| return malformedResult(receipt, at, "only Ed25519 receipt signatures are supported", derive); | ||
| } | ||
| const keyId = signature["key_id"]; | ||
| if (typeof keyId !== "string" || !keyId.trim() || keyId.length > 128) { | ||
| return malformedResult(receipt, at, "receipt signature key_id is invalid"); | ||
| return malformedResult(receipt, at, "receipt signature key_id is invalid", derive); | ||
| } | ||
@@ -211,3 +321,3 @@ let signatureBytes; | ||
| catch (error) { | ||
| return malformedResult(receipt, at, error.message); | ||
| return malformedResult(receipt, at, error.message, derive); | ||
| } | ||
@@ -219,3 +329,3 @@ let canonicalBytes; | ||
| catch (error) { | ||
| return malformedResult(receipt, at, error.message); | ||
| return malformedResult(receipt, at, error.message, derive); | ||
| } | ||
@@ -243,3 +353,3 @@ const digest = createHash("sha256").update(canonicalBytes).digest("hex"); | ||
| contract_version: "1", | ||
| scope: "signature_envelope", | ||
| scope: scopeFor(derive), | ||
| accepted: false, | ||
@@ -261,2 +371,3 @@ format: { valid: true }, | ||
| freshness: freshness(receipt, at), | ||
| ...decisionFields(receipt, derive), | ||
| }; | ||
@@ -267,3 +378,3 @@ } | ||
| contract_version: "1", | ||
| scope: "signature_envelope", | ||
| scope: scopeFor(derive), | ||
| accepted: false, | ||
@@ -285,2 +396,3 @@ format: { valid: true }, | ||
| freshness: freshness(receipt, at), | ||
| ...decisionFields(receipt, derive), | ||
| }; | ||
@@ -337,6 +449,16 @@ } | ||
| } | ||
| /* | ||
| * The verdict half, when it was asked for. | ||
| * | ||
| * It is computed AFTER the signature so the two answers stay separable in the result — a holder | ||
| * must be able to see "the bytes are authentic but the verdict does not follow from them" as a | ||
| * distinct outcome from "these bytes were never signed by us". It gates `accepted` in one | ||
| * direction only: it can refuse, never rescue. A receipt whose signature fails is not saved by | ||
| * re-deriving cleanly. | ||
| */ | ||
| const decision = derive ? decisionResult(receipt) : undefined; | ||
| return { | ||
| contract_version: "1", | ||
| scope: "signature_envelope", | ||
| accepted: cryptographicallyValid && trusted, | ||
| scope: scopeFor(derive), | ||
| accepted: cryptographicallyValid && trusted && (decision?.matches ?? true), | ||
| format: { valid: true }, | ||
@@ -365,2 +487,3 @@ receipt: receiptIdentity, | ||
| freshness: freshness(receipt, at), | ||
| ...(decision === undefined ? {} : { decision }), | ||
| }; | ||
@@ -370,2 +493,4 @@ } | ||
| const at = evaluatedAt(options.at); | ||
| const derive = options.derive_verdict === true; | ||
| const forward = { at: at.iso, ...(derive ? { derive_verdict: true } : {}) }; | ||
| let receipt; | ||
@@ -376,3 +501,3 @@ try { | ||
| catch (error) { | ||
| return malformedResult(null, at, `receipt JSON is invalid: ${error.message}`); | ||
| return malformedResult(null, at, `receipt JSON is invalid: ${error.message}`, derive); | ||
| } | ||
@@ -385,3 +510,3 @@ let keyDocument; | ||
| const message = `verification key JSON is invalid: ${error.message}`; | ||
| const result = verifyReceipt(receipt, {}, { at: at.iso }); | ||
| const result = verifyReceipt(receipt, {}, forward); | ||
| return { | ||
@@ -400,3 +525,3 @@ ...result, | ||
| } | ||
| return verifyReceipt(receipt, keyDocument, { at: at.iso }); | ||
| return verifyReceipt(receipt, keyDocument, forward); | ||
| } |
+1
-1
| { | ||
| "name": "@usekaval/kaval", | ||
| "version": "0.7.0", | ||
| "version": "0.7.1", | ||
| "license": "Apache-2.0", | ||
@@ -5,0 +5,0 @@ "description": "Before an AI agent acts, Kaval verifies the facts the action depends on and returns ALLOW, REVIEW, or BLOCK with an Ed25519-signed receipt.", |
+15
-7
@@ -164,4 +164,4 @@ # @usekaval/kaval | ||
| `verifyReceipt` answers three questions **separately**, and conflating them is the mistake it exists | ||
| to prevent: | ||
| `verifyReceipt` answers three questions by default. It answers a fourth question when you set | ||
| `derive_verdict: true`: | ||
@@ -172,2 +172,3 @@ 1. **Cryptographic validity** — does the Ed25519 signature cover the exact canonical unsigned bytes? | ||
| `not_yet_issued`? | ||
| 4. **Verdict derivation** — does the receipt's fact list produce its stated verdict and reason codes? | ||
@@ -189,3 +190,6 @@ A valid signature proves who sealed these exact bytes. It does not prove the claim is true, that its | ||
| const result = verifyReceipt(receipt, keyset, { at: "2026-07-20T12:00:00.000Z" }); | ||
| const result = verifyReceipt(receipt, keyset, { | ||
| at: "2026-07-20T12:00:00.000Z", | ||
| derive_verdict: true, | ||
| }); | ||
@@ -196,3 +200,4 @@ result.cryptographic.valid; // the bytes really were signed by this key | ||
| result.freshness.status; // "fresh" | "recheck_due" | "expired" | "not_yet_issued" | "unknown" | ||
| result.accepted; // cryptographic.valid && key.trusted | ||
| result.decision?.matches; // the published table reproduced the verdict and reason codes | ||
| result.accepted; // the signature, key trust, and requested verdict derivation passed | ||
| ``` | ||
@@ -205,2 +210,3 @@ | ||
| Exported: `verifyReceipt` · `verifyReceiptText` · `extractReceipt` · `verifyWebhookSignature` · | ||
| `decideCheck` · `deriveCheckDecision` · `checkDecisionInputFromReceipt` · | ||
| `parseJsonStrict` · `stableCanonicalJson` · `canonicalUnsignedReceiptJson` · | ||
@@ -257,2 +263,3 @@ `canonicalUnsignedReceiptBytes` · `parseVerificationKey` · `verificationKeyFromDocument` · | ||
| --require-fresh Exit non-zero unless freshness is "fresh" | ||
| --derive-verdict Re-derive the check verdict and require it to match | ||
| --allow-http-loopback Permit http://localhost/127.0.0.0/8/::1 for local development | ||
@@ -265,5 +272,6 @@ --compact Emit compact JSON | ||
| **Exit `0`** means the signature is valid and the key is trusted — an expired receipt still exits | ||
| `0`, because freshness is a different question; add `--require-fresh` to make anything but `fresh` | ||
| exit non-zero. **Exit `1`** is a completed verification that was not accepted. **Exit `2`** is an | ||
| input, I/O, or discovery failure. | ||
| `0`, because freshness is a different question. Add `--require-fresh` to require fresh evidence. | ||
| Add `--derive-verdict` to require the stated verdict to match the published decision table. | ||
| **Exit `1`** is a completed verification that was not accepted. **Exit `2`** is an input, I/O, or | ||
| discovery failure. | ||
@@ -270,0 +278,0 @@ `--at` and every receipt/key timestamp must be a component-valid RFC 3339 instant. Date-only |
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
237395
16.94%31
6.9%4418
15.99%549
1.48%6
20%