/** * Persistence contract for `withBudget(...)` middleware (#A6 Phase 3). * * Apps configure a `BudgetStorage` instance — the middleware calls * `checkAndDebit()` once before each request (estimated input cost) and * once per `usage` chunk (actual output cost). The storage is responsible * for atomic increment + cap enforcement. * * # Atomicity * * `checkAndDebit` MUST be atomic: a concurrent caller cannot observe a * stale `spent` value between the read and the write. Otherwise two * requests both pass the check before either debits, and a user can * exceed their cap by `costUsd × concurrency`. * * Implementations: * - `memoryBudgetStorage()` — single-process Map; atomic because we never * `await` between the read and the write. * - `ormBudgetStorage()` (Phase 4) — uses `UPDATE … RETURNING` (Postgres) * or row-level `SELECT … FOR UPDATE` (MySQL/SQLite) to keep the read + * write in a single transaction. * - Redis / Cache impls — would use `INCRBY` + a `EXPIRE`, treating the * counter as authoritative. * * # Period semantics * * `daily` rolls at the user's local midnight when `timezone` is provided * (IANA name, e.g. `'America/New_York'`); UTC otherwise. `monthly` rolls * on the calendar-month boundary in the same TZ. Phase 2 does not * implement rolling-30-days windows — adds storage complexity for marginal * value. */ export type BudgetPeriod = 'daily' | 'monthly'; export interface BudgetCheckOptions { /** Identifier for the budget owner (typically a user id). */ userId: string; /** Which cap to check against. */ period: BudgetPeriod; /** Cap in USD. Caller resolves `budget()` once per request. */ cap: number; /** Cost to debit in USD. Pass `0` to read the current spent value without mutating. */ costUsd: number; /** Override the clock — useful for tests + period-rollover assertions. */ now?: Date; /** IANA timezone for period boundaries. Defaults to UTC. */ timezone?: string; } export interface BudgetCheckResult { /** True when the debit was applied; false when it would have exceeded `cap`. */ allowed: boolean; /** * Current spend for the period. * * - When `allowed: true`, this is the value AFTER the debit (i.e. * includes `costUsd`). * - When `allowed: false`, this is the value BEFORE the rejected debit * (i.e. the prior `spent`). This matches the value `BudgetExceededError` * should report — the user spent `spent`, the cap was `cap`, the * request would have pushed past. */ spent: number; /** The cap passed in `BudgetCheckOptions.cap`, echoed for caller convenience. */ cap: number; } export interface BudgetStorage { /** * Atomically read the current `spent` value, add `costUsd` if the result * stays within `cap`, and return the outcome. * * Pass `costUsd: 0` for a pure read (e.g. for a "you've spent $X today" * status display) — the result reflects current spend without mutating. */ checkAndDebit(opts: BudgetCheckOptions): Promise; /** * Reset the budget counter for `(userId, period)` at the period * containing `now` (or "now" by clock). Useful for tests and admin * overrides; not required by the middleware. */ reset?(userId: string, period: BudgetPeriod, now?: Date, timezone?: string): Promise; } /** * Format a `Date` as the bucket key for a billing period. * * - `daily` → `YYYY-MM-DD` in the given timezone (or UTC) * - `monthly` → `YYYY-MM` in the given timezone (or UTC) * * Used by `memoryBudgetStorage` and `ormBudgetStorage` (Phase 4) to * partition the counter by billing window. Exposed publicly so apps * computing per-period totals outside the middleware (admin dashboards, * tests) get the same bucketing. */ export declare function periodKey(period: BudgetPeriod, now: Date, timezone?: string): string; /** * In-process `BudgetStorage` backed by a `Map`. Suitable for tests and * single-process dev servers. * * **Cross-process caveat:** counters live in process-local memory, so * queue workers (or any second process) see their own copy. For any app * with workers or horizontal scaling, use `ormBudgetStorage()` (Phase 4) * or a Redis-backed storage. Use `memoryBudgetStorage()` only when you're * sure all budget-relevant code paths run in the same Node process. */ export declare function memoryBudgetStorage(): BudgetStorage; //# sourceMappingURL=storage.d.ts.map