/** * @module @arcis/node/guards * * Guards API. Same Arcis decisioning (rate limit, bot detect, prompt * injection, token budget) applied to non-HTTP contexts where there's no * `req`/`res` pair. Use this for: * * - Job queue workers (BullMQ, agenda, sidekiq-style) * - Agent tool-call handlers (Claude/OpenAI tool dispatch) * - WebSocket / SSE / gRPC handlers * - Background processors (cron jobs, scheduled tasks) * * Each call to `guards.run(input)` returns a structured decision: `ok` + * (when denied) the `vector`, `severity`, `reason`, and `retryAfterSeconds` * the deny was triggered by. The first vector that denies short-circuits * the rest, so denial latency stays bounded. * * @example * import { Guards } from '@arcis/node'; * * const guards = new Guards({ * rateLimit: { max: 50, windowMs: 60_000 }, * tokenBudget: { maxTokens: 100_000, windowMs: 60 * 60 * 1000 }, * promptInjection: { redactLow: false }, * }); * * // In a job handler: * const decision = guards.run({ * key: jobUserId, * tokens: estimateTokens(prompt), * text: prompt, * }); * if (!decision.ok) { * throw new Error(`Job rejected (${decision.vector}): ${decision.reason}`); * } */ import { type BotProtectionOptions } from './middleware/bot-detection'; import { type PromptInjectionSeverity } from './sanitizers/prompt-injection'; export interface GuardsRateLimitOptions { /** Max events per window per key. Default: 100. */ max?: number; /** Window length in milliseconds. Default: 60000 (1 minute). */ windowMs?: number; } export interface GuardsTokenBudgetOptions { /** Max tokens a single key can spend in one window. Default: 100,000. */ maxTokens?: number; /** Window length in milliseconds. Default: 60 * 60 * 1000 (1 hour). */ windowMs?: number; /** * Optional per-call cap. When set, a single call with `tokens > maxRequestTokens` * denies BEFORE charging the window budget. */ maxRequestTokens?: number; } export interface GuardsPromptInjectionOptions { /** * Minimum severity that triggers a deny. Default: 'medium' (HIGH and * MEDIUM matches deny; LOW matches still surface in `decision.matches` * but don't deny). */ denyAt?: PromptInjectionSeverity; } export interface GuardsBotOptions { /** Categories that pass through. Default: SEARCH_ENGINE, SOCIAL, MONITORING. */ allow?: BotProtectionOptions['allow']; /** Categories that always deny. Default: AUTOMATED. */ deny?: BotProtectionOptions['deny']; /** Default for uncategorized bots. Default: 'allow'. */ defaultAction?: BotProtectionOptions['defaultAction']; } export interface GuardsConfig { /** When set, every call is rate-limited per `input.key`. Omit to disable. */ rateLimit?: GuardsRateLimitOptions; /** When set, calls with `input.tokens` charge a per-key sliding-window budget. */ tokenBudget?: GuardsTokenBudgetOptions; /** When set, `input.text` is scanned for prompt-injection signatures. */ promptInjection?: GuardsPromptInjectionOptions | true; /** When set, `input.userAgent` is matched against the bot corpus. */ bot?: GuardsBotOptions | true; } export interface GuardsInput { /** Identifier for rate-limit / token-budget bucketing. Required. */ key: string; /** Optional text payload for prompt-injection scanning. */ text?: string; /** Optional token cost for token-budget accounting. */ tokens?: number; /** Optional User-Agent string for bot detection. */ userAgent?: string; } export type GuardsVector = 'rate-limit' | 'token-budget' | 'prompt-injection' | 'bot'; export type GuardsSeverity = 'low' | 'medium' | 'high'; export interface GuardsDecision { /** True if the input passes every configured vector. */ ok: boolean; /** Which vector denied. Undefined when `ok` is true. */ vector?: GuardsVector; /** Human-readable reason for the deny. Undefined when `ok` is true. */ reason?: string; /** Severity of the deny. Undefined when `ok` is true. */ severity?: GuardsSeverity; /** How many seconds until the same key can retry (rate-limit / token-budget). */ retryAfterSeconds?: number; /** * For prompt-injection: every signature that matched, even when the deny * threshold wasn't hit. Lets callers log low-severity matches without * blocking on them. */ matches?: ReadonlyArray<{ rule: string; severity: GuardsSeverity; }>; } /** * Guards. Apply Arcis decisions to non-HTTP contexts. Construct once with * the vectors you care about, then call `.run(input)` per request/event. * Internal state (rate-limit buckets, token-budget buckets) lives on the * instance. Call `.close()` to release the periodic-cleanup interval. */ export declare class Guards { private readonly rl; private readonly tb; private readonly pi; private readonly bot; private readonly rlStore; private readonly tbStore; private readonly cleanup; private readonly piDenyRank; constructor(config?: GuardsConfig); /** * Evaluate every configured vector against `input`. Returns a structured * decision; the first denying vector short-circuits the rest. */ run(input: GuardsInput): GuardsDecision; /** Inspect rate-limit usage for a key. Useful for tests and telemetry. */ inspectRateLimit(key: string): { count: number; resetTime: number; } | null; /** Inspect token-budget usage for a key. */ inspectTokenBudget(key: string): { used: number; resetTime: number; } | null; /** Reset a single key's state, or all keys if `key` is omitted. */ reset(key?: string): void; /** Release the periodic cleanup interval. Idempotent. */ close(): void; private checkRateLimit; private checkTokenBudget; private checkBot; private sweepExpired; } export default Guards; //# sourceMappingURL=guards.d.ts.map