@usekaval/kaval
Advanced tools
+34
-26
| /** | ||
| * @usekaval/kaval — the evidence gate for AI agents. A typed, dependency-light HTTP client for the Kaval API. | ||
| * Mirrors the Python SDK (`pip install kaval`). Uses the global `fetch` (Node 18+, browsers, edge). | ||
| * @usekaval/kaval — before an AI agent acts, Kaval verifies the facts the action relies on and | ||
| * returns a time-bounded signed proof your policy can enforce — ALLOW, REVIEW, or BLOCK. | ||
| * A typed, dependency-light HTTP client for the Kaval API. Mirrors the Python SDK | ||
| * (`pip install kaval`). Uses the global `fetch` (Node 18+, browsers, edge). | ||
| */ | ||
| import type { AuditInput, ProofGateInput, ProofGateResult, ProofPacket } from "./proof.js"; | ||
| import { type CommerceActionTimeGateInput, type CommerceActionTimeGateResult, type LiveOfferSearchResult, type OfferSearchInput, type OfferSearchStreamEvent } from "./offer-search.js"; | ||
| import type { AuditInput, ProofGateInput, ProofGateResult, ProofPacket, VerifyRequest, VerifyResponse } from "./proof.js"; | ||
| export type * from "./proof.js"; | ||
| export type * from "./offer-search.js"; | ||
| export type VerdictStatus = "current" | "stale" | "contradicted" | "unsupported" | "conflicting" | "insufficient"; | ||
| /** Speed/depth tier for a verify() call. */ | ||
| /** Speed/depth tier for a legacy belief-freshness call. */ | ||
| export type VerifyMode = "instant" | "fast" | "auto" | "deep"; | ||
@@ -95,3 +95,5 @@ export interface Evidence { | ||
| export type OutcomeKind = "current_later_contradicted" | "stale_caught_real" | "stale_was_false_alarm" | "relied_and_correct"; | ||
| export interface VerifyInput { | ||
| /** LEGACY input for the belief-freshness fallback on /v1/verify. Prefer `VerifyRequest` | ||
| * (a conclusion + evidence_refs) via `verify()` for new integrations. */ | ||
| export interface VerifyBeliefInput { | ||
| belief: string; | ||
@@ -141,2 +143,9 @@ context?: string; | ||
| } | ||
| /** Thrown when POST /v1/gate returns HTTP 404 `proof_not_found`: no durable proof matches the | ||
| * supplied `proof_id`/`proof_key` in this workspace. Build one with `audit()` before gating — | ||
| * a missing proof is never a 200 gate state. */ | ||
| export declare class ProofNotFoundError extends KavalError { | ||
| readonly code = "proof_not_found"; | ||
| constructor(payload: unknown, idempotencyKey?: string); | ||
| } | ||
| export interface KavalOptions { | ||
@@ -163,3 +172,4 @@ apiKey?: string; | ||
| } | ||
| /** The Kaval client: evidence in, an action-bound decision or review-only research result out. */ | ||
| /** The Kaval client: build a signed proof with `audit()`, enforce it at act time with `gate()`, | ||
| * or verify one conclusion with `verify()`. */ | ||
| export declare class Kaval { | ||
@@ -173,4 +183,17 @@ private readonly base; | ||
| private post; | ||
| /** Pre-action gate: the verdict plus `act`. Treat `act === false` as "re-fetch before relying on it". */ | ||
| verify(input: string | VerifyInput, options?: RequestOptions): Promise<Decision>; | ||
| /** Build, sign, and persist a complete action-bound proof packet (the expensive research path). */ | ||
| audit(input: AuditInput, options?: RequestOptions): Promise<ProofPacket>; | ||
| /** Apply a current durable proof to the exact action at act time — no search, parsing, or model | ||
| * call. A missing proof is HTTP 404 `proof_not_found`, thrown as `ProofNotFoundError`. */ | ||
| gate(input: ProofGateInput, options?: RequestOptions): Promise<ProofGateResult>; | ||
| /** Alias for gate(), kept for callers of the previous method name. */ | ||
| gateAction(input: ProofGateInput, options?: RequestOptions): Promise<ProofGateResult>; | ||
| /** Compatibility surface: verify one load-bearing conclusion against its evidence references. | ||
| * Returns `valid` | `invalidated` | `could_not_verify` plus a signed proof receipt. Production | ||
| * actions should build proof with `audit()` and enforce it with `gate()`. */ | ||
| verify(request: VerifyRequest, options?: RequestOptions): Promise<VerifyResponse>; | ||
| /** LEGACY belief-freshness fallback (accepted on the same /v1/verify route): the verdict plus | ||
| * `act`. Treat `act === false` as "re-fetch before relying on it". New integrations should call | ||
| * `verify()` with a conclusion + evidence_refs, or `audit()`/`gate()` for production actions. */ | ||
| verifyBelief(input: string | VerifyBeliefInput, options?: RequestOptions): Promise<Decision>; | ||
| /** Re-ground a held belief → the raw freshness verdict (no act decision). */ | ||
@@ -190,17 +213,2 @@ check(input: string | CheckInput, options?: RequestOptions): Promise<Verdict>; | ||
| monitor(input: MonitorInput, options?: RequestOptions): Promise<MonitorResult>; | ||
| /** Search the accessible configured web for exact or possible offers. Current results are | ||
| * research-only: action.state is NEEDS_REVIEW or NO_RELIABLE_OFFER, never permission to quote. */ | ||
| searchOffers(input: OfferSearchInput, options?: RequestOptions): Promise<LiveOfferSearchResult>; | ||
| /** Stream bounded, review-only acquisition progress followed by one canonical final result. | ||
| * Cancellation closes the response body and propagates to the hosted acquisition operation. */ | ||
| streamOfferSearch(input: OfferSearchInput, options?: RequestOptions): AsyncGenerator<OfferSearchStreamEvent, LiveOfferSearchResult, void>; | ||
| /** Re-read one persisted offer generation at the exact action boundary. This final fence always | ||
| * returns REVIEW with commerce permission withheld; it never authorizes quoting or purchasing. */ | ||
| gateOfferSearch(input: CommerceActionTimeGateInput, options?: Pick<RequestOptions, "signal" | "timeoutMs">): Promise<CommerceActionTimeGateResult>; | ||
| /** Build, sign, and persist a complete action-bound proof packet. */ | ||
| audit(input: AuditInput, options?: RequestOptions): Promise<ProofPacket>; | ||
| /** Apply a current durable proof to the exact action without repeating research. */ | ||
| gateAction(input: ProofGateInput, options?: RequestOptions): Promise<ProofGateResult>; | ||
| /** Short alias for gateAction(). */ | ||
| gate(input: ProofGateInput, options?: RequestOptions): Promise<ProofGateResult>; | ||
| /** Report what actually happened, to calibrate trust over time. */ | ||
@@ -215,3 +223,3 @@ reportOutcome(input: { | ||
| /** Lower-level structured passthrough: a `KavalRequest` in, the raw `Verdict` out. Prefer | ||
| * `verify`/`check` unless you need the structured fact-type form. Mirrors the Python `kaval()`. */ | ||
| * `verifyBelief`/`check` unless you need the structured fact-type form. Mirrors the Python `kaval()`. */ | ||
| kaval(request: Record<string, unknown>, options?: RequestOptions): Promise<Verdict>; | ||
@@ -218,0 +226,0 @@ /** Batch of structured `KavalRequest`s → a `Verdict` per request (same order). Mirrors the Python |
+76
-200
| /** | ||
| * @usekaval/kaval — the evidence gate for AI agents. A typed, dependency-light HTTP client for the Kaval API. | ||
| * Mirrors the Python SDK (`pip install kaval`). Uses the global `fetch` (Node 18+, browsers, edge). | ||
| * @usekaval/kaval — before an AI agent acts, Kaval verifies the facts the action relies on and | ||
| * returns a time-bounded signed proof your policy can enforce — ALLOW, REVIEW, or BLOCK. | ||
| * A typed, dependency-light HTTP client for the Kaval API. Mirrors the Python SDK | ||
| * (`pip install kaval`). Uses the global `fetch` (Node 18+, browsers, edge). | ||
| */ | ||
| import { reviewOnlyCommerceActionTimeGateResult, reviewOnlyOfferSearchProgressEvent, reviewOnlyOfferSearchReplayEvent, reviewOnlyOfferSearchResult, } from "./offer-search.js"; | ||
| /** Thrown on any non-2xx response. */ | ||
@@ -21,2 +22,12 @@ export class KavalError extends Error { | ||
| } | ||
| /** Thrown when POST /v1/gate returns HTTP 404 `proof_not_found`: no durable proof matches the | ||
| * supplied `proof_id`/`proof_key` in this workspace. Build one with `audit()` before gating — | ||
| * a missing proof is never a 200 gate state. */ | ||
| export class ProofNotFoundError extends KavalError { | ||
| code = "proof_not_found"; | ||
| constructor(payload, idempotencyKey) { | ||
| super(404, payload, idempotencyKey); | ||
| this.name = "ProofNotFoundError"; | ||
| } | ||
| } | ||
| function attachIdempotencyKey(error, idempotencyKey) { | ||
@@ -85,2 +96,27 @@ if (error && (typeof error === "object" || typeof error === "function")) { | ||
| } | ||
| /** Fail fast on the wire-invalid evidence_refs shapes the server strictly rejects, before any | ||
| * network call or idempotency-key spend. */ | ||
| function assertEvidenceRefs(refs) { | ||
| if (!Array.isArray(refs) || refs.length < 1 || refs.length > 20) { | ||
| throw new TypeError("evidence_refs must contain between 1 and 20 references"); | ||
| } | ||
| const documentIds = new Set(); | ||
| for (const ref of refs) { | ||
| if (typeof ref === "string") | ||
| continue; | ||
| const url = ref?.url; | ||
| const documentId = ref?.document_id; | ||
| if (!ref || | ||
| typeof ref !== "object" || | ||
| typeof url !== "string" || | ||
| typeof documentId !== "string" || | ||
| documentId.length === 0) { | ||
| throw new TypeError("each evidence reference must be a plain https URL string or a { url, document_id } object; a bare { url } object without document_id is invalid — pass the plain string instead"); | ||
| } | ||
| if (documentIds.has(documentId)) { | ||
| throw new TypeError("evidence_refs document_id values must be unique"); | ||
| } | ||
| documentIds.add(documentId); | ||
| } | ||
| } | ||
| function requestSignal(external, timeoutMs) { | ||
@@ -107,3 +143,4 @@ if (timeoutMs !== null && (!Number.isFinite(timeoutMs) || timeoutMs <= 0)) { | ||
| } | ||
| /** The Kaval client: evidence in, an action-bound decision or review-only research result out. */ | ||
| /** The Kaval client: build a signed proof with `audit()`, enforce it at act time with `gate()`, | ||
| * or verify one conclusion with `verify()`. */ | ||
| export class Kaval { | ||
@@ -198,4 +235,36 @@ base; | ||
| } | ||
| /** Pre-action gate: the verdict plus `act`. Treat `act === false` as "re-fetch before relying on it". */ | ||
| verify(input, options) { | ||
| /** Build, sign, and persist a complete action-bound proof packet (the expensive research path). */ | ||
| audit(input, options) { | ||
| return this.billablePost("/v1/audit", input, options); | ||
| } | ||
| /** Apply a current durable proof to the exact action at act time — no search, parsing, or model | ||
| * call. A missing proof is HTTP 404 `proof_not_found`, thrown as `ProofNotFoundError`. */ | ||
| async gate(input, options) { | ||
| try { | ||
| return await this.billablePost("/v1/gate", input, options); | ||
| } | ||
| catch (error) { | ||
| if (error instanceof KavalError && | ||
| error.status === 404 && | ||
| apiErrorCode(error.payload) === "proof_not_found") { | ||
| throw new ProofNotFoundError(error.payload, error.idempotencyKey); | ||
| } | ||
| throw error; | ||
| } | ||
| } | ||
| /** Alias for gate(), kept for callers of the previous method name. */ | ||
| gateAction(input, options) { | ||
| return this.gate(input, options); | ||
| } | ||
| /** Compatibility surface: verify one load-bearing conclusion against its evidence references. | ||
| * Returns `valid` | `invalidated` | `could_not_verify` plus a signed proof receipt. Production | ||
| * actions should build proof with `audit()` and enforce it with `gate()`. */ | ||
| async verify(request, options) { | ||
| assertEvidenceRefs(request.evidence_refs); | ||
| return this.billablePost("/v1/verify", request, options); | ||
| } | ||
| /** LEGACY belief-freshness fallback (accepted on the same /v1/verify route): the verdict plus | ||
| * `act`. Treat `act === false` as "re-fetch before relying on it". New integrations should call | ||
| * `verify()` with a conclusion + evidence_refs, or `audit()`/`gate()` for production actions. */ | ||
| verifyBelief(input, options) { | ||
| return this.billablePost("/v1/verify", typeof input === "string" ? { belief: input } : input, options); | ||
@@ -219,195 +288,2 @@ } | ||
| } | ||
| /** Search the accessible configured web for exact or possible offers. Current results are | ||
| * research-only: action.state is NEEDS_REVIEW or NO_RELIABLE_OFFER, never permission to quote. */ | ||
| async searchOffers(input, options) { | ||
| const result = await this.billablePost("/v1/search-offers", input, options); | ||
| return reviewOnlyOfferSearchResult(result, input.request_id); | ||
| } | ||
| /** Stream bounded, review-only acquisition progress followed by one canonical final result. | ||
| * Cancellation closes the response body and propagates to the hosted acquisition operation. */ | ||
| async *streamOfferSearch(input, options = {}) { | ||
| const idempotencyKey = options.idempotencyKey ?? generatedIdempotencyKey(); | ||
| const headers = { | ||
| ...this.headers, | ||
| accept: "text/event-stream", | ||
| "idempotency-key": idempotencyKey, | ||
| }; | ||
| const request = requestSignal(options.signal, options.timeoutMs === undefined ? this.timeoutMs : options.timeoutMs); | ||
| let response; | ||
| let reader; | ||
| try { | ||
| for (let attempt = 0; attempt < MAX_BILLABLE_ATTEMPTS; attempt += 1) { | ||
| try { | ||
| response = await this.f(`${this.base}/v1/search-offers`, { | ||
| method: "POST", | ||
| headers, | ||
| signal: request.signal, | ||
| body: JSON.stringify(input), | ||
| }); | ||
| } | ||
| catch (error) { | ||
| if (request.signal?.aborted || attempt + 1 >= MAX_BILLABLE_ATTEMPTS) { | ||
| throw attachIdempotencyKey(error, idempotencyKey); | ||
| } | ||
| continue; | ||
| } | ||
| if (response.ok) | ||
| break; | ||
| const responseText = await response.text(); | ||
| let payload = responseText; | ||
| try { | ||
| payload = JSON.parse(responseText); | ||
| } | ||
| catch { | ||
| // A non-Kaval intermediary may return a plain-text error. | ||
| } | ||
| const code = apiErrorCode(payload); | ||
| if (attempt + 1 < MAX_BILLABLE_ATTEMPTS && | ||
| code !== undefined && | ||
| AMBIGUOUS_IDEMPOTENCY_CODES.has(code)) { | ||
| response = undefined; | ||
| continue; | ||
| } | ||
| throw new KavalError(response.status, payload, idempotencyKey); | ||
| } | ||
| if (!response?.ok) | ||
| throw new Error("unreachable Offer Search stream request state"); | ||
| if (!response.headers.get("content-type")?.includes("text/event-stream")) { | ||
| throw attachIdempotencyKey(new TypeError("Offer Search stream returned a non-SSE response"), idempotencyKey); | ||
| } | ||
| if (!response.body) { | ||
| throw attachIdempotencyKey(new TypeError("Offer Search stream response has no body"), idempotencyKey); | ||
| } | ||
| reader = response.body.getReader(); | ||
| const decoder = new TextDecoder(); | ||
| let buffer = ""; | ||
| let lastSequence = -1; | ||
| let finalResult; | ||
| let streamRequestDigest; | ||
| const consumeFrame = (frame) => { | ||
| const lines = frame.split("\n"); | ||
| const eventName = lines | ||
| .find((line) => line.startsWith("event:")) | ||
| ?.slice("event:".length) | ||
| .trim(); | ||
| const idText = lines | ||
| .find((line) => line.startsWith("id:")) | ||
| ?.slice("id:".length) | ||
| .trim(); | ||
| const dataText = lines | ||
| .filter((line) => line.startsWith("data:")) | ||
| .map((line) => line.slice("data:".length).trimStart()) | ||
| .join("\n"); | ||
| if (!eventName || !dataText) | ||
| return null; | ||
| let payload; | ||
| try { | ||
| payload = JSON.parse(dataText); | ||
| } | ||
| catch (error) { | ||
| throw attachIdempotencyKey(error, idempotencyKey); | ||
| } | ||
| const id = idText === undefined ? undefined : Number(idText); | ||
| if (idText !== undefined && (!Number.isInteger(id) || id < 0)) { | ||
| throw new TypeError("Offer Search stream event ID is invalid"); | ||
| } | ||
| if (eventName === "error") { | ||
| const error = payload; | ||
| throw new KavalError(typeof error?.status === "number" ? error.status : 500, payload, idempotencyKey); | ||
| } | ||
| if (eventName === "final") { | ||
| const result = reviewOnlyOfferSearchResult(payload, input.request_id); | ||
| if (streamRequestDigest !== undefined && | ||
| result.request_digest !== streamRequestDigest) { | ||
| throw new TypeError("Offer Search stream events are bound to another final result"); | ||
| } | ||
| const sequence = Number.isInteger(id) ? id : lastSequence + 1; | ||
| if (sequence <= lastSequence) { | ||
| throw new TypeError("Offer Search stream sequence is not monotonic"); | ||
| } | ||
| lastSequence = sequence; | ||
| finalResult = result; | ||
| return { type: "final", sequence, result }; | ||
| } | ||
| if (eventName === "replay") { | ||
| const event = reviewOnlyOfferSearchReplayEvent(payload, input.request_id); | ||
| if ((id !== undefined && id !== event.sequence) || | ||
| event.sequence <= lastSequence) { | ||
| throw new TypeError("Offer Search stream replay sequence is invalid"); | ||
| } | ||
| if (streamRequestDigest !== undefined && | ||
| event.request_digest !== streamRequestDigest) { | ||
| throw new TypeError("Offer Search replay request binding changed"); | ||
| } | ||
| streamRequestDigest = event.request_digest; | ||
| lastSequence = event.sequence; | ||
| return event; | ||
| } | ||
| const event = reviewOnlyOfferSearchProgressEvent(payload); | ||
| if (event.type !== eventName || | ||
| event.request_id !== input.request_id || | ||
| (id !== undefined && id !== event.sequence) || | ||
| event.sequence <= lastSequence) { | ||
| throw new TypeError("Offer Search stream event sequence or type is invalid"); | ||
| } | ||
| if (event.type === "candidate_provisional") { | ||
| if (streamRequestDigest !== undefined && | ||
| event.details.request_digest !== streamRequestDigest) { | ||
| throw new TypeError("Offer Search provisional candidate request binding changed"); | ||
| } | ||
| streamRequestDigest = event.details.request_digest; | ||
| } | ||
| lastSequence = event.sequence; | ||
| return event; | ||
| }; | ||
| while (true) { | ||
| const chunk = await reader.read(); | ||
| buffer = | ||
| `${buffer}${decoder.decode(chunk.value, { stream: !chunk.done })}`.replaceAll("\r\n", "\n"); | ||
| let boundary = buffer.indexOf("\n\n"); | ||
| while (boundary >= 0) { | ||
| const frame = buffer.slice(0, boundary); | ||
| buffer = buffer.slice(boundary + 2); | ||
| const event = consumeFrame(frame); | ||
| if (event) | ||
| yield event; | ||
| if (finalResult) | ||
| return finalResult; | ||
| boundary = buffer.indexOf("\n\n"); | ||
| } | ||
| if (chunk.done) | ||
| break; | ||
| } | ||
| if (buffer.trim().length > 0) { | ||
| const event = consumeFrame(buffer); | ||
| if (event) | ||
| yield event; | ||
| if (finalResult) | ||
| return finalResult; | ||
| } | ||
| throw attachIdempotencyKey(new TypeError("Offer Search stream ended before its final result"), idempotencyKey); | ||
| } | ||
| finally { | ||
| await reader?.cancel().catch(() => undefined); | ||
| request.cleanup(); | ||
| } | ||
| } | ||
| /** Re-read one persisted offer generation at the exact action boundary. This final fence always | ||
| * returns REVIEW with commerce permission withheld; it never authorizes quoting or purchasing. */ | ||
| async gateOfferSearch(input, options) { | ||
| const result = await this.post("/v1/search-offers/gate", input, options); | ||
| return reviewOnlyCommerceActionTimeGateResult(result, input); | ||
| } | ||
| /** Build, sign, and persist a complete action-bound proof packet. */ | ||
| audit(input, options) { | ||
| return this.billablePost("/v1/audit", input, options); | ||
| } | ||
| /** Apply a current durable proof to the exact action without repeating research. */ | ||
| gateAction(input, options) { | ||
| return this.billablePost("/v1/gate", input, options); | ||
| } | ||
| /** Short alias for gateAction(). */ | ||
| gate(input, options) { | ||
| return this.gateAction(input, options); | ||
| } | ||
| /** Report what actually happened, to calibrate trust over time. */ | ||
@@ -418,3 +294,3 @@ reportOutcome(input) { | ||
| /** Lower-level structured passthrough: a `KavalRequest` in, the raw `Verdict` out. Prefer | ||
| * `verify`/`check` unless you need the structured fact-type form. Mirrors the Python `kaval()`. */ | ||
| * `verifyBelief`/`check` unless you need the structured fact-type form. Mirrors the Python `kaval()`. */ | ||
| kaval(request, options) { | ||
@@ -421,0 +297,0 @@ return this.billablePost("/v1/kaval", request, options); |
+41
-2
@@ -503,4 +503,43 @@ /** Public proof-protocol types. Field names intentionally match the hosted REST JSON exactly. */ | ||
| }); | ||
| export type ProofGateState = "current" | "expired" | "not_yet_valid" | "invalidated" | "dependency_changed" | "integrity_failed" | "policy_mismatch" | "not_found" | "operational_failure"; | ||
| export type ProofBillingClass = "action_gate" | "direct_refresh" | "web_refresh" | "deep_refresh" | "operational_failure"; | ||
| /** Every state /v1/gate can return as a 200. A missing proof is HTTP 404 `proof_not_found` | ||
| * (thrown as `ProofNotFoundError`), never a 200 state. */ | ||
| export type ProofGateState = "current" | "not_yet_valid" | "expired" | "invalidated" | "dependency_changed" | "integrity_failed" | "policy_mismatch" | "operational_failure"; | ||
| export type ProofBillingClass = "action_gate" | "operational_failure"; | ||
| /** One evidence reference for POST /v1/verify: EITHER a plain https URL string OR a strict | ||
| * `{ url, document_id }` pair. A bare `{ url }` object without `document_id` is invalid on the | ||
| * wire — pass the plain string instead. `document_id` values must be unique per request. */ | ||
| export type EvidenceRef = string | { | ||
| url: string; | ||
| document_id: string; | ||
| }; | ||
| /** POST /v1/verify body — the compatibility surface for one load-bearing conclusion. */ | ||
| export interface VerifyRequest { | ||
| /** The exact conclusion the downstream workflow intends to rely on. */ | ||
| conclusion: string; | ||
| /** 1–20 references to the records or source versions that support the conclusion. */ | ||
| evidence_refs: EvidenceRef[]; | ||
| /** RFC 3339 datetime with offset. */ | ||
| as_of?: IsoTimestamp; | ||
| materiality?: Materiality; | ||
| intended_action?: string; | ||
| reversibility?: ActionReversibility; | ||
| jurisdiction?: string; | ||
| context?: string; | ||
| } | ||
| export type VerifyStatus = "valid" | "invalidated" | "could_not_verify"; | ||
| /** The signed receipt inside a /v1/verify response. There is no receipt-level `expires_at`; | ||
| * expiry lives at `packet.action_decision.expires_at`. */ | ||
| export interface VerifyReceipt { | ||
| proof_id: string; | ||
| decision: ActionDisposition; | ||
| reason: string; | ||
| /** Follow-up endpoint (`/v1/proofs/<id>/share`), deliberately not a raw bearer URL. */ | ||
| share_endpoint: string; | ||
| /** The full signed proof packet backing this receipt. */ | ||
| packet: ProofPacket; | ||
| } | ||
| export interface VerifyResponse { | ||
| status: VerifyStatus; | ||
| receipt: VerifyReceipt; | ||
| } | ||
| export interface ProofEnforcementResult { | ||
@@ -507,0 +546,0 @@ mode: "shadow" | "block_only" | "bounded"; |
+6
-5
| { | ||
| "name": "@usekaval/kaval", | ||
| "version": "0.4.0", | ||
| "version": "0.5.0", | ||
| "license": "Apache-2.0", | ||
| "description": "Evidence gates for AI agents: review-only offer research and action-bound ALLOW/REVIEW/BLOCK decisions.", | ||
| "description": "Fact verification for AI agents: before an agent acts, Kaval verifies the facts the action relies on and returns a time-bounded, Ed25519-signed proof your policy can enforce.", | ||
| "type": "module", | ||
@@ -23,6 +23,7 @@ "main": "./dist/index.js", | ||
| "evidence", | ||
| "commerce", | ||
| "verification", | ||
| "proof", | ||
| "receipts", | ||
| "freshness", | ||
| "rag", | ||
| "verification" | ||
| "rag" | ||
| ], | ||
@@ -29,0 +30,0 @@ "engines": { |
+90
-203
| # @usekaval/kaval | ||
| The evidence gate for AI agents. Before an agent acts, Kaval checks that the current evidence still | ||
| supports that exact action. The full proof lifecycle returns `ALLOW`, `REVIEW`, or `BLOCK`; when the | ||
| evidence changes or expires, the permission does too. | ||
| Before an AI agent acts, Kaval verifies the facts the action relies on and returns a time-bounded | ||
| signed proof your policy can enforce — `ALLOW`, `REVIEW`, or `BLOCK`. | ||
| **Search retrieves evidence. Kaval decides whether that evidence is sufficient for the action.** | ||
| Policy engines decide whether an action is permitted under the rules; Kaval verifies whether the | ||
| facts those rules depend on are still true. | ||
@@ -26,146 +26,13 @@ ```bash | ||
| ## Find current offer evidence (review-only) | ||
| ## Build a proof, then gate the action | ||
| ```ts | ||
| import { Kaval, type OfferSearchInput } from "@usekaval/kaval"; | ||
| `audit()` builds the proof — the expensive research path. `gate()` applies it at act time with no | ||
| search, parsing, or model call. | ||
| const request: OfferSearchInput = { | ||
| schema_revision: 1, | ||
| request_id: crypto.randomUUID(), | ||
| raw_description: "Makita XPH14Z hammer drill, tool only", | ||
| target: { | ||
| schema_revision: 1, | ||
| name: "Makita XPH14Z", | ||
| identifiers: [{ scheme: "model", value: "XPH14Z" }], | ||
| attributes: [{ key: "kit", value: false }], | ||
| }, | ||
| requested_condition: "new", | ||
| destination: { country_code: "US", region: "CA", postal_code: "94107" }, | ||
| match_policy: { | ||
| identity_requirement: "shared_identifier", | ||
| required_identifier_schemes: ["model"], | ||
| required_attribute_keys: ["kit"], | ||
| permitted_substitutions: [], | ||
| }, | ||
| seller_policy: { | ||
| allowed_seller_ids: [], | ||
| blocked_seller_ids: [], | ||
| allowed_kinds: ["brand_direct", "authorized_retailer"], | ||
| require_authorized: true, | ||
| }, | ||
| destination_policy: { | ||
| require_eligible: true, | ||
| require_exact_region: true, | ||
| require_exact_postal_code: true, | ||
| }, | ||
| price_policy: { | ||
| currency: "USD", | ||
| require_complete_landed_total: true, | ||
| allow_estimated_components: false, | ||
| allow_member_price: false, | ||
| allow_subscription_price: false, | ||
| allow_coupon_price: false, | ||
| allow_installment_display: false, | ||
| allow_trade_in_price: false, | ||
| }, | ||
| source_policy: { | ||
| allowed_source_ids: [], | ||
| blocked_source_ids: [], | ||
| require_origin_evidence: true, | ||
| }, | ||
| intended_action: { | ||
| description: "Quote this exact item to a customer", | ||
| materiality: "high", | ||
| reversibility: "partially_reversible", | ||
| }, | ||
| freshness_maximum_age_ms: 300_000, | ||
| max_results: 5, | ||
| minimum_unique_sellers: 2, | ||
| deadline_ms: 15_000, | ||
| maximum_cost_micro_usd: 50_000, | ||
| maximum_search_calls: 4, | ||
| maximum_fetches: 12, | ||
| }; | ||
| const kaval = new Kaval({ | ||
| apiKey: process.env.KAVAL_API_KEY, | ||
| }); | ||
| const result = await kaval.searchOffers(request); | ||
| if (result.action.state === "NEEDS_REVIEW") { | ||
| await queueForHumanReview(result.candidates); | ||
| } | ||
| // When durable lifecycle metadata is present, final-fence the exact generation at action time. | ||
| // Even current evidence remains REVIEW-only until commerce authorization is calibrated. | ||
| if (result.lifecycle?.persistence === "persisted") { | ||
| const finalFence = await kaval.gateOfferSearch({ | ||
| dependency_id: result.lifecycle.dependency_id, | ||
| generation_id: result.lifecycle.generation_id, | ||
| generation_number: result.lifecycle.generation_number, | ||
| generation_digest: result.lifecycle.generation_digest, | ||
| action_binding: result.lifecycle.action_binding, | ||
| }); | ||
| if (finalFence.state !== "current_review_only") { | ||
| await refreshOfferEvidence(result.lifecycle.dependency_id); | ||
| } | ||
| // finalFence.disposition === "REVIEW" and finalFence.permission === "withheld" in every state. | ||
| } | ||
| ``` | ||
| For progressive UI or agent feedback, consume the same operation as SSE. The last event contains the | ||
| same guarded result returned by `searchOffers()`; earlier events are explicitly `research_only` and | ||
| cannot authorize a quote or purchase: | ||
| ```ts | ||
| for await (const event of kaval.streamOfferSearch(request, { | ||
| idempotencyKey: crypto.randomUUID(), | ||
| })) { | ||
| if (event.type === "candidate_provisional") { | ||
| // Origin verification finished, but final selection and lifecycle persistence have not. | ||
| // durable=false, actionable=false, permission="withheld". | ||
| renderProvisionalOffer(event.details.candidate); | ||
| } else if (event.type === "final") { | ||
| await queueForHumanReview(event.result.candidates); | ||
| } else { | ||
| console.log( | ||
| event.type, | ||
| event.type === "replay" ? "completed operation replayed" : event.message, | ||
| ); | ||
| } | ||
| } | ||
| ``` | ||
| import { Kaval, ProofNotFoundError } from "@usekaval/kaval"; | ||
| `candidate_provisional` is the only pre-completion candidate event. Its typed details always state | ||
| `publication_state: "provisional"`, `durable: false`, `actionable: false`, | ||
| `permission: "withheld"`, and `final_inclusion: "not_yet_determined"`. The SDK binds its request | ||
| ID and cryptographic digest across provisional, replay, and terminal results and rejects drift. The | ||
| later `candidate` event has crossed the current final publication boundary; only the exact | ||
| `lifecycle.selected_candidate_id` is durable, and every candidate remains review-only. | ||
| const kaval = new Kaval({ apiKey: process.env.KAVAL_API_KEY }); | ||
| Offer Search researches the accessible configured web through configured structured source workers, | ||
| search discovery, direct origin re-fetches, serialized-DOM browser fallback, and optional | ||
| destination-aware checkout resolution. `candidate.checkout` contains the checkout receipt when one | ||
| was verified; `acquisition.source_ledger` states which planned sources succeeded, failed, were | ||
| prohibited, or remained unsearched. Coverage is explicitly bounded, not a claim to have searched the | ||
| literal entire internet. Its public output is deliberately shadow-grade: `action.state` is | ||
| `NEEDS_REVIEW` or `NO_RELIABLE_OFFER`, candidate dispositions are `review` or `rejected`, and the | ||
| SDK rejects any drifted response that claims `ALLOW`, `BLOCK`, `SAFE_TO_QUOTE`, or other commerce | ||
| authority. Do not quote or purchase from this result without review. `searchOffers()` accepts the same | ||
| `{ idempotencyKey?, signal?, timeoutMs? }` request options as other billable calls; | ||
| `streamOfferSearch()` also closes the response stream when its signal is aborted or iteration stops. | ||
| When the server has a durable commerce lifecycle configured, `result.lifecycle` identifies the | ||
| immutable evidence generation, exact selected candidate, and action binding. Call | ||
| `gateOfferSearch()` immediately before the action boundary. It re-reads that generation and the | ||
| latest stream head, but deliberately returns only `disposition: "REVIEW"` and | ||
| `permission: "withheld"`; stale, expired, invalidated, changed, revoked, unavailable, or mismatched | ||
| evidence must be refreshed or reviewed. | ||
| ## Build a proof, then gate the action | ||
| ```ts | ||
| import { Kaval } from "@usekaval/kaval"; | ||
| const kaval = new Kaval({ apiKey: process.env.KAVAL_API_KEY }); | ||
| // 1. Build, sign, and persist a complete action-bound proof packet. | ||
| const proof = await kaval.audit({ | ||
@@ -180,21 +47,20 @@ text: "Acme is eligible for a $12,000 refund", | ||
| }); | ||
| const gate = await kaval.gateAction({ | ||
| proof_id: proof.proof_id, | ||
| material_claim_ids: proof.action_decision.material_claim_ids, | ||
| threshold: proof.action_decision.threshold, | ||
| action: proof.research_contract.action, | ||
| }); | ||
| if (gate.enforcement?.controlApplied === true) { | ||
| if (gate.enforcement.executionAllowed !== true) { | ||
| throw new Error("Kaval blocked the action"); | ||
| // 2. At the exact action boundary, apply the durable proof — cheap and research-free. | ||
| try { | ||
| const gate = await kaval.gate({ | ||
| proof_id: proof.proof_id, | ||
| material_claim_ids: proof.action_decision.material_claim_ids, | ||
| threshold: proof.action_decision.threshold, | ||
| action: proof.research_contract.action, | ||
| }); | ||
| if (gate.state !== "current" || gate.decision.decision !== "ALLOW") { | ||
| throw new Error("Kaval did not allow the action"); // fail closed | ||
| } | ||
| } else if ( | ||
| gate.enforcement === undefined && | ||
| (gate.state !== "current" || gate.decision.decision !== "ALLOW") | ||
| ) { | ||
| // A direct integration without staged enforcement fails closed. | ||
| throw new Error("Kaval did not allow the action"); | ||
| } catch (error) { | ||
| if (error instanceof ProofNotFoundError) { | ||
| // No durable proof matches this proof_id/proof_key — build one with audit() first. | ||
| } | ||
| throw error; | ||
| } | ||
| // controlApplied === false is shadow mode: record wouldAllow, but keep the customer's existing | ||
| // action policy authoritative. | ||
| ``` | ||
@@ -204,28 +70,48 @@ | ||
| versions, exact evidence spans, lineage families, claim assessments, calibrated/withheld risk, | ||
| provenance, expiry, and signature. `gateAction()` is the cheap action-time check and includes staged | ||
| `enforcement` (`shadow`, `block_only`, or `bounded`) when configured by the deployment. | ||
| Only `enforcement.controlApplied === true` may control execution. Shadow mode returns | ||
| `controlApplied: false`, `executionAllowed: null`, and a counterfactual `wouldAllow` for calibration. | ||
| provenance, expiry, and an Ed25519 signature (`signature.algorithm: "Ed25519"`, key id like | ||
| `proof-ed25519-2026-07`). | ||
| Both methods accept `{ idempotencyKey?, signal?, timeoutMs? }`. The constructor defaults to a | ||
| 30-second deadline; override per call or set `timeoutMs: null` to disable it. Cancellation and timeout | ||
| errors retain `error.idempotencyKey`, because an interrupted billable request can be ambiguous. | ||
| `gate()` returns `{ proofId, state, decision, billingClass, proofReused, researchPerformed: false, | ||
| latencyMs }`. `state` is one of `current`, `not_yet_valid`, `expired`, `invalidated`, | ||
| `dependency_changed`, `integrity_failed`, `policy_mismatch`, or `operational_failure`. A missing | ||
| proof is never a 200 state: the server returns HTTP 404 `proof_not_found`, which this client throws | ||
| as the typed `ProofNotFoundError` (a `KavalError` subclass with `code: "proof_not_found"`). | ||
| `gateAction()` remains as an alias for `gate()`. | ||
| ## Legacy held-belief compatibility | ||
| ## Verify a single conclusion (compatibility surface) | ||
| `verify()` checks one load-bearing conclusion against its evidence references and returns | ||
| `valid`, `invalidated`, or `could_not_verify` plus a signed proof receipt. Production actions | ||
| should build proof with `audit()` and enforce it with `gate()`. | ||
| ```ts | ||
| import { Kaval } from "@usekaval/kaval"; | ||
| const { status, receipt } = await kaval.verify({ | ||
| conclusion: "The 2024 International Building Code is the current IBC edition.", | ||
| evidence_refs: ["https://codes.iccsafe.org/content/IBC2024V2.0"], | ||
| }); | ||
| const kaval = new Kaval({ apiKey: process.env.KAVAL_API_KEY }); | ||
| const decision = await kaval.verify("Acme's CEO is Jane Doe"); | ||
| if (!decision.act) { | ||
| // stale / contradicted — re-fetch before relying on it | ||
| } | ||
| status; // "valid" | "invalidated" | "could_not_verify" | ||
| receipt.decision; // "ALLOW" | "BLOCK" | "REVIEW" | ||
| receipt.reason; // e.g. "All material claims verified against current evidence." | ||
| receipt.share_endpoint; // "/v1/proofs/<id>/share" | ||
| receipt.packet; // the full signed ProofPacket | ||
| receipt.packet.action_decision.expires_at; // expiry lives here, not on the receipt | ||
| ``` | ||
| `verify()` preserves the original currentness API. It returns the verdict plus `act` — `true` only | ||
| when the belief is `current` and confident | ||
| (≥ 0.7 by default; override with `minConfidence`). | ||
| Each item in `evidence_refs` (1–20 entries) is **either** a plain https URL string **or** a strict | ||
| `{ url, document_id }` object; `document_id` values must be unique per request. A bare `{ url }` | ||
| object without `document_id` is invalid — pass the plain string instead. The client rejects these | ||
| wire-invalid shapes locally before spending a request. | ||
| ## Ed25519 receipts, verifiable offline | ||
| Receipts are Ed25519-signed. Anyone can verify one offline with the open verifier | ||
| (`@kaval/receipt-verifier` in the main Kaval repo) against the published JWK at | ||
| `GET /v1/proof-verification-keys/:kid` — no Kaval account required. | ||
| ## Honest boundaries | ||
| Demo results carry no organizational authority. A production `ALLOW` requires a customer-bound | ||
| action policy and applicable empirical calibration; `REVIEW` is never permission. | ||
| ## Safe retries and idempotency | ||
@@ -242,4 +128,4 @@ | ||
| const operationId = crypto.randomUUID(); | ||
| const decision = await kaval.verify( | ||
| { belief: "Acme's CEO is Jane Doe" }, | ||
| const proof = await kaval.audit( | ||
| { text: "Acme is eligible for a $12,000 refund", as_of: new Date().toISOString() }, | ||
| { idempotencyKey: operationId }, | ||
@@ -255,25 +141,25 @@ ); | ||
| ## Pick a speed/depth tier | ||
| All billable methods accept `{ idempotencyKey?, signal?, timeoutMs? }`. The constructor defaults to | ||
| a 30-second deadline; override per call or set `timeoutMs: null` to disable it. Cancellation and | ||
| timeout errors retain `error.idempotencyKey`, because an interrupted billable request can be | ||
| ambiguous. | ||
| ## Legacy held-belief compatibility | ||
| The original currentness API remains available under legacy names — the server still accepts a | ||
| belief-freshness body on the same `/v1/verify` route: | ||
| ```ts | ||
| const decision = await kaval.verify({ | ||
| belief: "Acme's CEO is Jane Doe", | ||
| mode: "deep", | ||
| }); | ||
| decision.tier; // "deep" — the tier that ran (echoes your `mode`) | ||
| decision.explanation?.content; // deep only: a cited, markdown rationale with [n] citations | ||
| decision.explanation?.citations; // [{ url, title? }] — drawn only from the gathered evidence | ||
| decision.explanation?.confidence; // "high" | "medium" | "low" | ||
| const decision = await kaval.verifyBelief("Acme's CEO is Jane Doe"); | ||
| if (!decision.act) { | ||
| // stale / contradicted — re-fetch before relying on it | ||
| } | ||
| ``` | ||
| `mode` selects the tier (default `auto`): | ||
| `verifyBelief()` returns the verdict plus `act` — `true` only when the belief is `current` and | ||
| confident (≥ 0.7 by default; override with `minConfidence`). `mode` selects a speed/depth tier | ||
| (`instant` | `fast` | `auto` | `deep`); the deep tier adds a cited `explanation`. The related legacy | ||
| surfaces also still work: `check`, `extractAndCheck`, `scanStore`, `monitor`, `kaval`, `kavalBatch`, | ||
| and `reportOutcome`. | ||
| - **`instant`** — cache / graph-prior only, no fetch or LLM; fastest, answers from what's already known. | ||
| - **`fast`** — a cheap model, origin-only. | ||
| - **`auto`** — balanced (the default). | ||
| - **`deep`** — the strongest model + a synthesized, inline-cited `explanation` for audit/human review. | ||
| ## Sweep a store for drift | ||
| ```ts | ||
@@ -291,7 +177,8 @@ const report = await kaval.scanStore({ | ||
| `searchOffers` · `streamOfferSearch` · `gateOfferSearch` · `audit` · `gateAction` (`gate` alias) · `verify` · `check` · `extractAndCheck` · `scanStore` · | ||
| `monitor` · `reportOutcome` · `kaval` · `kavalBatch` · `health`. Billable methods accept a final | ||
| `{ idempotencyKey?, signal?, timeoutMs? }` request-options argument (`kavalBatch` includes it alongside | ||
| `concurrency`). Construct with `{ apiKey, baseUrl?, fetch?, timeoutMs? }` — `baseUrl` defaults to | ||
| `https://api.usekaval.com`. Works in Node 18+, browsers, and edge runtimes (uses the global `fetch`). | ||
| `audit` · `gate` (`gateAction` alias) · `verify` · `verifyBelief` · `check` · `extractAndCheck` · | ||
| `scanStore` · `monitor` · `reportOutcome` · `kaval` · `kavalBatch` · `health`. Billable methods | ||
| accept a final `{ idempotencyKey?, signal?, timeoutMs? }` request-options argument (`kavalBatch` | ||
| includes it alongside `concurrency`). Construct with `{ apiKey, baseUrl?, fetch?, timeoutMs? }` — | ||
| `baseUrl` defaults to `https://api.usekaval.com`. Works in Node 18+, browsers, and edge runtimes | ||
| (uses the global `fetch`). | ||
@@ -298,0 +185,0 @@ **Env vars:** this package does **not** read `KAVAL_BASE_URL` from the environment — pass |
| /** Public REST request and response types for the current review-only Offer Search surface. */ | ||
| export type ProductIdentifierScheme = "gtin" | "upc" | "ean" | "isbn" | "mpn" | "manufacturer_sku" | "model"; | ||
| export interface ProductIdentifier { | ||
| scheme: ProductIdentifierScheme; | ||
| value: string; | ||
| issuer?: string; | ||
| } | ||
| export interface ProductAttribute { | ||
| key: string; | ||
| value: string | number | boolean; | ||
| unit?: string; | ||
| } | ||
| export interface PackSpec { | ||
| count: number; | ||
| units_per_item?: number; | ||
| unit?: string; | ||
| } | ||
| export interface ProductTarget { | ||
| schema_revision: number; | ||
| family?: { | ||
| brand?: string; | ||
| name?: string; | ||
| category?: string; | ||
| }; | ||
| name?: string; | ||
| identifiers: ProductIdentifier[]; | ||
| attributes: ProductAttribute[]; | ||
| pack?: PackSpec; | ||
| } | ||
| export interface ProductFamily { | ||
| schema_revision: number; | ||
| family_id: string; | ||
| brand: string; | ||
| name: string; | ||
| category?: string; | ||
| identifiers: ProductIdentifier[]; | ||
| } | ||
| export interface ProductVariant { | ||
| schema_revision: number; | ||
| variant_id: string; | ||
| family: ProductFamily; | ||
| name: string; | ||
| identifiers: ProductIdentifier[]; | ||
| attributes: ProductAttribute[]; | ||
| pack: PackSpec; | ||
| } | ||
| export type ProductCondition = "new" | "open_box" | "refurbished" | "used_like_new" | "used_good" | "used_acceptable" | "unknown"; | ||
| export type SellerKind = "brand_direct" | "authorized_retailer" | "marketplace" | "independent_retailer" | "unknown"; | ||
| interface SubstitutionBase { | ||
| rule_id: string; | ||
| rationale: string; | ||
| maximum_materiality: "low" | "medium" | "high" | "critical"; | ||
| } | ||
| export type PermittedSubstitution = (SubstitutionBase & { | ||
| kind: "attribute"; | ||
| key: string; | ||
| requested_value: string | number | boolean; | ||
| permitted_value: string | number | boolean; | ||
| requested_unit?: string; | ||
| permitted_unit?: string; | ||
| }) | (SubstitutionBase & { | ||
| kind: "pack"; | ||
| requested: PackSpec; | ||
| permitted: PackSpec; | ||
| }) | (SubstitutionBase & { | ||
| kind: "condition"; | ||
| requested: ProductCondition; | ||
| permitted: ProductCondition; | ||
| }) | (SubstitutionBase & { | ||
| kind: "variant"; | ||
| requested_identifiers: ProductIdentifier[]; | ||
| permitted_variant_id: string; | ||
| permitted_identifiers: ProductIdentifier[]; | ||
| }); | ||
| export interface OfferSearchInput { | ||
| schema_revision: number; | ||
| request_id: string; | ||
| raw_description: string; | ||
| target: ProductTarget; | ||
| requested_condition: ProductCondition; | ||
| destination: { | ||
| country_code: string; | ||
| region?: string; | ||
| postal_code?: string; | ||
| }; | ||
| match_policy: { | ||
| identity_requirement: "shared_identifier" | "shared_identifier_or_complete_attributes"; | ||
| required_identifier_schemes: ProductIdentifierScheme[]; | ||
| required_attribute_keys: string[]; | ||
| permitted_substitutions: PermittedSubstitution[]; | ||
| }; | ||
| seller_policy: { | ||
| allowed_seller_ids: string[]; | ||
| blocked_seller_ids: string[]; | ||
| allowed_kinds: SellerKind[]; | ||
| require_authorized: boolean; | ||
| }; | ||
| destination_policy: { | ||
| require_eligible: boolean; | ||
| require_exact_region: boolean; | ||
| require_exact_postal_code: boolean; | ||
| }; | ||
| price_policy: { | ||
| currency: string; | ||
| maximum_landed_total_minor?: number; | ||
| require_complete_landed_total: boolean; | ||
| allow_estimated_components: boolean; | ||
| allow_member_price: boolean; | ||
| allow_subscription_price: boolean; | ||
| allow_coupon_price: boolean; | ||
| allow_installment_display: boolean; | ||
| allow_trade_in_price: boolean; | ||
| }; | ||
| source_policy: { | ||
| allowed_source_ids: string[]; | ||
| blocked_source_ids: string[]; | ||
| require_origin_evidence: boolean; | ||
| }; | ||
| intended_action: { | ||
| description: string; | ||
| materiality: "low" | "medium" | "high" | "critical"; | ||
| reversibility: "reversible" | "partially_reversible" | "irreversible"; | ||
| }; | ||
| freshness_maximum_age_ms: number; | ||
| max_results: number; | ||
| minimum_unique_sellers: number; | ||
| deadline_ms: number; | ||
| maximum_cost_micro_usd: number; | ||
| maximum_search_calls: number; | ||
| maximum_fetches: number; | ||
| } | ||
| export interface Money { | ||
| amount_minor: number; | ||
| currency: string; | ||
| } | ||
| export interface ExtractedOriginOffer { | ||
| evidence_kind: "json_ld" | "embedded_product_json" | "product_meta"; | ||
| source_block_index: number; | ||
| jsonld_product_index: number; | ||
| jsonld_offer_index: number | null; | ||
| variant: ProductVariant; | ||
| title: string; | ||
| purchase_url: string; | ||
| seller_name: string | null; | ||
| condition: ProductCondition; | ||
| availability: "in_stock" | "out_of_stock" | "preorder" | "unknown"; | ||
| item_price: Money | null; | ||
| destination_eligibility: "unknown"; | ||
| landed_price_complete: false; | ||
| extraction_gaps: string[]; | ||
| } | ||
| export type OfferConflictCode = "FAMILY_BRAND_CONFLICT" | "FAMILY_NAME_CONFLICT" | "IDENTIFIER_CONFLICT" | "IDENTIFIER_AMBIGUOUS" | "IDENTIFIER_MISSING" | "ATTRIBUTE_CONFLICT" | "ATTRIBUTE_MISSING" | "PACK_CONFLICT" | "PACK_INCOMPLETE" | "CONDITION_CONFLICT" | "SELLER_BLOCKED" | "SELLER_NOT_ALLOWED" | "SELLER_KIND_NOT_ALLOWED" | "SELLER_AUTHORIZATION_REQUIRED" | "DESTINATION_CONFLICT" | "DESTINATION_INELIGIBLE" | "DESTINATION_UNKNOWN" | "CURRENCY_CONFLICT" | "PRICE_LIMIT_EXCEEDED" | "PRICE_INCOMPLETE" | "MATERIAL_EVIDENCE_MISSING" | "OBSERVATION_EXPIRED"; | ||
| export interface OfferMatchAssessment { | ||
| state: "exact" | "permitted_substitute" | "ambiguous" | "conflict" | "insufficient_identity"; | ||
| conflict_codes: OfferConflictCode[]; | ||
| matched_identifier_schemes: ProductIdentifierScheme[]; | ||
| matched_attribute_keys: string[]; | ||
| applied_substitutions: PermittedSubstitution[]; | ||
| explanation: string; | ||
| } | ||
| export interface LiveOfferSearchCandidate { | ||
| candidate_id: `sha256:${string}`; | ||
| origin_url: string; | ||
| source_id: string; | ||
| discovered_by: string[]; | ||
| discovery_metadata: Array<{ | ||
| provider: string; | ||
| title: string | null; | ||
| }>; | ||
| origin_evidence: { | ||
| kind: ExtractedOriginOffer["evidence_kind"]; | ||
| content_digest: `sha256:${string}`; | ||
| source_block_index: number; | ||
| jsonld_product_index: number; | ||
| jsonld_offer_index: number | null; | ||
| }; | ||
| origin_offer: ExtractedOriginOffer; | ||
| identity: OfferMatchAssessment; | ||
| /** Current shadow output can only be queued for review or rejected. */ | ||
| disposition: "review" | "rejected"; | ||
| gaps: string[]; | ||
| reason_codes: string[]; | ||
| /** Destination-aware checkout evidence. Its action remains REVIEW-only. */ | ||
| checkout?: CommerceCheckoutVerification; | ||
| } | ||
| export type CommerceSourceFamily = "catalog" | "merchant_feed" | "retailer_origin" | "shopping_search" | "open_web"; | ||
| export interface CommerceCheckoutResolverDescriptor { | ||
| schema_revision: 1; | ||
| source_id: string; | ||
| adapter_revision: string; | ||
| execution_mode: "recorded_fixture" | "live"; | ||
| estimated_cost_micro_usd: number; | ||
| } | ||
| export interface CommerceCheckoutObservation { | ||
| destination_eligibility: "eligible" | "ineligible" | "unknown"; | ||
| availability: "in_stock" | "out_of_stock" | "preorder" | "unknown"; | ||
| seller_authorized: boolean | null; | ||
| item_price: Money | null; | ||
| shipping_price: Money | null; | ||
| tax_price: Money | null; | ||
| mandatory_fees: Money | null; | ||
| declared_landed_total: Money | null; | ||
| quote_id: string | null; | ||
| evidence_digest: `sha256:${string}`; | ||
| observed_at: string; | ||
| expires_at: string; | ||
| } | ||
| export type LandedPriceValidationReason = "EXPECTED_CURRENCY_INVALID" | "ITEM_PRICE_MISSING" | "SHIPPING_PRICE_MISSING" | "TAX_PRICE_MISSING" | "MANDATORY_FEES_MISSING" | "DECLARED_LANDED_TOTAL_MISSING" | "MONEY_VALUE_INVALID" | "PRICE_CURRENCY_CONFLICT" | "LANDED_TOTAL_OVERFLOW" | "LANDED_TOTAL_ARITHMETIC_MISMATCH"; | ||
| export interface LandedPriceValidation { | ||
| state: "complete" | "incomplete" | "invalid" | "inconsistent"; | ||
| expected_currency: string; | ||
| calculated_landed_total: Money | null; | ||
| reason_codes: LandedPriceValidationReason[]; | ||
| } | ||
| export interface CommerceCheckoutVerification { | ||
| status: "verified" | "review_required" | "rejected" | "operational_failure"; | ||
| resolver: CommerceCheckoutResolverDescriptor | null; | ||
| request_digest: `sha256:${string}`; | ||
| observation: CommerceCheckoutObservation | null; | ||
| landed_price_validation: LandedPriceValidation; | ||
| action: { | ||
| state: "REVIEW"; | ||
| action_authorized: false; | ||
| reason_codes: string[]; | ||
| }; | ||
| actual_cost_micro_usd: number; | ||
| version_receipt: string | null; | ||
| operational_error_code: "UPSTREAM_UNAVAILABLE" | "DESTINATION_UNSUPPORTED" | "MALFORMED_RESPONSE" | "RIGHTS_REVOKED" | "CANCELLED" | null; | ||
| } | ||
| export interface CommercePlannedSource { | ||
| source_id: string; | ||
| family: CommerceSourceFamily; | ||
| call_kind: "search" | "fetch"; | ||
| independence_group: string; | ||
| estimated_cost_micro_usd: number; | ||
| field_guarantees: string[]; | ||
| health_state: "healthy" | "degraded"; | ||
| concurrency_limit: number; | ||
| supports_cancellation: boolean; | ||
| role: "structured_acquisition" | "origin_verification" | "discovery_tail"; | ||
| winner_must_be_origin_verified: boolean; | ||
| } | ||
| export interface CommerceSourcePlan { | ||
| schema_revision: number; | ||
| request_id: string; | ||
| request_digest: `sha256:${string}`; | ||
| supplier_registry_schema_revision: number; | ||
| supplier_registry_digest: `sha256:${string}`; | ||
| waves: Array<{ | ||
| wave: number; | ||
| purpose: "structured_authoritative" | "retailer_origin" | "unresolved_identity_and_coverage"; | ||
| sources: CommercePlannedSource[]; | ||
| }>; | ||
| receipt: { | ||
| schema_revision: number; | ||
| request_id: string; | ||
| coverage_claim: "bounded_not_comprehensive"; | ||
| name_only_target: boolean; | ||
| minimum_independent_families_required: number; | ||
| planned_independent_families: CommerceSourceFamily[]; | ||
| planned_independence_groups: string[]; | ||
| independence_requirement_met: boolean; | ||
| origin_verification_required: true; | ||
| origin_verification_planned: boolean; | ||
| origin_verification_source_ids: string[]; | ||
| eligible_supplier_count_before_budget: number; | ||
| total_planned_cost_micro_usd: number; | ||
| total_planned_search_calls: number; | ||
| total_planned_fetches: number; | ||
| exclusions: Array<{ | ||
| source_id: string; | ||
| family: CommerceSourceFamily; | ||
| call_kind: "search" | "fetch"; | ||
| estimated_cost_micro_usd: number; | ||
| reason: string; | ||
| }>; | ||
| }; | ||
| } | ||
| export interface CommerceAcquisitionSourceLedgerEntry { | ||
| source_id: string; | ||
| family: CommerceSourceFamily; | ||
| disposition: "succeeded" | "failed" | "cancelled" | "prohibited" | "deferred" | "unsearched"; | ||
| reason_code: string; | ||
| } | ||
| export interface CommerceAcquisitionRunReport { | ||
| schema_revision: 1; | ||
| request_digest: `sha256:${string}`; | ||
| plan: CommerceSourcePlan; | ||
| /** Full planner state is retained for audit/replay and may add fields within schema revision 1. */ | ||
| state: Readonly<Record<string, unknown>>; | ||
| stop: { | ||
| reason: Exclude<OfferSearchStopReason, "sufficient_offers">; | ||
| explanation: string; | ||
| }; | ||
| calls: Array<Readonly<Record<string, unknown>>>; | ||
| records: Array<Readonly<Record<string, unknown>>>; | ||
| source_ledger: CommerceAcquisitionSourceLedgerEntry[]; | ||
| coverage: { | ||
| claim: "bounded_not_comprehensive"; | ||
| attempted_source_families: CommerceSourceFamily[]; | ||
| unique_candidate_keys: number; | ||
| unique_sellers: number; | ||
| unsearched_source_count: number; | ||
| prohibited_source_count: number; | ||
| failed_source_count: number; | ||
| }; | ||
| deduplication: { | ||
| source_records: number; | ||
| unique_urls: number; | ||
| unique_variants: number; | ||
| unique_sellers: number; | ||
| unique_listings: number; | ||
| unique_offers: number; | ||
| independent_information_origins: number; | ||
| }; | ||
| replay_digest: `sha256:${string}`; | ||
| } | ||
| export interface LiveOfferSearchAcquisitionTrace { | ||
| coverage_claim: "bounded_not_comprehensive"; | ||
| plan: CommerceSourcePlan; | ||
| plan_digest: `sha256:${string}`; | ||
| source_ledger: CommerceAcquisitionSourceLedgerEntry[]; | ||
| adapter_run?: CommerceAcquisitionRunReport; | ||
| } | ||
| /** Digests that bind one persisted evidence generation to one exact downstream action slot. */ | ||
| export interface CommerceActionBinding { | ||
| action_slot_key: string; | ||
| action_input_digest: `sha256:${string}`; | ||
| action_consequence_digest: `sha256:${string}`; | ||
| } | ||
| export type CommerceActionTimeGateState = "current_review_only" | "not_found" | "stale_generation" | "binding_mismatch" | "expired" | "invalidated" | "refresh_required" | "source_revoked" | "retention_unavailable" | "integrity_failed" | "operational_failure"; | ||
| /** Exact body accepted by POST /v1/search-offers/gate. Tenant identity is server-derived. */ | ||
| export interface CommerceActionTimeGateInput { | ||
| dependency_id: string; | ||
| generation_id: string; | ||
| generation_number: number; | ||
| generation_digest: `sha256:${string}`; | ||
| action_binding: CommerceActionBinding; | ||
| } | ||
| /** | ||
| * A final-fence read of one persisted offer generation. Commerce remains review-only: even a | ||
| * current generation returns REVIEW with permission withheld. | ||
| */ | ||
| export interface CommerceActionTimeGateResult { | ||
| state: CommerceActionTimeGateState; | ||
| disposition: "REVIEW"; | ||
| permission: "withheld"; | ||
| reason_codes: string[]; | ||
| checked_at: string; | ||
| final_fence_checked: boolean; | ||
| generation_id?: string; | ||
| generation_number?: number; | ||
| generation_digest?: `sha256:${string}`; | ||
| expires_at?: string; | ||
| } | ||
| export type CommerceOfferSearchLifecycle = { | ||
| persistence: "persisted"; | ||
| dependency_id: string; | ||
| generation_id: string; | ||
| generation_number: number; | ||
| generation_digest: `sha256:${string}`; | ||
| selected_candidate_id: `sha256:${string}`; | ||
| expires_at: string; | ||
| action_binding: CommerceActionBinding; | ||
| action_time_gate: CommerceActionTimeGateResult; | ||
| } | { | ||
| persistence: "not_created"; | ||
| reason_codes: string[]; | ||
| action_time_gate: Pick<CommerceActionTimeGateResult, "disposition" | "permission" | "reason_codes" | "checked_at" | "final_fence_checked"> & { | ||
| state: "not_found"; | ||
| }; | ||
| }; | ||
| export type CommerceSourceAttemptErrorCode = "INVALID_DISCOVERY_URL" | "DISCOVERY_IDENTIFIER_MISMATCH" | "ORIGIN_BLOCKED" | "ORIGIN_HTTP_ERROR" | "ORIGIN_JSONLD_INVALID" | "ORIGIN_TIMEOUT" | "ORIGIN_UNAVAILABLE" | "SEARCH_UNAVAILABLE" | "BUDGET_EXHAUSTED" | "DEADLINE_REACHED" | "CANCELLED" | "COVERAGE_SATISFIED"; | ||
| export interface CommerceLiveSourceAttempt { | ||
| sequence: number; | ||
| kind: "search" | "origin_fetch"; | ||
| call_attempted: boolean; | ||
| source_id: string; | ||
| provider: string | null; | ||
| query: string | null; | ||
| url: string | null; | ||
| outcome: "succeeded" | "empty" | "failed" | "blocked" | "skipped" | "cancelled"; | ||
| error_code: CommerceSourceAttemptErrorCode | null; | ||
| latency_ms: number; | ||
| cost_micro_usd: number; | ||
| reuse: "executed" | "tenant_private_cache"; | ||
| avoided_cost_micro_usd: number; | ||
| result_count: number | null; | ||
| http_status: number | null; | ||
| bytes_received: number | null; | ||
| } | ||
| export type OfferSearchStopReason = "coverage_satisfied" | "sufficient_offers" | "source_exhausted" | "budget_exhausted" | "deadline_reached" | "cancelled" | "upstream_unavailable" | "policy_blocked"; | ||
| export interface LiveOfferSearchResult { | ||
| schema_revision: 2; | ||
| request_id: string; | ||
| request_digest: `sha256:${string}`; | ||
| status: "complete" | "partial" | "failed"; | ||
| /** Offer Search is shadow-only and cannot authorize a quote or purchase. */ | ||
| action: { | ||
| state: "NEEDS_REVIEW" | "NO_RELIABLE_OFFER"; | ||
| reason_codes: string[]; | ||
| }; | ||
| stop_reason: OfferSearchStopReason; | ||
| query: string | null; | ||
| candidates: LiveOfferSearchCandidate[]; | ||
| source_attempts: CommerceLiveSourceAttempt[]; | ||
| receipt: { | ||
| search_calls: number; | ||
| fetch_calls: number; | ||
| providers_configured: number; | ||
| providers_succeeded: number; | ||
| cost_micro_usd: number; | ||
| cost_basis: "reserved_ceiling"; | ||
| provider_estimated_cost_micro_usd: number | null; | ||
| provider_estimated_cost_reported_search_calls: number; | ||
| discovery_cache_hits: number; | ||
| cost_avoided_micro_usd: number; | ||
| elapsed_ms: number; | ||
| }; | ||
| started_at: string; | ||
| completed_at: string; | ||
| /** Auditable rights, coverage, and attempted-source trace. */ | ||
| acquisition?: LiveOfferSearchAcquisitionTrace; | ||
| /** Present only when the hosted server has a configured durable commerce lifecycle. */ | ||
| lifecycle?: CommerceOfferSearchLifecycle; | ||
| } | ||
| export type OfferSearchProgressStage = "accepted" | "acquisition" | "verification" | "coverage" | "candidate_provisional" | "candidate" | "warning"; | ||
| interface OfferSearchProgressEventBase { | ||
| sequence: number; | ||
| at: string; | ||
| request_id: string; | ||
| message: string; | ||
| authority: "research_only"; | ||
| action_state: "REVIEW"; | ||
| details: Readonly<Record<string, unknown>>; | ||
| } | ||
| export interface OfferSearchStageEvent extends OfferSearchProgressEventBase { | ||
| type: Exclude<OfferSearchProgressStage, "candidate_provisional">; | ||
| } | ||
| /** Origin-verified research observed before final selection and lifecycle persistence. */ | ||
| export interface OfferSearchProvisionalCandidateEvent extends OfferSearchProgressEventBase { | ||
| type: "candidate_provisional"; | ||
| details: Readonly<{ | ||
| request_digest: `sha256:${string}`; | ||
| origin_sequence: number; | ||
| publication_state: "provisional"; | ||
| durable: false; | ||
| actionable: false; | ||
| permission: "withheld"; | ||
| final_inclusion: "not_yet_determined"; | ||
| candidate: LiveOfferSearchCandidate; | ||
| }>; | ||
| } | ||
| export type OfferSearchProgressEvent = OfferSearchStageEvent | OfferSearchProvisionalCandidateEvent; | ||
| /** A same-key completed-operation replay performs no new provider work. */ | ||
| export interface OfferSearchReplayEvent { | ||
| type: "replay"; | ||
| sequence: number; | ||
| replayed_at: string; | ||
| request_id: string; | ||
| request_digest: `sha256:${string}`; | ||
| authority: "research_only"; | ||
| action_state: "REVIEW"; | ||
| } | ||
| export type OfferSearchStreamEvent = OfferSearchProgressEvent | OfferSearchReplayEvent | { | ||
| type: "final"; | ||
| sequence: number; | ||
| result: LiveOfferSearchResult; | ||
| }; | ||
| /** Validate a public progressive event before exposing it to an agent. */ | ||
| export declare function reviewOnlyOfferSearchProgressEvent(value: unknown): OfferSearchProgressEvent; | ||
| /** Validate the content-free event emitted for a durable same-key replay. */ | ||
| export declare function reviewOnlyOfferSearchReplayEvent(value: unknown, expectedRequestId?: string): OfferSearchReplayEvent; | ||
| /** Reject authority drift and validate the exact public action-time commerce gate response. */ | ||
| export declare function reviewOnlyCommerceActionTimeGateResult(value: unknown, expectedGeneration?: Pick<CommerceActionTimeGateInput, "generation_id" | "generation_number" | "generation_digest">): CommerceActionTimeGateResult; | ||
| /** Reject a drifted commerce response before a caller can mistake shadow research for permission. */ | ||
| export declare function reviewOnlyOfferSearchResult(value: unknown, expectedRequestId?: string): LiveOfferSearchResult; | ||
| export {}; |
| /** Public REST request and response types for the current review-only Offer Search surface. */ | ||
| function record(value) { | ||
| return typeof value === "object" && value !== null && !Array.isArray(value) | ||
| ? value | ||
| : null; | ||
| } | ||
| const COMMERCE_DIGEST = /^sha256:[0-9a-f]{64}$/u; | ||
| const ACTION_TIME_GATE_STATES = new Set([ | ||
| "current_review_only", | ||
| "not_found", | ||
| "stale_generation", | ||
| "binding_mismatch", | ||
| "expired", | ||
| "invalidated", | ||
| "refresh_required", | ||
| "source_revoked", | ||
| "retention_unavailable", | ||
| "integrity_failed", | ||
| "operational_failure", | ||
| ]); | ||
| function stringArray(value) { | ||
| return (Array.isArray(value) && value.every((item) => typeof item === "string")); | ||
| } | ||
| function digest(value) { | ||
| return typeof value === "string" && COMMERCE_DIGEST.test(value); | ||
| } | ||
| function actionBinding(value) { | ||
| const binding = record(value); | ||
| return (typeof binding?.["action_slot_key"] === "string" && | ||
| binding["action_slot_key"].length > 0 && | ||
| digest(binding["action_input_digest"]) && | ||
| digest(binding["action_consequence_digest"]) && | ||
| binding["action_input_digest"] !== binding["action_consequence_digest"]); | ||
| } | ||
| /** Detect permission-shaped fields anywhere in a commerce response, including future extensions. */ | ||
| function containsCommerceAuthority(value) { | ||
| if (Array.isArray(value)) | ||
| return value.some(containsCommerceAuthority); | ||
| const current = record(value); | ||
| if (!current) | ||
| return false; | ||
| for (const [key, nested] of Object.entries(current)) { | ||
| const authorityToken = typeof nested === "string" ? nested.toUpperCase() : undefined; | ||
| if (((key === "safe_to_quote" || | ||
| key === "action_authorized" || | ||
| key === "execution_allowed" || | ||
| key === "executionAllowed" || | ||
| key === "act") && | ||
| nested === true) || | ||
| (key === "permission" && nested !== "withheld") || | ||
| ((key === "decision" || key === "disposition" || key === "state") && | ||
| (authorityToken === "ALLOW" || | ||
| authorityToken === "BLOCK" || | ||
| authorityToken === "SAFE_TO_QUOTE")) || | ||
| containsCommerceAuthority(nested)) { | ||
| return true; | ||
| } | ||
| } | ||
| return false; | ||
| } | ||
| const OFFER_SEARCH_PROGRESS_STAGES = new Set([ | ||
| "accepted", | ||
| "acquisition", | ||
| "verification", | ||
| "coverage", | ||
| "candidate_provisional", | ||
| "candidate", | ||
| "warning", | ||
| ]); | ||
| /** Validate a public progressive event before exposing it to an agent. */ | ||
| export function reviewOnlyOfferSearchProgressEvent(value) { | ||
| const event = record(value); | ||
| if (!event || | ||
| !OFFER_SEARCH_PROGRESS_STAGES.has(event["type"]) || | ||
| !Number.isInteger(event["sequence"]) || | ||
| event["sequence"] < 0 || | ||
| typeof event["at"] !== "string" || | ||
| typeof event["request_id"] !== "string" || | ||
| typeof event["message"] !== "string" || | ||
| event["authority"] !== "research_only" || | ||
| event["action_state"] !== "REVIEW" || | ||
| record(event["details"]) === null || | ||
| containsCommerceAuthority(event)) { | ||
| throw new TypeError("Offer Search stream returned an invalid or authority-bearing progress event"); | ||
| } | ||
| if (event["type"] === "candidate_provisional") { | ||
| const details = record(event["details"]); | ||
| const candidate = record(details?.["candidate"]); | ||
| if (!details || | ||
| !digest(details["request_digest"]) || | ||
| !Number.isInteger(details["origin_sequence"]) || | ||
| details["origin_sequence"] < 0 || | ||
| details["publication_state"] !== "provisional" || | ||
| details["durable"] !== false || | ||
| details["actionable"] !== false || | ||
| details["permission"] !== "withheld" || | ||
| details["final_inclusion"] !== "not_yet_determined" || | ||
| !candidate || | ||
| !digest(candidate["candidate_id"]) || | ||
| typeof candidate["origin_url"] !== "string" || | ||
| typeof candidate["source_id"] !== "string" || | ||
| (candidate["disposition"] !== "review" && | ||
| candidate["disposition"] !== "rejected")) { | ||
| throw new TypeError("Offer Search stream returned an invalid provisional candidate event"); | ||
| } | ||
| } | ||
| return value; | ||
| } | ||
| /** Validate the content-free event emitted for a durable same-key replay. */ | ||
| export function reviewOnlyOfferSearchReplayEvent(value, expectedRequestId) { | ||
| const event = record(value); | ||
| if (!event || | ||
| event["type"] !== "replay" || | ||
| !Number.isInteger(event["sequence"]) || | ||
| event["sequence"] < 0 || | ||
| typeof event["replayed_at"] !== "string" || | ||
| typeof event["request_id"] !== "string" || | ||
| event["request_id"].length === 0 || | ||
| !digest(event["request_digest"]) || | ||
| (expectedRequestId !== undefined && | ||
| event["request_id"] !== expectedRequestId) || | ||
| event["authority"] !== "research_only" || | ||
| event["action_state"] !== "REVIEW" || | ||
| containsCommerceAuthority(event)) { | ||
| throw new TypeError("Offer Search stream returned an invalid or authority-bearing replay event"); | ||
| } | ||
| return value; | ||
| } | ||
| /** Reject authority drift and validate the exact public action-time commerce gate response. */ | ||
| export function reviewOnlyCommerceActionTimeGateResult(value, expectedGeneration) { | ||
| const gate = record(value); | ||
| if (!gate || | ||
| !ACTION_TIME_GATE_STATES.has(gate["state"]) || | ||
| gate["disposition"] !== "REVIEW" || | ||
| gate["permission"] !== "withheld" || | ||
| !stringArray(gate["reason_codes"]) || | ||
| typeof gate["checked_at"] !== "string" || | ||
| typeof gate["final_fence_checked"] !== "boolean" || | ||
| (gate["generation_id"] !== undefined && | ||
| typeof gate["generation_id"] !== "string") || | ||
| (gate["generation_number"] !== undefined && | ||
| (!Number.isInteger(gate["generation_number"]) || | ||
| gate["generation_number"] <= 0)) || | ||
| (gate["generation_digest"] !== undefined && | ||
| !digest(gate["generation_digest"])) || | ||
| (gate["expires_at"] !== undefined && | ||
| typeof gate["expires_at"] !== "string") || | ||
| containsCommerceAuthority(gate)) { | ||
| throw new TypeError("Offer Search action-time gate returned an invalid or authority-bearing response; commerce permission must remain withheld"); | ||
| } | ||
| if (gate["state"] === "current_review_only" && | ||
| (gate["final_fence_checked"] !== true || | ||
| typeof gate["generation_id"] !== "string" || | ||
| gate["generation_id"].length === 0 || | ||
| !Number.isInteger(gate["generation_number"]) || | ||
| gate["generation_number"] <= 0 || | ||
| !digest(gate["generation_digest"]) || | ||
| (expectedGeneration !== undefined && | ||
| (gate["generation_id"] !== expectedGeneration.generation_id || | ||
| gate["generation_number"] !== expectedGeneration.generation_number || | ||
| gate["generation_digest"] !== expectedGeneration.generation_digest)))) { | ||
| throw new TypeError("Offer Search action-time gate returned an invalid or authority-bearing response; commerce permission must remain withheld"); | ||
| } | ||
| return value; | ||
| } | ||
| function commerceLifecycle(value, candidates) { | ||
| const lifecycle = record(value); | ||
| if (lifecycle?.["persistence"] === "persisted") { | ||
| if (typeof lifecycle["dependency_id"] !== "string" || | ||
| typeof lifecycle["generation_id"] !== "string" || | ||
| !Number.isInteger(lifecycle["generation_number"]) || | ||
| lifecycle["generation_number"] <= 0 || | ||
| !digest(lifecycle["generation_digest"]) || | ||
| !digest(lifecycle["selected_candidate_id"]) || | ||
| typeof lifecycle["expires_at"] !== "string" || | ||
| !actionBinding(lifecycle["action_binding"])) { | ||
| throw new TypeError("Offer Search returned invalid lifecycle metadata"); | ||
| } | ||
| const selectedCandidateMatches = candidates.filter((candidate) => record(candidate)?.["candidate_id"] === | ||
| lifecycle["selected_candidate_id"]).length; | ||
| if (selectedCandidateMatches !== 1) { | ||
| throw new TypeError("Offer Search returned invalid lifecycle metadata"); | ||
| } | ||
| const expectedGeneration = { | ||
| generation_id: lifecycle["generation_id"], | ||
| generation_number: lifecycle["generation_number"], | ||
| generation_digest: lifecycle["generation_digest"], | ||
| }; | ||
| const gate = reviewOnlyCommerceActionTimeGateResult(lifecycle["action_time_gate"], expectedGeneration); | ||
| if ((gate.generation_id !== undefined && | ||
| gate.generation_id !== expectedGeneration.generation_id) || | ||
| (gate.generation_number !== undefined && | ||
| gate.generation_number !== expectedGeneration.generation_number) || | ||
| (gate.generation_digest !== undefined && | ||
| gate.generation_digest !== expectedGeneration.generation_digest)) { | ||
| throw new TypeError("Offer Search returned invalid lifecycle metadata"); | ||
| } | ||
| return value; | ||
| } | ||
| if (lifecycle?.["persistence"] === "not_created" && | ||
| stringArray(lifecycle["reason_codes"])) { | ||
| const gate = reviewOnlyCommerceActionTimeGateResult(lifecycle["action_time_gate"]); | ||
| if (gate.state === "not_found") { | ||
| return value; | ||
| } | ||
| } | ||
| throw new TypeError("Offer Search returned invalid lifecycle metadata"); | ||
| } | ||
| /** Reject a drifted commerce response before a caller can mistake shadow research for permission. */ | ||
| export function reviewOnlyOfferSearchResult(value, expectedRequestId) { | ||
| const result = record(value); | ||
| if (typeof result?.["request_id"] !== "string" || | ||
| result["request_id"].length === 0 || | ||
| !digest(result["request_digest"])) { | ||
| throw new TypeError("Offer Search returned an invalid request ID or digest binding"); | ||
| } | ||
| if (expectedRequestId !== undefined && | ||
| result["request_id"] !== expectedRequestId) { | ||
| throw new TypeError("Offer Search result is bound to another request"); | ||
| } | ||
| const action = record(result?.["action"]); | ||
| const candidates = result?.["candidates"]; | ||
| if (result?.["schema_revision"] !== 2 || | ||
| result?.["decision"] === "ALLOW" || | ||
| result?.["safe_to_quote"] === true || | ||
| (action?.["state"] !== "NEEDS_REVIEW" && | ||
| action?.["state"] !== "NO_RELIABLE_OFFER") || | ||
| action?.["decision"] === "ALLOW" || | ||
| action?.["safe_to_quote"] === true || | ||
| containsCommerceAuthority(result) || | ||
| !Array.isArray(candidates) || | ||
| candidates.some((candidate) => { | ||
| const candidateRecord = record(candidate); | ||
| const disposition = candidateRecord?.["disposition"]; | ||
| return ((disposition !== "review" && disposition !== "rejected") || | ||
| candidateRecord?.["safe_to_quote"] === true); | ||
| })) { | ||
| throw new TypeError("Offer Search returned a non-review-only response; shadow results cannot authorize an action"); | ||
| } | ||
| if (result["lifecycle"] !== undefined) { | ||
| commerceLifecycle(result["lifecycle"], candidates); | ||
| } | ||
| return value; | ||
| } |
64251
-37.58%7
-22.22%1108
-42.02%184
-38.05%