export type WebhookOperation = "create" | "update" | "delete"; /** The JSON body the worker signs and POSTs. */ export interface WebhookPayload { id: string; appId: string; path: string; relativePath: string; operation: WebhookOperation; document: Record | null; previousDocument: Record | null; timestamp: number; /** * SHW-05: stable per-delivery id (also in the `X-Bounded-Delivery-Id` header), * REUSED across every retry of one delivery. Delivery is durable + at-least- * once, so dedupe on THIS - not the timestamp or signature, which change per * retry. Optional so a payload signed before the field existed still verifies. */ deliveryId?: string; } export interface WebhookPublicKey { id: string; alg: "ed25519"; publicKey: string; } /** * A pluggable replay store. Implementations should atomically record a signed * delivery replay key and return whether it had already been seen. Entries only * need to survive the skew window; a process-local in-memory store is used by * default, and a simple store ({@link InMemoryReplayStore}) is exported for * callers that want explicit control. */ export interface WebhookReplayStore { /** * Atomically mark `id` as seen until `expiresAtMs`. Return `true` if the * delivery was ALREADY recorded (a replay), `false` if this is the first time. */ checkAndRecord(id: string, expiresAtMs: number): boolean | Promise; } export interface VerifyWebhookOptions { /** * URL of the hosted public keys. When omitted, it is derived from the * Bounded network configured via `init({ network })` (e.g. * `bounded-staging` → the staging keys endpoint), falling back to the * PRODUCTION endpoint when no Bounded network is set — so an uninitialized / * production receiver still trusts production-signed keys. Point this at a * custom worker as needed. */ keysUrl?: string; /** Max allowed clock skew between the signed timestamp and now, in seconds. Default 300. */ maxSkewSeconds?: number; /** In-memory key cache TTL in milliseconds. Default 300_000 (5 min). */ cacheTtlMs?: number; /** * Negative-cache TTL in milliseconds for unknown key ids: how long a forced * keys-endpoint refresh is suppressed for an X-Bounded-Key-Id that no fetch * could resolve (audit U-053). A genuinely rotated key waits at most this * long. Default 60_000 (60s). */ unknownKeyTtlMs?: number; /** Override the fetch implementation (mainly for tests). */ fetchImpl?: typeof fetch; /** Override "now" (epoch ms), mainly for tests. */ now?: () => number; /** * Expected Bounded app id for this receiver. When set, a validly signed * webhook for another app is rejected instead of relying on every caller to * compare `payload.appId` manually. * * #072: when this is OMITTED, verifyWebhook now binds to the SDK's init()-time * app id by DEFAULT (secure-by-default). Because Bounded signs every app's * webhooks with one shared platform key, a valid signature only proves "from * Bounded", not "from YOUR app" - so a bind is required to stop cross-app * forgery. If no app id can be resolved (this option unset AND no init()-time * app id), verifyWebhook FAILS CLOSED. Multi-app receivers must set * {@link allowAnyAppId} to accept any app deliberately. */ expectedAppId?: string; /** * Opt out of the app binding to accept a validly-signed webhook from ANY app * (the rare multi-app receiver). #072: this MUST be set explicitly - the safe * default binds to the SDK's app id. Ignored when {@link expectedAppId} is set * (an explicit expected id always enforces a match). */ allowAnyAppId?: boolean; /** * Replay store. By default, verifyWebhook uses a process-local in-memory * store and rejects exact signed-delivery replays within the skew window. * Pass a shared store (Redis, KV, DB unique constraint) for multi-instance * receivers. Pass `null` only when another layer already handles replay. */ replayStore?: WebhookReplayStore | null; } export declare const DEFAULT_WEBHOOK_KEYS_URL = "https://realtime.bounded.sh/.well-known/bounded-webhook-keys.json"; export declare class WebhookVerificationError extends Error { constructor(message: string); } export type WebhookHeaders = Record | { get(name: string): string | null; }; /** Clear the in-memory key caches, including the unknown-key negative cache (mainly for tests). */ export declare function clearWebhookKeyCache(): void; /** * A simple in-memory {@link WebhookReplayStore}. Records seen event ids until * their skew-window expiry and lazily evicts expired entries. Suitable for a * single-process receiver; use a shared store (Redis, KV, a DB unique * constraint) for multi-instance deployments. */ export declare class InMemoryReplayStore implements WebhookReplayStore { private readonly seen; checkAndRecord(id: string, expiresAtMs: number): boolean; /** Clear all recorded ids (mainly for tests). */ clear(): void; } /** Clear the default in-memory replay cache (mainly for tests). */ export declare function clearWebhookReplayCache(): void; /** * Verify a signed inbound webhook and return its typed payload. * * @param rawBody The exact raw request body string the platform signed. Must be * the unparsed bytes — re-serializing parsed JSON may not match byte-for-byte. * @param headers The inbound request headers (a Headers instance or a plain * object; case-insensitive). * @param opts Optional overrides (keys URL, skew, cache TTL, fetch). * @returns The parsed, validated {@link WebhookPayload}. * @throws {WebhookVerificationError} if anything fails verification. */ export declare function verifyWebhook(rawBody: string, headers: WebhookHeaders, opts?: VerifyWebhookOptions): Promise;