🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@usekaval/kaval

Package Overview
Dependencies
Maintainers
1
Versions
12
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@usekaval/kaval - npm Package Compare versions

Comparing version
0.4.0
to
0.5.0
+34
-26
dist/index.d.ts
/**
* @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

/**
* @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);

@@ -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";

{
"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;
}