/** * Offload delivery verifier — closes the loop between the engine's * `routeState="offload"` STAMP and what actually LANDED in the customer's * offload sink (S3). * * The rest of the MCP trusts the stamp: cost / savings / commitment_report / * offload-status all read `all_events_summaryBytes_total{routeState=...}` and * NEVER check the sink. That makes two real failure modes invisible: * * 1. SILENT LOSS — the engine stamps `offload`, but the forwarder never * routes those events to S3 (missing s3 plugin, wrong bucket, bad IAM). * The metric still shows the "saving"; no bytes were actually offloaded. * * 2. COPY-EVERYTHING — the forwarder ships ALL events to the sink AND to * the SIEM (the exact shape found on the otel demo: an `@type copy` * proof config). The "offloaded" bytes never left the SIEM, so the * saving is phantom — yet the stamp reports it as real. * * This verifier issues three falsifiable checks against the LIVE sink: * - LIVENESS — recent objects exist under the bucket/prefix. * - PURITY — sampled sink objects carry ONLY `routeState="offload"`. * A sink that also holds `drop`/`pass` events is the * copy-everything tell (the forwarder isn't filtering). * - NOT-LOSS — if the engine stamped offload bytes in the window but the * sink has no recent objects, delivery is broken. * * Byte-for-byte reconciliation is deliberately NOT a pass/fail gate: S3 * stores the JSON-wrapped fullText, the metric counts engine summaryBytes — * different units. The delivered/stamped relationship is informational only. * * Dependencies are injectable so the suite drives every verdict without AWS * or a metric backend (mirrors the `retriever-probe.ts` ProbeDeps pattern). * * The sink is not always S3. An env-config destination of type `azure_blob` * is read through `az storage blob list`, and every message names the blob * container and the blob-level role rather than a bucket and an IAM policy. * `src/lib/object-store.ts` owns that dispatch; this file stays store-neutral * and renders whichever URI the target carries. */ import { type ObjectStoreKind, type ObjectStoreTarget } from './object-store.js'; /** * The honest end states. Mapped to doctor pass/warn/fail by the caller: * fail ← silent_loss, leak * warn ← unverified, stale * pass ← verified, idle, not_configured */ export type OffloadDeliveryVerdict = 'verified' | 'silent_loss' | 'leak' | 'stale' | 'idle' | 'unverified' | 'not_configured'; export interface RouteStateTally { [routeState: string]: number; } export interface OffloadDeliveryResult { verdict: OffloadDeliveryVerdict; bucket?: string; prefix?: string; /** Objects modified within the recency window. */ recent_object_count: number; /** All objects under the prefix (subject to the lister's own ceiling). */ total_object_count: number; /** Age of the newest object in seconds; null when none/unparseable. */ newest_object_age_sec: number | null; /** Sum of `.Size` over recent objects (raw sink bytes, not metric bytes). */ delivered_bytes_recent: number; /** Engine-stamped offload bytes in the window; null when unavailable. */ stamped_offload_bytes: number | null; /** Total event lines parsed across the sampled objects. */ sampled_events: number; /** routeState → count across sampled events. */ sampled_routestates: RouteStateTally; /** Non-`offload` routeStates found in the sink (the leak set). */ leak_routestates: string[]; /** One-line human summary. */ message: string; } export interface S3ObjectMeta { Key: string; Size?: number; LastModified?: string; } export interface OffloadDeliveryDeps { /** List objects under bucket/prefix. Throws on a real error (e.g. NoSuchBucket). */ listObjects(bucket: string, prefix: string): Promise; /** Fetch one object's body (newline-delimited JSON events). */ getObject(bucket: string, key: string): Promise; /** * Engine-stamped offload bytes over the window (PromQL * `sum(increase(all_events_summaryBytes_total{routeState="offload"}[w]))`). * Returns null when no metric backend is wired — the verifier then relies * on liveness + purity alone and cannot assert silent_loss. */ stampedOffloadBytes(): Promise; } export interface OffloadDeliveryArgs { bucket?: string; prefix?: string; /** Destination type of the offload sink. Default `s3`. */ storeKind?: ObjectStoreKind; /** Azure storage account holding the container. Read when `storeKind` is `azure_blob`. */ storageAccount?: string; /** Recency window in minutes for "recent" + stale detection. Default 30. */ recencyMinutes?: number; /** How many newest objects to sample for the purity check. Default 3. */ sampleObjects?: number; /** Injected clock (unix-ms) for deterministic tests. Default Date.now(). */ nowMs?: number; } /** * Verify that engine-stamped offload is actually delivered to (and only to) * the configured sink. Pure given its deps — no global state. */ export declare function verifyOffloadDelivery(args: OffloadDeliveryArgs, deps: OffloadDeliveryDeps): Promise; /** * Build default deps for one store target. `stampedOffloadBytes` has no * store-free default: the caller (doctor / commitment_report) wires it to a * PromQL query because it needs the EnvConfig + executor. Without it, pass * `() => Promise.resolve(null)` and the verifier runs on liveness + purity * alone. * * `target` names the store. Omitting it keeps the historical S3 behaviour, so * a caller that has not been taught about destination types is unchanged. */ export declare function defaultOffloadDeliveryDeps(stampedOffloadBytes?: () => Promise, target?: ObjectStoreTarget): OffloadDeliveryDeps;