/** * Effect ledger: a per-request, append-only, ordered record of side-effect intents and outcomes. * * Every owned effect seam (a repository write, an external call, an email send) records one entry via * the `useCapability` beacon. The entry type is deliberately **token-only**: capability ids, phase, * an adapter/resource token, dimensionless cost counters, an optional keyed digest, and an error code. * There is no free-form payload field - request bodies, rows, and values cannot enter the ledger, so * redaction holds by construction rather than by filtering. The sealed ledger carries the **route * pattern** (`/users/:id`), never the concrete URL, so path parameters cannot leak either. * * The ledger is the substrate that audit, replay, reconciliation, and cost accounting read. * This module ships the primitive: the entry contract, a bounded per-request ledger with an optional * tamper-evident hash chain, a sink seam, an in-memory sink, and a keyed digest helper. Durable, * encrypted, or retention-managed sinks implement the same {@link LedgerSink} seam externally. */ /** Lifecycle phase of one effect. `intent` precedes execution; the rest describe its outcome. */ export type EffectPhase = "intent" | "committed" | "failed" | "compensated"; /** * Dimensionless resource counters (`{ ms: 12, calls: 1, bytes: 512 }`). Counters carry *how much * resource* an effect consumed; mapping counters to money/pricing is deliberately out of scope here. */ export type EffectCost = Readonly>; /** Token-only caller metadata shared by an effect intent and outcome. */ export interface EffectMetadata { /** Adapter/resource token (`repo:orders`, `provider:payments`). A token - never a value or a row. */ readonly target?: string; /** Dimensionless counters; see {@link EffectCost}. At most {@link MAX_COST_AXES} axes. */ readonly cost?: EffectCost; /** Optional keyed-HMAC digest of the effect payload (see {@link computeEffectDigest}); hex, 64 chars. */ readonly digest?: string; } /** Caller-supplied fields for one entry. Everything else (`seq`, `at`) is assigned by the ledger. */ export interface EffectEntryInput extends EffectMetadata { /** Opaque execution correlation token shared by one intent and its terminal outcome. */ readonly effectId?: string; /** Capability token (`db.write`, `payments.charge`). Must be a valid capability id. */ readonly capability: string; /** Default `"intent"`. Record `committed`/`failed`/`compensated` only after the outcome is known. */ readonly phase?: EffectPhase; /** Outcome error as a bounded token code - never a message, never a stack. */ readonly error?: { readonly code: string; }; } /** One recorded effect. Frozen; token-only by construction (no payload field exists). */ export interface EffectEntry { /** Monotonic position within the request (0-based). The replay/simulation order. */ readonly seq: number; /** Milliseconds from the ledger's (injectable) clock at append time. */ readonly at: number; readonly effectId?: string; readonly capability: string; readonly phase: EffectPhase; readonly target?: string; readonly cost?: EffectCost; readonly digest?: string; readonly error?: { readonly code: string; }; } /** Tamper-evidence over the route identity, declarations, and sealed entries. */ export interface EffectChain { /** `hashes[i]` = SHA-256(hex) over (`hashes[i-1]` + canonical entry `i`). */ readonly hashes: readonly string[]; /** Last entry hash, or the route-header hash for an empty ledger. Anchor/sign this externally. */ readonly head: string; } /** The immutable result of sealing a request's ledger. Token-only; safe to hand to any sink. */ export interface SealedEffectLedger { readonly method: string; /** The registered route pattern (`/users/:id`) - never the concrete request URL. */ readonly path: string; readonly entries: readonly EffectEntry[]; /** The route's declared capability tokens - the runtime-enforcement view: recorded ⊆ declared is * guaranteed by the beacon, and `declared` minus the recorded ids is the unused declaration set. */ readonly declared: readonly string[]; /** Present when the ledger was created with `chain: true`. */ readonly chain?: EffectChain; } /** * Receives each sealed ledger once per request (only when it has entries). Implementations must not * assume a payload: the ledger is token-only. A durable/tenant-scoped sink lives behind this seam. */ export type LedgerSink = (ledger: SealedEffectLedger) => void | Promise; /** Thrown by `append` when the per-request entry bound is exceeded. Fails the request closed. */ export declare class EffectLedgerOverflowError extends Error { readonly maxEntries: number; constructor(maxEntries: number); } /** Thrown by `append` after `seal()` - e.g. an effect attempted while streaming a response body. */ export declare class EffectLedgerSealedError extends Error { constructor(); } /** Per-request ledger. `append` is synchronous (hot-path safe); `seal` is idempotent and async. */ export interface RequestLedger { /** Validate, freeze, and record one entry. Throws on invalid input, overflow, or after seal. */ append(input: EffectEntryInput): EffectEntry; /** Entries recorded so far (frozen snapshot view). */ entries(): readonly EffectEntry[]; readonly size: number; readonly sealed: boolean; /** Finalize the ledger (computing the chain when enabled). Idempotent - always the same result. */ seal(): Promise; } export interface CreateRequestLedgerOptions { /** HTTP method of the matched route. */ readonly method: string; /** The registered route pattern - callers must never pass the concrete request URL. */ readonly path: string; /** The route's declared capability tokens, surfaced verbatim on the sealed ledger. Default `[]`. */ readonly declared?: readonly string[]; /** Entry bound; exceeding it throws {@link EffectLedgerOverflowError}. Default {@link DEFAULT_MAX_ENTRIES}. */ readonly maxEntries?: number; /** Compute the tamper-evident hash chain at seal. Default false. */ readonly chain?: boolean; /** Injectable monotonic clock (ms) for deterministic tests. Default `performance.now`. */ readonly clock?: () => number; } /** Per-request entry bound. Generous for real handlers, small enough to stop a runaway loop. */ export declare const DEFAULT_MAX_ENTRIES = 1000; /** Most cost axes one entry may carry. */ export declare const MAX_COST_AXES = 8; /** Validate, copy, and freeze token-only effect metadata before it reaches a ledger or policy hook. */ export declare function normalizeEffectMetadata(input: EffectMetadata): EffectMetadata; /** Create a bounded per-request ledger. The server wires one per capability-declaring route. */ export declare function createRequestLedger(options: CreateRequestLedgerOptions): RequestLedger; export interface MemoryLedgerSinkOptions { /** Retain at most this many sealed ledgers (oldest evicted first). Default 1000. */ readonly maxLedgers?: number; } export interface MemoryLedgerSink { readonly sink: LedgerSink; /** Sealed ledgers received so far, oldest first. */ readonly ledgers: readonly SealedEffectLedger[]; clear(): void; } /** Bounded in-memory sink for tests and local development. Token-only, like every sink. */ export declare function createMemoryLedgerSink(options?: MemoryLedgerSinkOptions): MemoryLedgerSink; /** Minimum digest key material. A short key would make the keyed digest brute-forceable. */ export declare const MIN_DIGEST_KEY_BYTES = 16; /** Fresh random digest key (32 bytes). Per-process by default - persist one externally to correlate across restarts. */ export declare function randomEffectDigestKey(): Uint8Array; /** * Keyed HMAC-SHA-256 digest (hex) of an effect payload, for replay/reconciliation matching without * storing the payload. Keyed on purpose: a bare hash of low-entropy data (an email, a flag) is * brute-forceable and would itself leak. Digest the **whole** effect payload, never a single field. */ export declare function computeEffectDigest(key: Uint8Array | CryptoKey, payload: Uint8Array): Promise; /** Framework wiring: attach a per-request ledger to a handler context. Not for application code. */ export declare function attachEffectLedger(context: object, ledger: RequestLedger): void; /** The request's effect ledger, when the server enabled one for this route. Read-only access. */ export declare function effectLedgerOf(context: object): RequestLedger | undefined; /** Server-level effect ledger configuration (see `server({ effectLedger })`). */ export interface EffectLedgerOptions { /** Receives each request's sealed ledger (only when it recorded entries). */ readonly sink: LedgerSink; /** Per-request entry bound. Default {@link DEFAULT_MAX_ENTRIES}. */ readonly maxEntries?: number; /** Compute the tamper-evident hash chain at seal. Default false. */ readonly chain?: boolean; } //# sourceMappingURL=ledger.d.ts.map