/** * The agent's secret cache, and why it never touches disk. * * An application on managed compute asks the agent for a value rather than the * platform, so a rotation reaches every process on the box without any of them * holding a credential to fetch it with. That only helps if the cache is * cheaper than the call it replaces AND does not create the very artefact the * design exists to remove. * * ## Memory only, deliberately * * Writing the cache to disk would give an attacker with filesystem access every * secret this guest has ever read — which is precisely the file that does not * exist today, and the entire reason a node holds a signing key instead of a * `.env`. A restart re-fetches. That costs one round trip and removes a class of * compromise; there is no version of "just persist it, it is encrypted" that * survives the key also being on the box. * * ## Invalidation is driven by the cursor, not by a timer * * A TTL alone means a rotated secret keeps working for the length of the TTL — * which is exactly the window rotation exists to close. `sync()` polls the * platform's `/changes` cursor and drops what moved, so a rotation propagates at * the poll interval regardless of TTL. The TTL is the backstop for the case * where sync itself has stopped, and it is short for that reason. * * ## A stale read is refused, not served * * If sync has not succeeded within the staleness bound, `get` refuses rather * than serving a value it can no longer vouch for. An application that receives * a revoked credential and succeeds with it is worse off than one that receives * an error: the error is visible, the success is not. */ export declare class CacheError extends Error { readonly code: 'STALE' | 'NOT_CACHED' | 'FETCH_FAILED'; constructor(code: 'STALE' | 'NOT_CACHED' | 'FETCH_FAILED', message: string); } export interface CacheOptions { /** * Fetches one value from the platform. Injected so the cache holds no * transport and a test needs no network. */ fetch(name: string): Promise<{ value: string; version: number; }>; /** * Names changed since a cursor, and the new cursor. * * `resync` means the caller's cursor is unusable — a restored backup or a * moved clock — and everything is dropped rather than a gap being left * exactly where a rotation was. */ changes(since: number): Promise<{ version: number; changed: string[]; resync?: boolean; }>; /** * Every name in the assigned scope. Present means explicit full-scope mode. * * Absent means the old behaviour — fetch on demand, hold what was asked for. * Present means the caller deliberately pulls the whole bound project across every environment at startup and * on every resync, so an application reads with no round trip and keeps * working while the platform is unreachable. * * That is a deliberate widening of what a compromised guest exposes: not * "whatever this app read" but "everything in scope". It is bounded by the * scope the compute is assigned and by SEV-SNP keeping the host out of guest * memory, and it is the point of the agent — a local copy that survives the * API being down is not a cache, and a cache that only has what you already * fetched does not survive anything. */ list?(): Promise; /** Backstop for a sync that has stopped. Short on purpose. */ ttlMs?: number; /** Refuse to serve anything if sync has not succeeded within this. */ maxStaleMs?: number; now?: () => number; } export declare function createSecretCache(options: CacheOptions): { /** For an operator asking what this guest is holding. Names, never values. */ names: () => string[]; /** True once the whole scope is resident. False in on-demand mode. */ readonly replica: boolean; /** * Load the whole assigned scope into memory. * * Called at startup and again after a `resync`. A resync means the cursor * is unusable, so the alternative is an empty cache that refills lazily — * which is exactly the availability the replica exists to provide, lost at * the moment something already went wrong. */ load: () => Promise<{ loaded: number; failed: string[]; }>; readonly cursor: number; /** * One value, from cache when it is fresh enough to vouch for. * * The staleness check runs BEFORE the cache lookup. Serving a cached value * while sync is broken is the failure this guards, so checking after the * lookup would mean the hit path — the common one — skipped the guard. */ get(name: string): Promise; /** * Poll for changes and drop what moved. * * Returns what it invalidated so a caller can log a rotation actually * arriving — "sync ran" and "sync did something" are different facts, and * only the second one tells you rotation works. */ sync(): Promise<{ invalidated: string[]; cursor: number; resync: boolean; }>; /** Drop everything. Used on revocation, and by tests. */ clear(): void; /** For the journal: how long since sync last succeeded. */ staleForMs: () => number; }; export type SecretCache = ReturnType;