import type { ReviewQueueSessionState } from "./review-queue-session.js"; import { reviewResourceApiVersion, type ReviewItem } from "../review-resource.js"; import { type ExtractionEnvelopeImport, type ExtractionEnvelopeImportResult } from "../extraction-envelope.js"; /** * The durable attestation record. Serializable; a consumer stores it beside the * queue when the round opens and presents it, unchanged, at every later * validation. * * `itemNames` is deliberately redundant with the hash: it is what makes a * refusal diagnosable (which item was removed or added, by name) and it keeps * the set comparison independent of the hash derivation. */ export interface ReviewQueueBinding { readonly apiVersion: typeof reviewResourceApiVersion; readonly kind: "ReviewQueueBinding"; readonly spec: { readonly sessionName: string; /** sha256 of the canonical open-time snapshot; see {@link hashReviewQueueSnapshot}. */ readonly snapshotHash: string; /** Sorted, unique names of every ReviewItem the binding covers. Never empty. */ readonly itemNames: readonly string[]; /** ISO timestamp of when the binding was taken. Informational, not trusted. */ readonly boundAt: string; }; } export type ReviewQueueBindingIssueCode = "binding-malformed" | "session-name-mismatch" | "empty-queue" | "ambiguous-item-identity" | "snapshot-hash-mismatch" | "item-removed" | "item-added"; export interface ReviewQueueBindingIssue { readonly code: ReviewQueueBindingIssueCode; readonly message: string; /** The ReviewItem name a set-membership issue is about, when there is one. */ readonly itemName?: string; } export declare class UnattestedReviewQueueError extends Error { readonly name = "UnattestedReviewQueueError"; readonly issues: readonly ReviewQueueBindingIssue[]; constructor(issues: readonly ReviewQueueBindingIssue[]); } export interface BindReviewQueueOptions { readonly sessionName: string; /** Defaults to now. Informational only; nothing validates against it. */ readonly boundAt?: Date | string; } export interface ValidateReviewQueueBindingOptions { /** When set, the binding must name this session. */ readonly sessionName?: string; } /** * The digest a binding stores: sha256 over the canonical JSON of the whole * open-time session state. Byte-identical to server-review-session's * `hashReviewSessionSnapshot` (pinned by test), so a consumer already * persisting that digest adopts the binding without invalidating stored state. */ export declare function hashReviewQueueSnapshot(snapshot: ReviewQueueSessionState): string; /** * Take the binding for a queue, once, when the round/session opens. * * Call this at queue construction and persist the result beside the queue. * Calling it again later, on bytes that may have changed, produces a binding * that agrees with whatever it was given — which is the self-agreement bypass, * not an attestation. * * Refuses an empty queue: a binding over nothing validates nothing, and every * later check against it would be vacuously green. Refuses duplicate item * names: the binding's set comparison is by name, so an ambiguous name would * let two different items answer for one membership. */ export declare function bindReviewQueue(snapshot: ReviewQueueSessionState, options: BindReviewQueueOptions): ReviewQueueBinding; /** * Compare a presented queue against its stored binding. * * `binding` must be the record persisted when the round opened — passing one * derived from `snapshot` here checks nothing (see module doc). `snapshot` is * the base queue the binding was taken over, not the state after event replay: * decisions live in the event log precisely so the bound bytes never move. * * A malformed binding fails closed with `binding-malformed` rather than * skipping the checks it cannot perform. */ export declare function validateReviewQueueBinding(binding: ReviewQueueBinding, snapshot: ReviewQueueSessionState, options?: ValidateReviewQueueBindingOptions): ReviewQueueBindingIssue[]; export declare function assertReviewQueueBinding(binding: ReviewQueueBinding, snapshot: ReviewQueueSessionState, options?: ValidateReviewQueueBindingOptions): void; export type ReviewQueueExtractionIssueCode = "import-invalid" | "import-not-grounded" | "empty-queue" | "item-missing-from-queue" | "item-not-in-extraction" | "item-diverges-from-extraction"; export interface ReviewQueueExtractionIssue { readonly code: ReviewQueueExtractionIssueCode; readonly message: string; readonly itemName?: string; } export declare class UnattestedExtractionQueueError extends Error { readonly name = "UnattestedExtractionQueueError"; readonly issues: readonly ReviewQueueExtractionIssue[]; constructor(issues: readonly ReviewQueueExtractionIssue[]); } /** * Check a stored queue for consistency with the extraction import record it * was derived from. * * The queue binding alone cannot catch a writer who edits the queue and * re-binds: hashing mutated bytes yields a self-consistent pair. This check * closes the QUEUE half of that hole: it revalidates the presented record * through the public import boundary (a forged `grounded` status throws; an * ungrounded import is refused), re-derives the canonical ReviewItems from it, * and requires the stored queue to be the SAME SET, byte-identically per item, * in both directions. A queue edited independently of its record fails. * * What it does NOT attest: the record itself. The envelope carries the * prepared artifact's digest and contentLength, never the prepared bytes, so * a library handed only the record cannot verify a proposal's bytes against * the digested artifact. A writer who edits the record's proposals and * re-derives the queue from the edited record presents a pair this check * blesses, while `result.preparedArtifact.digest` still names the honest * bytes. Record integrity is therefore the caller's storage obligation, and * checking prepared bytes against that digest does not meet it — the digest * covers the artifact, not the proposals, so it stays green through the * rewrite. Meeting it takes one of: a record digest/MAC anchored where the * record's writer cannot reach, immutable or authenticated record storage, * or independently re-deriving the proposals from trusted prepared bytes and * comparing. This limit is pinned by a boundary test and by * scripts/check-guards.mjs. * * This is the whole-extraction rule: it applies to a queue whose items all come * from one import. A consumer whose rounds mix in items the extraction cannot * attest (recheck rounds against a prior observation, for one) owns that * dispatch and those semantics — deciding which attestation applies to which * item from a mutable label is bypass 3, and it stays with the data that can * cross-check the label. */ export declare function validateReviewQueueAgainstExtractionImport(items: readonly ReviewItem[], importResult: ExtractionEnvelopeImportResult): ReviewQueueExtractionIssue[]; /** * What a reload path can say about a stored queue and the extraction import it * came from: * * - `attested`: an import record was supplied and the queue matches it (see * {@link validateReviewQueueAgainstExtractionImport}). * - `diverges`: an import record was supplied and the queue does not match it, * or the record itself is invalid. The queue must not be reviewed. * - `unverified`: items carry the extraction-envelope binding but no import * record was supplied, so nothing checked them. A rival value deleted from * the stored queue would not show; every surface says so. * - `not-extraction`: no item carries the binding and no record was supplied; * there is no import to check against. * * Detection reads the binding the import writes on every item and candidate. * A writer that strips it from every item also strips every other extraction * fact from the queue; keeping the stored import record beside the queue is * what makes a reload path able to check it. */ export type ReviewQueueExtractionAttestation = { readonly state: "attested"; } | { readonly state: "not-extraction"; } | { readonly state: "unverified"; readonly itemNames: readonly string[]; readonly message: string; } | { readonly state: "diverges"; readonly issues: readonly ReviewQueueExtractionIssue[]; readonly message: string; }; /** Whether a ReviewItem, or any of its candidates, carries the extraction-envelope binding. */ export declare function reviewItemCarriesExtractionBinding(item: ReviewItem): boolean; /** * Check a queue a reload path is about to present against its extraction * import. Pass the import record (or import result) stored beside the queue; * pass `undefined` when the path has none, and the result says whether that * leaves the queue unverified. */ export declare function attestReviewQueueExtraction(items: readonly ReviewItem[], extractionImport: ExtractionEnvelopeImport | ExtractionEnvelopeImportResult | undefined): ReviewQueueExtractionAttestation; /** The notice every surface shows for an `unverified` queue. */ export declare function unverifiedExtractionQueueSentence(itemCount: number): string; export declare function assertReviewQueueAgainstExtractionImport(items: readonly ReviewItem[], importResult: ExtractionEnvelopeImportResult): void;