// Types for tenant-rate-limit.js — written rather than `@ts-expect-error`'d, on the same criterion // as retry.d.ts and theme-read.d.ts: this is OUR module, and the hosted door depends on the SHAPE // of what it returns, not merely on the call succeeding. Two of those shapes are load-bearing: // // • `tenantRateLimitStep` hands back the WHOLE tenant record (both buckets, one storage value) — // under `any` a Durable Object that persisted `result.bucket` instead of `result.state` would // compile perfectly and reset the other bucket on every call. // • `retry_after` is OPTIONAL and present only on a refusal. Typed, a caller that reads it on the // allowed branch is a compile error; untyped, it is a refusal that promises `undefined` seconds. export type RateLimitBucket = 'read' | 'write'; export const READ_BUCKET: 'read'; export const WRITE_BUCKET: 'write'; export const RATE_LIMIT_BUCKETS: readonly RateLimitBucket[]; /** The two horizons, in SECONDS. Fixed implementation contract — only the four limits are set by * the environment. */ export const RATE_LIMIT_HORIZONS: { readonly burst: number; readonly sustained: number }; /** The exact environment-variable names, per bucket and horizon. */ export const TENANT_RATE_LIMIT_VARS: { readonly read: { readonly burst: string; readonly sustained: string }; readonly write: { readonly burst: string; readonly sustained: string }; }; export const TENANT_RATE_LIMIT_DEFAULTS: { readonly read: { readonly burst: number; readonly sustained: number }; readonly write: { readonly burst: number; readonly sustained: number }; }; /** The Durable Object instance-name prefix `tenantRateLimiterKey` stamps on. */ export const TENANT_RATE_LIMIT_KEY_PREFIX: string; /** How many full per-grant allowances one tenant may spend at once. */ export const TENANT_TOTAL_MULTIPLIER: number; /** The bucket every token issued before grant ids shares, per tenant. */ export const LEGACY_GRANT_KEY: string; /** How many grants' buckets one tenant's record may hold; past this the least recently active is * dropped (the tenant-total layer still counts every call, so this costs no safety). */ export const MAX_TRACKED_GRANTS: number; export type RateLimitScope = 'grant' | 'tenant'; export const RATE_LIMIT_SCOPES: readonly RateLimitScope[]; /** One horizon of one bucket, as persisted. Three integers, no matter how much traffic passes. */ export interface HorizonState { start: number; count: number; previous: number; } export interface BucketState { burst: HorizonState; sustained: HorizonState; } export interface BucketPair { read?: BucketState; write?: BucketState; } /** * The whole tenant record, ONE storage value: the top-level buckets are the TENANT TOTAL, and * `grants` holds one pair per grant id (plus `LEGACY_GRANT_KEY` for tokens that carry none). */ export interface TenantRateLimitState extends BucketPair { grants?: Record; } export interface ResolvedHorizon { limit: number; window_seconds: number; } export interface ResolvedBucket { burst: ResolvedHorizon; sustained: ResolvedHorizon; } /** What the door enforces AND what `describe_feelreef` publishes — the same object, by construction. */ export interface ResolvedTenantRateLimitConfig { read: ResolvedBucket; write: ResolvedBucket; } /** One horizon of the snapshot handed back to the caller. */ export interface RateLimitHorizonSnapshot { limit: number; window_seconds: number; remaining: number; /** ISO-8601 UTC. */ reset_at: string; } export interface RateLimitSnapshot { bucket: RateLimitBucket; /** The calling grant's own budget (or the shared pre-grant-id bucket). */ burst: RateLimitHorizonSnapshot; sustained: RateLimitHorizonSnapshot; /** The second layer: everything this account is spending across all of its grants. */ tenant_total: { burst: RateLimitHorizonSnapshot; sustained: RateLimitHorizonSnapshot }; /** Which layer refused. Present only on a refusal. */ scope?: RateLimitScope; } /** The honest snapshot a read gets when the limiter itself could not be reached. */ export interface UnavailableRateLimitSnapshot { bucket: RateLimitBucket; unavailable: true; } /** * The four limits, from the environment or the built-in defaults. * * THROWS on a present-but-unusable value, naming the variable: a deployment that states a limit it * cannot parse is a boot failure, never a silent fall back to a limit nobody chose. */ export function resolveTenantRateLimitConfig( env: Record | undefined | null, ): ResolvedTenantRateLimitConfig; /** `readOnlyHint === true` buys the read bucket; everything else — including an unexpected shape — * falls to write. */ export function bucketFor(annotations: { readOnlyHint?: unknown } | undefined | null): RateLimitBucket; /** The Durable Object instance name for a VERIFIED tenant. Throws on anything else. */ export function tenantRateLimiterKey(tenant: string): string; /** The state sub-key one grant's buckets live under, inside its tenant's record. `null` (a token * issued before grant ids) resolves to the shared `LEGACY_GRANT_KEY`; anything else throws. */ export function grantStateKey(grantId: string | null | undefined): string; /** The per-tenant ceiling, derived from the per-grant config by `TENANT_TOTAL_MULTIPLIER`. */ export function resolveTenantTotalConfig(config: ResolvedTenantRateLimitConfig): ResolvedTenantRateLimitConfig; export function unavailableRateLimitSnapshot(bucket: RateLimitBucket): UnavailableRateLimitSnapshot; export interface TenantRateLimitStepInput { /** The tenant's whole persisted record; `undefined`/`null` is a tenant that has never called. */ state?: TenantRateLimitState | null; /** A millisecond epoch, read by the Durable Object — never supplied by a caller. */ now: number; bucket: RateLimitBucket; config: ResolvedTenantRateLimitConfig; /** The VERIFIED grant id from the token's props, or `null` for a token issued before them. */ grantId?: string | null; } export interface TenantRateLimitStepResult { allowed: boolean; /** What to persist — and ONLY when `allowed`; a refusal spends nothing. */ state: TenantRateLimitState; rate_limit: RateLimitSnapshot; /** Whole seconds, at least one, sufficient for BOTH horizons. Present only on a refusal. */ retry_after?: number; } export function tenantRateLimitStep(input: TenantRateLimitStepInput): TenantRateLimitStepResult;