import { AppContext } from '@voltro/runtime'; import { ColumnBuilder } from '@voltro/database'; import { ColumnDefinition } from '@voltro/database'; import { Context } from 'effect'; import { DataStore } from '@voltro/database'; import { Effect } from 'effect'; import { EventDescriptor } from '@voltro/protocol'; import { EventWebhookSpec } from '@voltro/protocol'; import { FieldDefinitions } from '@voltro/database'; import { OutboxHandlerDefinition } from '@voltro/runtime'; import { PluginPermission } from '@voltro/protocol'; import { Schema } from 'effect'; import { Table } from '@voltro/database'; import { VoltroPlugin } from '@voltro/protocol'; import { Workflow } from '@effect/workflow'; import { WorkflowEngine } from '@effect/workflow/WorkflowEngine'; import { WorkflowInstance } from '@effect/workflow/WorkflowEngine'; import { WorkflowRunHandle } from '@voltro/protocol'; /** * Acquire one slot in EVERY scope for the window containing `now` * (all-or-nothing: a partial acquisition is refunded before deferring, * so a delivery blocked by the event-global limit doesn't waste its * target's budget). * * Returns `{ acquired: true }` when the delivery may POST now, or * `{ acquired: false, retryInMs }` where `retryInMs` is the time until * the next window opens. */ export declare const acquireRateSlots: (store: DataStore, scopes: ReadonlyArray, now: number, windowMs?: number) => Promise; /** * Project a declared event onto the outgoing-event descriptor this plugin * already understands. * * A projection rather than a second registry: every downstream deployment — * the delivery workflow, the JSON-Schema export, the dashboard's event list — * keeps reading ONE shape. Adding a parallel path for declared events would * mean each of them handles two, which is how the two drift. * * The event's `name` becomes the webhook `id`, so a subscriber that registered * for `orders.paid` keeps working across the migration and the dashboard shows * one event rather than two spellings of it. */ export declare const asOutgoingEvent: (descriptor: EventDescriptor & { readonly payload: unknown; readonly webhook?: EventWebhookSpec | undefined; }) => OutgoingEventDescriptor | undefined; /** What `recordDeliveryOutcome` did — surfaced so the workflow can log * the auto-disable transition. */ export declare interface AutoDisableOutcome { /** The streak count AFTER this outcome was recorded. */ readonly consecutiveFailures: number; /** True when THIS call flipped the target to `active=false`. */ readonly autoDisabled: boolean; } export declare const buildDeliverWebhookExecute: (ctx: AppContext, options?: DeliverWorkflowOptions) => (input: DeliverInput, _executionId: string) => Effect.Effect<{ finalStatus: "failed"; attempts: number; } | { finalStatus: "deferred"; attempts: number; } | { finalStatus: "succeeded"; attempts: number; }, never, WorkflowEngine | WorkflowInstance>; export declare const buildWebhooksService: (ctx: AppContext, trigger: (input: { readonly deliveryId: string; readonly targetId: string; readonly event: string; readonly eventId: string; readonly payloadJson: string; readonly attemptEpoch?: number; }) => Promise, options?: WebhooksServiceOptions) => WebhooksServiceShape; export declare const compareVersions: (targetVersion: number, eventVersion: number) => VersionState; /** Consume ONE slot from the (key, bucket) window. Returns `true` * when a slot was atomically claimed, `false` when the window is * full. Throws only on pathological contention (transient). * * Ensure-then-CAS: the window row is created at `count: 0` via * `insertIgnore` over the deterministic PK (so the create race is * harmless — every racer converges on ONE row), and every slot claim * is a conditional increment (`count = read` in the WHERE). With a * deterministic key the billing-style "my minted id came back" * ownership check can't disambiguate concurrent creators, so the * insert path never claims a slot directly. */ export declare const consumeRateSlot: (store: DataStore, scope: RateScope, bucket: number) => Promise; export declare interface CustomSignatureScheme { readonly _tag: 'custom'; readonly header: string; readonly sign: (rawBody: Uint8Array, secret: string) => string; /** Verify against the request's FULL header map (lowercased keys) — a scheme * whose timestamp lives in a second header can read it here instead of * relying on a caller to splice the two values together. */ readonly verify: (rawBody: Uint8Array, secret: string, headers: Readonly>) => boolean; } /** The structural slice of a `defineEvent` descriptor this plugin needs. * Structural rather than an import so the plugin does not depend on a specific * protocol version's class identity. */ export declare interface DeclaredEventLike { readonly kind: 'event'; readonly name: string; readonly payload: unknown; readonly webhook?: { readonly description?: string; readonly version?: number; readonly rateLimit?: { readonly perMinute: number; }; } | undefined; } /** Default URL path for an incoming webhook when the descriptor * doesn't override. Stage 4's discovery walker uses this to * register routes. */ export declare const defaultIncomingPath: (webhookId: string) => `/${string}`; /** * The DEFAULT for a new outgoing subscription: Standard Webhooks v1.0.0. * * An interoperable spec beats a house format for the one signature shape a * third party has to implement against. Every Standard-Webhooks consumer * library — and the package `voltro webhooks consumer` generates — verifies * these deliveries with no per-vendor code. That is the whole argument for a * spec, and it only pays if the spec is what we send by DEFAULT rather than * what you can opt into. */ export declare const defaultOutgoingSignature: () => StandardWebhooksSignatureScheme; /** Sensible default for new outgoing subscriptions: exponential, 8 * attempts, 5s → 1h. Roughly: 5s, 10s, 20s, 40s, 80s, 160s, 320s, * 640s. Total ~17 minutes before giving up. */ export declare const defaultRetryPolicy: () => RetryPolicy; /** The row payload. Deliberately flat and JSON-only — see the header. */ export declare interface DeferredEmitPayload { readonly event: string; readonly payload: unknown; readonly tenantId: string | null; } export declare const defineIncomingWebhook: (spec: Omit, "_tag">) => IncomingWebhookDescriptor; export declare const defineWebhookProvider: (spec: Omit) => WebhookProviderDescriptor; /** * Build the workflow body. `ctx` carries the per-request store + * runtime services — passed in by the framework when it registers * the workflow's `.toLayer(...)`. * * The body's structure is intentionally LINEAR — one for-loop with * activities marking each checkpoint. Activity caching means a * crash mid-loop resumes at the next un-cached activity. */ declare interface DeliverInput { readonly deliveryId: string; readonly targetId: string; readonly event: string; readonly eventId: string; readonly payloadJson: string; readonly attemptEpoch?: number; } export declare const deliverWebhookWorkflow: Workflow.Workflow<"voltro.deliverWebhook", Schema.Struct<{ deliveryId: typeof Schema.String; targetId: typeof Schema.String; event: typeof Schema.String; eventId: typeof Schema.String; payloadJson: typeof Schema.String; attemptEpoch: Schema.optionalWith number; }>; }>, Schema.Struct<{ finalStatus: Schema.Literal<["succeeded", "failed", "deferred"]>; attempts: typeof Schema.Number; }>, typeof Schema.Never>; declare interface DeliverWorkflowOptions { /** Discovered outgoing events (declared events with a `webhook:` block, * projected by `asOutgoingEvent`) — the workflow reads * the emitted event's `globalRateLimit` from here. Absent events * simply have no global limit. */ readonly events?: ReadonlyArray>; /** The fixed window backing "per minute" — injectable for tests * (production uses the 60s default). */ readonly rateWindowMs?: number; /** * Per-attempt wire timeout, milliseconds. Default 30 000. * * There was no timeout at all before this, so one receiver that accepted the * connection and never answered held a durable workflow — and its rate-limit * slot — indefinitely. Standard Webhooks recommends "somewhere between 15 and * 30s"; the top of that band is the default because a slow-but-alive receiver * being cut off produces a retry storm, which is the worse of the two * failures. Also settable per deployment with `VOLTRO_WEBHOOK_TIMEOUT_MS`. */ readonly timeoutMs?: number; } /** `getDelivery` adds the two heavy columns `listDeliveries` omits. */ export declare interface DeliveryDetail extends DeliverySummary { readonly payload: unknown; readonly responseBody: string | null; } /** One delivery ATTEMPT, as the management UI reads it. */ export declare interface DeliverySummary { readonly id: string; readonly deliveryId: string; readonly targetId: string; readonly event: string; readonly eventId: string | null; readonly attempt: number; readonly status: 'pending' | 'inFlight' | 'succeeded' | 'failed' | 'retryScheduled'; readonly responseStatus: number | null; readonly errorMessage: string | null; readonly latencyMs: number | null; readonly scheduledAt: string | null; readonly nextAttemptAt: string | null; readonly createdAt: string | null; } /** Compact duration literal — parsed without a deps-pulling library. * Covers ms / s / m / h / d. Default seconds (no suffix). */ export declare type DurationLiteral = `${number}${'ms' | 's' | 'm' | 'h' | 'd'}` | `${number}`; /** * Per-emit scoping. * * **The outgoing fan-out was NOT tenant-scoped, and this is what closes it.** * The service is built once at boot with the app-level store, so the `tenant()` * mixin on `_voltro_webhook_targets` had no request subject to scope by — the * target lookup was `eq('event', name)` and nothing else. Confinement therefore * rested entirely on each target's own `filter`, i.e. on the app remembering. * * A deployment found this by reading the CLI, and named exactly why it had looked * safe: target filters usually predicate on a globally unique app id, so a * cross-tenant match is impossible **by accident**. It stopped being accidental * for them the moment they introduced a value deliberately equal across teams * (a tenant-wide audience marker). * * `ctx.webhooks` binds this from the request subject, so an emit inside a * handler is scoped without the handler saying anything. Absent — a background * job, a schedule, a replay — keeps the unscoped behaviour, because there is no * tenant to scope BY and refusing would break every system emit. */ export declare interface EmitOptions { /** Only deliver to targets of this tenant. Absent ⇒ every target. */ readonly tenantId?: string | null; /** * Dispatch NOW instead of after the enclosing transaction commits. * * Inside a mutation, `emit` defaults to a transactional enqueue: the intent * commits with your rows or rolls back with them, and the POST goes out * afterwards. That is the right default and it is not the right answer * everywhere — a diagnostic ping, or an emit whose receiver the caller is * about to poll, wants the request on the wire immediately. * * It does NOT make the emit safe. A rollback after an immediate emit still * tells a subscriber about a change that did not happen. The point of the flag * is that the trade is written at the call site: the old behaviour was * un-transactional by accident, and nothing said so. * * Outside a mutation this is a no-op — there is no transaction to skip. * * **Read by `ctx.webhooks`, not by this service.** The deferral lives in the * request-scoped proxy (`cli/src/requestWebhooks.ts`), because only the * request knows whether there is a transaction; the boot-level service is * always immediate by construction. The option is declared here because this * is the type a caller sees — but calling the boot service directly with it * changes nothing, and that is the truthful behaviour rather than a gap. */ readonly immediate?: boolean; } export declare interface EmitResult { /** Stable id matching the row(s) the deliverWebhook workflow * writes to `_voltro_webhook_deliveries`. Use as the foreign * key when joining a domain event to its webhook deliveries. */ readonly eventId: string; /** Per-target deliveries. Empty when no target matched the * filter. `'dispatched'` — a delivery workflow run was kicked * off; `'queued'` — the target is paused, so the delivery was * written as a `status='pending'` row that `resumeTarget` * flushes. */ readonly deliveries: ReadonlyArray<{ readonly targetId: string; readonly deliveryId: string; readonly status: 'dispatched' | 'queued'; }>; } export declare interface EncodedPayload { readonly contentType: string; readonly bytes: Uint8Array; } /** Re-encode the JSON-stringified payload into the target's wire * format. Throws `WebhookPayloadUnrepresentable` when the shape can't * be honestly represented in the requested format. */ export declare const encodePayload: (format: WireFormat, payloadJson: string) => EncodedPayload; /** The events a subscribe names, whichever spelling was used. */ export declare const eventsOf: (input: SubscribeInput) => ReadonlyArray; /** Aggressive policy — retry every 30s for an hour. Use for a * downstream that's expected to come back quickly. */ export declare const fastRetryPolicy: () => RetryPolicy; export declare const generateSecret: () => string; /** * Mint a spec-shaped signing secret: `whsec_` + base64 of 32 random bytes. * * 32 bytes sits inside the spec's 24..64 band and matches the SHA-256 block * this key feeds. Per the spec, keys "should be unique per endpoint" — which is * already how `subscribe` mints them. */ export declare const generateStandardWebhooksSecret: () => string; /** * The house HMAC shape: `X-Webhook-Signature: t=NNN,v1=hex` over * `.`. * * Kept as a named CHOICE, not as a default and not as a fallback: a receiver * that already implemented this exact format should not have to change to keep * receiving. It is a peer of `stripeSignature()` / `githubSignature()` — a * specific counterparty's format — rather than a second general-purpose path. */ export declare const genericHmacSignature: () => HmacSignatureScheme; export declare const getIdempotencyCache: () => IdempotencyCache; /** GitHub-style: `X-Hub-Signature-256: sha256=hex` (no timestamp). */ export declare const githubSignature: () => HmacSignatureScheme; /** Does this declared event opt into outbound HTTP delivery? */ export declare const hasWebhookAudience: (descriptor: { readonly webhook?: EventWebhookSpec | undefined; }) => boolean; export declare type HmacAlgorithm = 'hmacSha256' | 'hmacSha1'; export declare interface HmacSignatureScheme { readonly _tag: 'hmac'; readonly algorithm: HmacAlgorithm; /** Header the signature is written to / read from. Stripe uses * `Stripe-Signature`, GitHub uses `X-Hub-Signature-256`, generic * apps use `X-Webhook-Signature`. */ readonly header: string; /** When `true`, the signed payload is `.` and * the header carries both (`t=...,v1=...`). Recipients reject * signatures whose `t` is older than `replayWindowSeconds`. */ readonly includeTimestamp: boolean; /** Window during which a signed payload is replay-safe. Older * signatures get rejected. Default 5 minutes. Only relevant when * `includeTimestamp: true`. */ readonly replayWindowSeconds?: number; /** Encoding the signature is rendered in. `hex` is most common; * Slack uses `hex` prefixed with `v0=`. */ readonly encoding?: 'hex' | 'base64'; /** Prefix written before the hex/base64 signature in the header * value. Slack: `v0=`. GitHub: `sha256=`. Default empty. */ readonly versionPrefix?: string; /** Optional secret-rotation hint: a SECOND secret accepted during * verification but never used for outgoing signing. Lets you * rotate the primary secret without breaking in-flight inbound * deliveries. */ readonly previousSecret?: string; } /** * In-process LRU cache for idempotency keys. Map preserves insertion * order, so eviction = `entries().next().value` (the oldest key). * * `claim(key, ttlMs)` returns one of: * - `'fresh'` — first time we've seen this key; handler runs. * - `'inflight'` — another concurrent request claimed it but * hasn't completed. Receiver should 409 / 425. * - `'duplicate'` — already processed within TTL. Receiver * should 200 OK (idempotent re-acknowledge). * * After the handler completes, call `commit(key)` to flip the * 'inflight' marker to 'processed'. On handler failure, call * `release(key)` to drop the inflight marker so a retry can run. */ export declare class IdempotencyCache { private readonly capacity; private readonly cache; constructor(capacity?: number); private evictExpired; claim(key: string, ttlMs: number): 'fresh' | 'inflight' | 'duplicate'; commit(key: string): void; release(key: string): void; size(): number; /** Test helper. */ clear(): void; } export declare interface IncomingLogRecord { readonly webhookId: string; readonly status: number; readonly signatureOk: boolean | 'skipped'; readonly idempotency: 'fresh' | 'inflight' | 'duplicate' | 'skipped'; readonly durationMs: number; readonly errorMessage?: string; } export declare interface IncomingRequest { readonly method: string; readonly path: string; readonly headers: Readonly>; readonly rawBody: Uint8Array; } export declare interface IncomingResponse { readonly status: number; readonly contentType: string; readonly body: string; readonly headers?: Readonly>; } export declare interface IncomingWebhookContext { /** The fully-decoded body, validated against `payload`. */ readonly body: Body; /** Raw bytes — needed to recompute signatures. Already used by * the framework's middleware to verify the inbound signature * before this handler runs; passed along in case the user wants * to compute additional MAC's for downstream relays. */ readonly rawBody: Uint8Array; /** Request headers (lowercased keys). The middleware has already * consumed signature / idempotency headers. */ readonly headers: Readonly>; /** Idempotency key extracted by the middleware (provider-specific * extraction — Stripe uses `Stripe-Signature`'s `t=`, GitHub uses * `X-GitHub-Delivery`, generic uses `Idempotency-Key`). */ readonly idempotencyKey: string; /** Workflow facade supplied by the framework runtime. Use this for * verified external incoming calls that should start or signal * durable workflows after signature + idempotency checks pass. */ readonly workflows?: IncomingWorkflowFacade; } export declare interface IncomingWebhookDescriptor { readonly _tag: 'incomingWebhook'; readonly id: WebhookId; /** URL path the framework mounts the route on. Defaults to * `/webhooks/` if absent. Use a custom path for legacy * integrations (`/integrations/stripe/v1`). */ readonly path?: `/${string}`; /** Signature scheme used to authenticate inbound requests. Reject * on mismatch with a 401. Usually filled in by `provider`. */ readonly signature?: SignatureScheme; /** * How this endpoint authenticates its caller. An incoming webhook is a * PUBLIC POST that runs your application code, so the framework will not * mount one that has made no decision here: `mountIncomingWebhook` throws * at boot when there is no effective `signature` (from this descriptor or * from `provider`) AND no explicit value below. * * - omitted → derived. `signature` / `provider` present → `'signature'`; * nothing present → boot refuses. * - `'provider'` → the handler verifies with the provider's own SDK * (Stripe's `constructEvent`, etc.). The framework's generic HMAC layer * is not the authority and does not require a framework-side secret. * - `'none'` → deliberately unverified, because a gateway + IP allow-list * owns the trust boundary. Logged as a warning at every boot, on purpose. */ readonly verification?: 'signature' | 'provider' | 'none'; /** Idempotency key extraction. Default `'Idempotency-Key'` header. * Provider-templates override this to match the provider's wire * format. */ readonly idempotency?: { /** Header name OR a function that reads from headers/body. */ readonly from: string | ((headers: Readonly>, rawBody: Uint8Array) => string | undefined); /** TTL the framework retains the key for de-dup. Default 7 days. */ readonly ttl?: '5m' | '1h' | '6h' | '1d' | '7d' | '30d'; }; /** Validated body schema. The framework decodes the request body * AFTER signature verification, BEFORE the handler runs. */ readonly payload: Schema.Schema; /** Body parser — JSON by default. Stripe / GitHub send `application/json` * but other providers use `application/x-www-form-urlencoded`. */ readonly bodyType?: 'json' | 'form' | 'raw'; /** Provider preset — when set, the framework fills `signature` + * `idempotency` + `bodyType` from the provider's known shape. * Explicit fields above always win. */ readonly provider?: WebhookProviderDescriptor; /** Typed handler. Returns void on success, throws to reject. The * HTTP status is 2xx for success, 4xx for typed validation * errors, 5xx for handler exceptions. The framework retries * 5xx-classified failures via the provider's expected behavior * (most providers retry their own POST on 5xx). */ readonly handler: (context: IncomingWebhookContext) => Promise | void; } /** How a mounted incoming webhook authenticates its caller. Mirrors * `@voltro/runtime`'s `WebhookVerification` STRUCTURALLY — this package must * not depend on the runtime (it is imported by browser-safe descriptor files), * and the runtime reads the stamped value back by shape. */ export declare type IncomingWebhookVerification = 'signature' | 'provider' | 'none'; /** * The verification decision for a descriptor, WITHOUT mounting it. * * One function, two readers: `mountIncomingWebhook` (which turns `null` into a * boot refusal) and `voltro doctor` (which reports it as a preflight, before a * deploy discovers it). A doctor that re-derived this could disagree with the * boot it is supposed to predict — the failure shape a preflight must not have. * * `null` = nothing authenticates the caller. Declaring nothing, and declaring * `'signature'` with no scheme to verify AGAINST, are the same open endpoint, * so both answer `null`. */ export declare const incomingWebhookVerification: (descriptor: IncomingWebhookDescriptor) => IncomingWebhookVerification | null; export declare interface IncomingWorkflowFacade { start(workflowName: string, payload: Payload): Promise; signal(target: { readonly id?: string; readonly executionId?: string; readonly workflowName?: string; }, signalName: string, payload?: unknown): Promise<{ readonly eventId: string; }>; update(target: { readonly id?: string; readonly executionId?: string; readonly workflowName?: string; }, updateName: string, payload?: unknown, options?: { readonly timeoutMs?: number; readonly pollIntervalMs?: number; }): Promise<{ readonly eventId: string; readonly updateId: string; readonly completedEventId: string; readonly result: unknown; }>; } /** Is this a spec-shaped secret? Used to decide whether a target needs one minted. */ export declare const isStandardWebhooksSecret: (secret: string) => boolean; /** Type guard for file-walker discovery — every `*.webhook.tsx` * must default-export one of these. */ export declare const isWebhookDescriptor: (value: unknown) => value is WebhookDescriptor; /** * Build the handler that performs a deferred emit after commit. * * `emitter` is the BOOT-level service — the same object `ctx.webhooks` proxies. * By the time this runs the transaction is gone, so the tenant cannot be read * from a request subject and travels in the row instead. * * `maxAttempts` is 1 on purpose and it is not a shortcut. The work here is * "resolve targets and start a delivery workflow"; the DELIVERY itself already * has its own retry policy per target, with backoff, a delivery-history table * and a dashboard replay button. Retrying the fan-out on top would multiply the * two schedules together and produce duplicate delivery rows for one emit — * retrying a retry is how a webhook storm starts. What the outbox adds here is * the guarantee that the fan-out HAPPENS at all, which is precisely the crash * window the after-commit callback left open. */ export declare const makeDeferredEmitHandler: ( /** * Resolved LAZILY, and it has to be. `voltro dev` starts the outbox runner * before it builds the webhooks service (the service needs a trigger context * that needs the workflow engine), while `voltro serve` builds them the other * way round. A thunk is the one shape both orders can register, so the two * boot paths cannot disagree about whether this handler exists — which is the * drift class this repo keeps re-learning. */ emitter: () => Pick | undefined) => OutboxHandlerDefinition; /** * Evaluate a target's filter against a payload. `true` → this target receives * the delivery. * * **Every supported form, because under-describing this cost a deployment the * whole life of the feature.** The sentence that used to be here said the * key-path equality form was "v1" and that "future versions can grow operators * ({ gt, lt, in, … })" — while the operators were implemented in * `compareValue`, twenty lines below it, and had been all along. They read that * as a roadmap note, refused a `resourceIds` filter they needed, and shipped a * typed `ValidationError` telling their own users it was impossible. * * A doc comment that describes an intention rather than the code under it is * not a smaller doc — it is a wrong one, and it is more expensive than none, * because a reader who finds nothing goes and reads the source. * * `{ 'payload.status': 'paid' }` equality (a bare value) * `{ 'payload.status': { eq: 'paid' } }` explicit equality * `{ 'payload.id': { in: ['a', 'b'] } }` membership * `{ 'payload.total': { gt: 100 } }` > (numbers, or strings * `{ 'payload.total': { gte: 100 } }` >= compared lexicographically * `{ 'payload.at': { lt: '2026-01-01' } }` < — ISO dates sort correctly) * `{ 'payload.total': { lte: 100 } }` <= * * Keys are ANDed. The root of a path is `{ payload }`, so every path begins * `payload.`; a path that does not resolve reads `undefined` and fails the * comparison, which is how a typo routes nothing rather than everything. */ export declare const matchesFilter: (filter: WebhookFilter | Readonly> | null, payload: unknown) => boolean; /** A mounted handler, carrying its verification declaration. */ export declare type MountedIncomingWebhook = ((request: IncomingRequest) => Promise) & { readonly [WEBHOOK_VERIFICATION_PROPERTY]: IncomingWebhookVerification; }; /** * Build a request handler for a specific `IncomingWebhookDescriptor`. * The returned function is what stage 4 mounts on the HTTP layer. * * @throws {UnverifiedIncomingWebhook} at mount time when the descriptor has no * effective signature scheme and no explicit `verification`. */ export declare const mountIncomingWebhook: (descriptor: IncomingWebhookDescriptor, options: MountOptions) => MountedIncomingWebhook; export declare interface MountOptions { /** Resolves the per-webhook signing secret. Returning `null` for a * webhook whose verification is `'signature'` makes every delivery * answer 503 — it does NOT skip verification. */ readonly resolveSecret: (webhookId: string) => Promise; /** Optional idempotency cache override. Defaults to the * process-singleton LRU. Pass a custom cache for tests or for a * shared (Redis-backed) cache across processes. */ readonly idempotencyCache?: IdempotencyCache; /** Optional structured logger — receives `{ webhookId, event, * status, durationMs, signatureOk, idempotencyResult }` per * request. Defaults to a no-op so unit tests don't print noise. */ readonly log?: (record: IncomingLogRecord) => void; /** Optional workflow facade. Resolved lazily so host runtimes can * mount incoming routes before workflow layers finish booting. */ readonly resolveWorkflows?: () => IncomingWorkflowFacade | undefined; } /** Compute the next-retry decision given the policy + the attempt * number that just failed (1-indexed: `attempt=1` after the first * try) + optional recipient hints from the failed response. Returns * `null` when no more retries are allowed. */ export declare const nextRetry: (policy: RetryPolicy, attempt: number, lastResponse?: { readonly status?: number; readonly retryAfterSeconds?: number; }) => RetryDecision | null; export declare interface OutgoingEventDescriptor { readonly _tag: 'outgoingEvent'; readonly id: WebhookId; /** Human-readable summary surfaced in the dashboard's events list. */ readonly description?: string; /** Payload schema — drives JSON-Schema export for the dashboard's * "view example" affordance and the typed `emit()` call site * (pass the descriptor itself to `emit(descriptor, payload)` to * type the payload; either way `emit` DECODES the payload against * this schema and rejects a mismatch with `WebhookPayloadInvalid` * before any delivery is created). * * The BODY on the wire is this payload and nothing else — no envelope. It * used to say recipients receive `{ event, eventId, occurredAt, payload }`, * which was never true of the code: `emit` posts `JSON.stringify(payload)` * verbatim (asserted in `deliverWorkflow.integration.test.ts`). The event * name, ids and attempt count ride in HEADERS (`x-voltro-event`, * `webhook-id`, `x-voltro-attempt`), which is also where Standard Webhooks * puts the delivery metadata — so the generated consumer package reads them * from there. */ readonly payload: Schema.Schema; /** Schema version. Increment when the payload shape changes in a * way subscribers must adapt to. The dashboard surfaces version * divergence per-target. New subscriptions pin to this version by * default. Default 1. */ readonly version?: number; /** Default retry policy for new subscriptions of this event — * applied when the subscriber doesn't pass `retry` explicitly * (explicit wins, then this, then the package default). */ readonly defaultRetry?: RetryPolicy; /** Default signing scheme for new subscriptions of this event — * applied when the subscriber doesn't pass `signing` explicitly * (explicit wins, then this, then HMAC-SHA-256 with a generated * 32-byte secret). */ readonly defaultSigning?: SignatureScheme; /** Rate-limit ceiling that ALL deliveries of this event share — * protects against a runaway emit loop. Enforced by the delivery * workflow as a fixed-window counter at the shared store (holds * across replicas); over-limit deliveries are DEFERRED to the * next window (parked as `status='pending'` rows), never dropped. * Per-target rate-limits are configured at subscribe time. */ readonly globalRateLimit?: { readonly perMinute: number; }; } export declare const parseDuration: (literal: DurationLiteral) => number; export declare const parseTtl: (literal: keyof typeof TTL_MS | undefined) => number; /** The fixed window backing "per minute". Injectable in * `buildDeliverWebhookExecute` options for tests; production always * uses the default. */ export declare const RATE_WINDOW_MS = 60000; export declare const RATE_WINDOW_TABLE = "_voltro_webhook_rate_windows"; export declare interface RateAcquireResult { readonly acquired: boolean; /** When `acquired === false`: milliseconds until the next window * opens — the caller durable-sleeps this long, then re-acquires. * `0` when acquired. */ readonly retryInMs: number; } /** One rate-limit scope a delivery must hold a slot in before POSTing. * `target:` for the per-target limit, `event:` for * an event's global limit. */ export declare interface RateScope { readonly key: string; /** Max deliveries admitted per window for this scope. */ readonly limit: number; } /** * Record a terminal delivery outcome against the target's failure * streak. `succeeded === true` resets the streak to 0; otherwise it * CAS-increments and auto-disables when the streak reaches * `autoDisableAfter` (a positive integer; null/≤0 disables the * feature). `reason` is the terminal failure detail stamped onto the * target when the auto-disable fires. Returns the post-write streak + * whether THIS call auto-disabled. */ export declare const recordDeliveryOutcome: (store: DataStore, targetId: string, succeeded: boolean, reason: string, now?: Date, /** * Disable the target on THIS failure, whatever the streak says. * * Standard Webhooks is explicit that `410 Gone` means "disable the endpoint", * and that is a different signal from a streak: the receiver has TOLD us the * endpoint is gone, so waiting for `autoDisableAfter` more failures is us * ignoring an answer we asked for. It also fires when `autoDisableAfter` is * unset — the streak feature being off does not make a 410 ambiguous. */ disableNow?: boolean) => Promise; /** Refund a slot claimed by `consumeRateSlot` — used when a later scope * in the same acquisition denies, so a deferred delivery doesn't burn * window budget it never used. */ export declare const releaseRateSlot: (store: DataStore, key: string, bucket: number) => Promise; /** Apply defaults + freeze the resolved target descriptor. Pure. * * Default resolution is three-tiered: the subscriber's explicit * value wins, then the event descriptor's per-event defaults * (`defaultSigning` / `defaultRetry` / `version` from * the declared event's `webhook:` block), then the package-global defaults. */ export declare const resolveSubscribe: (input: SubscribeInput, event?: OutgoingEventDescriptor, /** The event this ROW is for, and the secret the group shares. Both default * to the single-event behaviour, so existing callers are unchanged. */ forEvent?: string, sharedSecret?: string) => { readonly id: string; readonly event: string; readonly url: string; readonly secret: string; readonly signing: SignatureScheme; readonly retry: RetryPolicy; readonly filter: Readonly> | null; readonly scope: Readonly> | null; readonly headers: Readonly> | null; readonly rateLimitPerMinute: number | null; readonly active: boolean; readonly format: "json" | "form" | "xml"; readonly autoDisableAfter: number | null; readonly consecutiveFailures: number; readonly autoDisabledAt: Date | null; readonly autoDisableReason: string | null; readonly payloadVersion: number; readonly description: string | null; readonly tenantId: string | null; }; export declare interface RetryDecision { /** Milliseconds to sleep before the next attempt. */ readonly delayMs: number; /** The strategy-computed delay before jitter was applied — surfaced * for log lines like `next attempt 1.2s (base 1.0s + jitter)`. */ readonly baseDelayMs: number; } export declare interface RetryPolicy { readonly strategy: RetryStrategy; /** Max total attempts including the first. After this, the * delivery is marked `failed` permanently. Default 8 attempts * with exponential gives ~8.5h of total wait before giving up. */ readonly maxAttempts: number; /** First-retry delay. Subsequent delays derive from `strategy`. */ readonly initialDelay: DurationLiteral; /** Cap on any single delay. Hard-stops exponential growth. */ readonly maxDelay: DurationLiteral; /** HTTP status codes that trigger a retry. Anything outside this * list is a permanent failure (won't retry). Default covers the * conservative "transient" set: 408 / 425 / 429 / 5xx. */ readonly retryOn?: ReadonlyArray; /** When the recipient returns `Retry-After` (HTTP standard), honour * it instead of computing our own delay. Default true. */ readonly honourRetryAfter?: boolean; /** Jitter mode applied on top of computed delay. `none` is * deterministic (useful for tests); `full` is the cheapest * thundering-herd guard for many subscribers retrying together. */ readonly jitter?: 'none' | 'full'; } export declare type RetryStrategy = 'fixed' | 'linear' | 'exponential'; export declare type SignatureScheme = HmacSignatureScheme | CustomSignatureScheme | StandardWebhooksSignatureScheme; export declare interface SignedRequest { /** Every header the scheme contributes, lowercased. */ readonly headers: Readonly>; /** The instant embedded in the signature, when the scheme embeds one. * Surfaced so callers can log it beside the delivery record. */ readonly timestamp?: number; } /** * Render the signature headers for an outbound request. * * ONE function for every scheme: a caller that has to know which scheme it is * holding in order to assemble the right headers is a caller that will get a * new scheme wrong. */ export declare const signRequest: (scheme: SignatureScheme, input: SignRequestInput) => SignedRequest; /** Everything a scheme may need to render its headers. */ export declare interface SignRequestInput { /** The exact bytes on the wire — the signature covers these, never the * pre-encoded JSON. */ readonly rawBody: Uint8Array; /** The signing secret for this target. */ readonly secret: string; /** * The unique message identifier. Standard Webhooks signs OVER it and sends it * as `webhook-id`; the other schemes ignore it. * * Required rather than optional, and the reason is worth keeping: it is also * that deployment's idempotency key, so a delivery without one cannot be * de-duplicated by the receiver at all. Making it optional would let a call * site omit it and produce a spec-shaped message that is missing the one * field the spec tells consumers to rely on. */ readonly messageId: string; /** Unix seconds. Defaults to now — a test pins it. */ readonly timestampSeconds?: number; } /** * Slack-style: `X-Slack-Signature: v0=hex`, with the timestamp in the SEPARATE * `X-Slack-Request-Timestamp` header and a signed content of * `v0::`. * * This used to be a stub whose `verify` returned `false` unconditionally, with a * comment explaining that the route adapter spliced the two header values into * one string before calling it. It did — so `slackSignature()` was an exported, * documented API that rejected every request when used as written, and the only * thing that made Slack work was a special case in `incoming.ts` keyed on * `_tag === 'custom'`. A verifier that receives the whole header map does not * need either. */ export declare const slackSignature: (options?: { readonly replayWindowSeconds?: number; }) => CustomSignatureScheme; /** Default replay tolerance. The spec requires A tolerance and names no number, * so this is ours: 5 minutes, matching the window every other scheme here uses. */ export declare const STANDARD_WEBHOOKS_DEFAULT_TOLERANCE_SECONDS = 300; /** The three headers, exactly as the spec names them (lowercase). */ export declare const STANDARD_WEBHOOKS_ID_HEADER = "webhook-id"; export declare const STANDARD_WEBHOOKS_MAX_KEY_BYTES = 64; /** Spec: "Between 24 bytes (192 bits) and 64 bytes (512 bits)". */ export declare const STANDARD_WEBHOOKS_MIN_KEY_BYTES = 24; /** Secret serialization prefix. */ export declare const STANDARD_WEBHOOKS_SECRET_PREFIX = "whsec_"; export declare const STANDARD_WEBHOOKS_SIGNATURE_HEADER = "webhook-signature"; export declare const STANDARD_WEBHOOKS_TIMESTAMP_HEADER = "webhook-timestamp"; /** The symmetric signature identifier. `v1a` is the asymmetric one — see the * header for why it is refused rather than ignored. */ export declare const STANDARD_WEBHOOKS_VERSION = "v1"; /** * The three outbound headers for one delivery. * * `secrets` may carry more than one: during a rotation the spec has the producer * sign "with both the current and old keys" and space-delimit the tokens, so a * consumer holding either key still verifies. Order is producer-chosen; a * consumer tries each. */ export declare const standardWebhooksHeaders: (input: { readonly secrets: ReadonlyArray; readonly messageId: string; readonly timestampSeconds: number; readonly rawBody: Uint8Array; }) => Readonly>; /** * The raw HMAC key behind a `whsec_…` secret. * * The prefix is REQUIRED, and that strictness is deliberate. A hex secret (what * this plugin's generic schemes mint) is also valid base64, so a lenient * "decode if it looks like base64" rule would silently HMAC 48 bytes of garbage * — self-consistently, so our own round-trip would pass while every conformant * consumer library rejected the delivery. Refusing loudly at signing time is the * only version of this that cannot ship a broken endpoint. */ export declare const standardWebhooksKey: (secret: string) => Buffer; /** * Standard Webhooks v1.0.0 — the interoperable scheme. * * Use it on an INCOMING webhook whose sender signs to the spec, and read it as * the OUTGOING default via `defaultOutgoingSignature()`. * * Symmetric (HMAC-SHA256, `v1`) only. The spec's asymmetric half (ed25519, * `v1a`, `whsk_`/`whpk_`) is not implemented, and `verifyRequest` says so * explicitly rather than reporting a generic mismatch. */ export declare const standardWebhooksSignature: (options?: { readonly toleranceSeconds?: number; readonly previousSecret?: string; }) => StandardWebhooksSignatureScheme; /** * The Standard Webhooks (v1.0.0) scheme — three `webhook-*` headers, a * `msg_id.timestamp.payload` signed content, base64 `v1,` signatures, a * `whsec_`-prefixed base64 key. Implemented in `./standardWebhooks`, which * carries the spec quotations and the interop vector it is verified against. */ export declare interface StandardWebhooksSignatureScheme { readonly _tag: 'standardWebhooks'; /** Replay tolerance. The spec REQUIRES a tolerance and names no number; 300s * is ours. */ readonly toleranceSeconds?: number; /** A second key accepted on verify AND signed alongside on send — the spec's * zero-downtime rotation, which is what the space-delimited signature list * exists for. */ readonly previousSecret?: string; } /** One `v1,` token. */ export declare const standardWebhooksSignatureToken: (secret: string, messageId: string, timestampSeconds: number, rawBody: Uint8Array) => string; /** The exact bytes the spec signs: `msg_id.timestamp.payload`. */ export declare const standardWebhooksSignedContent: (messageId: string, timestampSeconds: number, rawBody: Uint8Array) => Buffer; export declare type StandardWebhooksVerifyResult = { readonly ok: true; readonly messageId: string; readonly timestampSeconds: number; } | { readonly ok: false; readonly reason: string; }; /** Stripe-style: `Stripe-Signature: t=NNN,v1=hex`, 5-min replay window. */ export declare const stripeSignature: (header?: string) => HmacSignatureScheme; export declare interface SubscribeInput { /** A single event. Use `events` for a multi-event subscription. */ readonly event?: string; /** * The tenant this subscription belongs to. * * **You should not normally pass this.** `ctx.webhooks.subscribe(...)` binds it * from the acting subject, exactly as `emit` binds `EmitOptions.tenantId` — it * is here for the same reason the read side has it, and for the admin tooling * the error message names. * * It exists because the write path had NO binding at all and no way to supply * one. `_voltro_webhook_targets` carries `.with(tenant())`, the mixin scopes by * the REQUEST subject, and this service is built once at boot with the app-level * store and no subject. So `subscribe` from an authenticated executor died with * `TenantScopeViolation: cannot insert into tenant-scoped table without an * authenticated tenant` — and both escapes that message offered were * unreachable: you cannot "authenticate first" (the service is subject-less by * construction) and you could not "pass tenantId explicitly" (this field did not * exist). * * A deployment hit it on every subscribe for the life of the feature. The * consequence worth recording: their `count(*) FROM _voltro_webhook_targets = 0` * did not mean "unused", it meant "never worked" — and both sides read that zero * as reassurance. */ readonly tenantId?: string | null; readonly url: string; /** Secret used to sign deliveries to this target. Auto-generated * when omitted (32-byte hex). The caller receives it ONCE in the * return value — store it if you want to display it again later. */ readonly secret?: string; readonly signing?: SignatureScheme; readonly retry?: RetryPolicy; /** * Optional routing filter — only emits whose payload matches fan out to this * target. See {@link WebhookFilter}: dotted paths into `{ payload }`, a bare * value for equality, or `{ eq | in | gt | gte | lt | lte }`. */ readonly filter?: WebhookFilter; /** * Subscribe ONE url to SEVERAL events at once. * * A subscription, as every webhook UI models it, is one URL with a list of * event checkboxes — ours, Stripe's, GitHub's. The row is one event, so five * checkboxes are five rows, and the gap between the two is where an app ends * up hand-rolling every operation a user thinks of as single. * * The rows created here share ONE secret and one `scope`, which is what makes * the group addressable afterwards — and it is not a convenience: the receiver * verifies one signature for one URL, so N rows for one endpoint MUST sign * identically. Without this the only way to say so was to read the secret * column back out of `_voltro_webhook_targets`, which is exactly the coupling * `listDeliveries` was added to remove, re-entered through another door. * * Mutually exclusive with `event`. Pass whichever reads better; one event is * still one row. */ readonly events?: ReadonlyArray; /** * The APP's own scoping dimension — opaque, stored and returned verbatim. * * `.with(tenant())` is one level too coarse for real deployments: endpoints * are commonly scoped to a team, a project or a workspace, and a tenant has * many of those. Pass whatever identifies yours; `listTargets({ scope })` * filters on equality against it. The framework never interprets it. */ readonly scope?: Readonly>; readonly headers?: Readonly>; readonly rateLimitPerMinute?: number; readonly format?: 'json' | 'form' | 'xml'; /** Auto-disable the target after this many CONSECUTIVE terminal * delivery failures (dead-letter guard). Omit / `undefined` to * leave it OFF. Must be a positive integer when set. */ readonly autoDisableAfter?: number; readonly payloadVersion?: number; readonly description?: string; } export declare interface SubscribeResult { /** Every row created, when `events` named more than one. Absent for a * single-event subscribe, where `id` and `event` already say it. */ readonly targets?: ReadonlyArray<{ readonly id: string; readonly event: string; }>; readonly id: string; readonly event: string; readonly url: string; /** Secret. Surfaced ONLY at subscribe time. The dashboard's * rotate-secret UI returns the new secret here; subsequent reads * via `getTarget(id)` redact it. */ readonly secret: string; readonly signing: SignatureScheme; readonly retry: RetryPolicy; } /** The patchable subset of a target. Absent keys are left alone. */ export declare interface TargetPatch { readonly url?: string; readonly description?: string | null; readonly filter?: WebhookFilter | null; readonly scope?: Readonly> | null; readonly headers?: Readonly> | null; readonly rateLimitPerMinute?: number | null; readonly autoDisableAfter?: number | null; readonly format?: 'json' | 'form' | 'xml'; } export declare const TARGETS_TABLE = "_voltro_webhook_targets"; /** * WHICH target(s) an operation addresses. * * A string is one row. A `{ scope }` is the GROUP — every row whose opaque * scope matches, which for a multi-event subscription is the whole endpoint. * * This exists because a subscription, as a user models it, is one URL with a * list of event checkboxes, while a row is one event. Without a group selector * every operation the user thinks of as single — pause the endpoint, fix its * URL, rotate its secret, read its history — becomes a fan-out the app writes * by hand, and `rotateSecret` in particular becomes delete + re-subscribe, * which mints new ids and orphans the delivery history. */ export declare type TargetSelector = string | { readonly scope: Readonly>; }; export declare interface TargetSummary { readonly id: string; /** The app's own scoping dimension as written at subscribe time, verbatim. * `null` when the app stored none. */ readonly scope: Readonly> | null; readonly event: string; readonly url: string; readonly active: boolean; readonly rateLimitPerMinute: number | null; readonly description: string | null; readonly payloadVersion: number; /** Auto-disable threshold (`null` = OFF). */ readonly autoDisableAfter: number | null; /** Current consecutive terminal-failure streak. */ readonly consecutiveFailures: number; /** When this target was auto-disabled (`null` = not auto-disabled). * A non-null value + `active:false` distinguishes an auto-disabled * target from a manually-paused one on the dashboard. */ readonly autoDisabledAt: string | null; /** The terminal failure reason that tripped the auto-disable. */ readonly autoDisableReason: string | null; } declare const TTL_MS: Record; /** Thrown at mount (i.e. at boot) for a webhook that verifies nothing and never * said so. The message is the deliverable — it names the endpoint and every * way out of the failure. */ export declare class UnverifiedIncomingWebhook extends Error { readonly webhookId: string; readonly name = "UnverifiedIncomingWebhook"; constructor(webhookId: string); } /** * Convenience accessor for handlers. The framework's `AppContext` * types the `webhooks` slot as `unknown` to avoid a circular dep * between `@voltro/runtime` and `@voltro/plugin-webhooks`; this * helper performs the structural cast and throws if the plugin * isn't active. * * import { useWebhooks } from '@voltro/plugin-webhooks' * const execute = async (input, ctx) => { * const webhooks = useWebhooks(ctx) * await webhooks.emit('order.completed', { orderId: input.id }) * } */ export declare const useWebhooks: (ctx: { readonly webhooks?: unknown; }) => WebhooksServiceShape; export declare const useWebhooksEffect: Effect.Effect; export declare const validateSubscribe: (input: SubscribeInput) => void; /** * Constant-time verification of an incoming request. * * Returns the structured reason so a caller can attach it to a 401 (helpful in * dev, fine to elide in prod). */ export declare const verifyRequest: (scheme: SignatureScheme, input: VerifyRequestInput) => VerifyResult; export declare interface VerifyRequestInput { readonly rawBody: Uint8Array; readonly secret: string; /** The request's headers, LOWERCASED keys. A scheme reads whichever of them * it needs (Slack needs two; Standard Webhooks needs three). */ readonly headers: Readonly>; /** Overridable clock for the replay window — tests pin it. */ readonly nowSeconds?: number; } export declare type VerifyResult = { readonly ok: true; } | { readonly ok: false; readonly reason: string; }; /** * Verify an inbound Standard-Webhooks request. * * Order matters and follows the spec's own reasoning: shape → timestamp * tolerance (a replay is rejected before any HMAC work) → constant-time compare * against every accepted key × every offered token. */ export declare const verifyStandardWebhooks: (input: { readonly secrets: ReadonlyArray; readonly rawBody: Uint8Array; readonly headers: Readonly>; readonly toleranceSeconds?: number; readonly nowSeconds?: number; }) => StandardWebhooksVerifyResult; /** Compare a target's pinned `payloadVersion` against the event's * current `version`. Three states: * * - `current` — version match. Target sees the current shape. * - `behind` — target.payloadVersion < event.version. The * consumer is reading an older shape; the dashboard * surfaces a warning + "Re-pin" affordance. * - `ahead` — target.payloadVersion > event.version. Rare — * usually a config error (someone re-pinned to a * not-yet-released version). The dashboard flags * this as a config-issue, not a normal divergence. */ export declare type VersionState = 'current' | 'behind' | 'ahead'; /** * One row per delivery attempt — NOT per emit. An emit fans out to * N targets; each target then runs ≤ `maxAttempts` deliveries. The * primary key is `(deliveryId, attempt)`; `deliveryId` is shared * across retries of the SAME (event, payload, target) tuple so the * dashboard groups them. * * Status lifecycle: `pending` (queued — the target was paused at emit * time, or the attempt is rate-deferred to the next window) → * `inFlight` → `succeeded` | `failed` | `retryScheduled`. On retry the * workflow creates a new `(deliveryId, attempt+1)` row; on * resume/deferral the SAME attempt-1 row transitions out of `pending`. */ export declare const _voltroWebhookDeliveriesTable: Table<"_voltro_webhook_deliveries", FieldDefinitions<{ readonly id: ColumnBuilder; /** Grouping key for retries of the same delivery. See above. */ readonly deliveryId: ColumnBuilder; /** FK to `_voltro_webhook_targets.id`. */ readonly targetId: ColumnBuilder; /** Event id at emit time — denormalised so dashboard listings * don't need to JOIN through `_voltro_webhook_targets` (which * may have been deleted by the time someone audits this row). */ readonly event: ColumnBuilder; /** The emit's `eventId` (shared by every target fan-out of one * emit) — lets `resumeTarget`'s flush re-trigger a queued delivery * with its ORIGINAL event id, and correlates rows across targets. */ readonly eventId: ColumnBuilder; /** Attempt counter (1-indexed). */ readonly attempt: ColumnBuilder; readonly status: ColumnBuilder<"failed" | "pending" | "succeeded" | "inFlight" | "retryScheduled", "text", boolean>; /** Payload as sent over the wire. Stored verbatim — re-rendering * from a referenced event row would lose the snapshot if the * source event was deleted. */ readonly payload: ColumnBuilder; /** HTTP status code returned. `null` for transport errors (DNS, * TLS, timeout) — `errorMessage` carries the detail. */ readonly responseStatus: ColumnBuilder; /** Response body sample (clipped to 8 KB). Lets the dashboard * show the recipient's error reply inline. */ readonly responseBody: ColumnBuilder; /** Transport-layer error message ("ENOTFOUND", "ETIMEDOUT", TLS * handshake failure). Null on HTTP-layer errors (those carry * `responseStatus`). */ readonly errorMessage: ColumnBuilder; /** End-to-end attempt latency in ms — includes DNS, TLS, request, * response read. Useful for the dashboard's "slowest endpoint" * ranking. */ readonly latencyMs: ColumnBuilder; /** When this attempt was scheduled (NOT when it was sent — sent * time is approximately `scheduledAt + queueDelay`). */ readonly scheduledAt: ColumnBuilder; /** When the next retry is due (set ONLY when `status = * retryScheduled`). Lets the workflow's sleep block read its * wake time from the persisted row across restarts. */ readonly nextAttemptAt: ColumnBuilder; }> & { tenantId: ColumnDefinition; } & { readonly createdAt: ColumnDefinition; readonly updatedAt: ColumnDefinition; readonly createdBy: ColumnDefinition; readonly updatedBy: ColumnDefinition; }, true, never>; /** * ONE row per declared event, stamped every time `emit` runs. * * **Why this is not derivable from `_voltro_webhook_deliveries`, which is the * whole reason the table exists.** A delivery row is written when an emit MATCHES * a target. So "no delivery rows" conflates three different facts: * * 1. no `emit(...)` call site exists, or none ever ran ← the defect * 2. it ran, but nobody was subscribed yet * 3. it ran, but every target's `filter` excluded the payload, or every * target was paused * * Only (1) is a bug, and it is the one a deployment spent a week finding by hand: * seven of eleven advertised events had no emit call site anywhere. Reading (2) * or (3) as (1) turns a working integration into a false alarm; reading (1) as * (2) hides it. Delivery history also ages out — `_voltro_webhook_deliveries` * carries a 90-day retention — so an event emitted correctly and quietly can * decay into looking dead. * * This row is written REGARDLESS of whether any target matched, which is * precisely the axis history cannot see. The dashboard shows both, labelled, * and their disagreement is itself the useful signal: emitted but never * delivered means every target is paused, filtered out, or failing. * * Deliberately NOT tenant-scoped. The question is "does this event have a live * call site in this deployment", which is a property of the CODE, not of a * tenant's data — and scoping it per tenant would make an event look dead for * every tenant that has not happened to trigger it yet. */ export declare const _voltroWebhookEventStatsTable: Table<"_voltro_webhook_event_stats", FieldDefinitions<{ /** * A GENERATED id. The event name is the natural key and it is deliberately * NOT reused here. * * An `id()` column is `VARCHAR(64)` on MySQL/MariaDB (`applier.ts`), while an * event name is allowed 191. Using the name as the id would make a namespaced * event longer than 64 characters fail its insert — and because the stats * write is best-effort and swallows every error, it would fail SILENTLY and * the dashboard would report "never emitted" for a live event. That is * precisely the false positive this table exists to remove, reintroduced by * its own primary key, on exactly the dialect that reported the original * defect. */ readonly id: ColumnBuilder; /** The natural key. Unique, so a concurrent double-insert loses rather than * duplicating the row. */ readonly event: ColumnBuilder; /** Total emits seen, including those that matched no target. */ readonly emitCount: ColumnBuilder; readonly lastEmitAt: ColumnBuilder; readonly createdAt: ColumnBuilder; readonly updatedAt: ColumnBuilder; }>, true, never>; /** * Fixed-window rate-limit counters — one row per (scope, minute * bucket), where scope is `target:` (per-target * `rateLimitPerMinute`) or `event:` (an outgoing event's * `globalRateLimit`). The delivery workflow claims a slot via a CAS * loop over `count` (see `rateLimit.ts`) BEFORE every wire POST, so * the cap holds across replicas — the counter lives here, never in * process memory. * * Deliberately NOT tenant-scoped: rows carry only a scope key + an * integer count (no payload, no secret, no tenant data), and the * background delivery workflow that writes them has no request * subject. The deterministic PK `@` is the * `insertIgnore` conflict target for the first-in-window create race. */ export declare const _voltroWebhookRateWindowsTable: Table<"_voltro_webhook_rate_windows", FieldDefinitions<{ /** Deterministic `@` — always supplied explicitly by * the CAS writer (the prefix scheme only fires for omitted ids, * which never happens here). */ readonly id: ColumnBuilder; /** `target:` | `event:`. */ readonly scope: ColumnBuilder; /** Epoch-minute bucket (`floor(now / windowMs)`). */ readonly bucket: ColumnBuilder; /** Slots consumed in this window. */ readonly count: ColumnBuilder; readonly createdAt: ColumnBuilder; readonly updatedAt: ColumnBuilder; }>, true, never>; /** * One row per subscribed delivery target. Created via * `webhooks.subscribe(...)`. Read-only from app code; mutate via * the `webhooks` service so the framework can run validation + * generate secrets + invalidate caches. */ export declare const _voltroWebhookTargetsTable: Table<"_voltro_webhook_targets", FieldDefinitions<{ readonly id: ColumnBuilder; /** Event id from the outgoing-event descriptor (e.g. `'order.completed'`). */ readonly event: ColumnBuilder; /** Active subscription URL the delivery posts to. */ readonly url: ColumnBuilder; /** Per-target signing secret. Generated at subscribe time when not * supplied. ENCRYPTED at rest — it is the key that signs every delivery, * so a database read must not be a forgery kit — and server-only on the * wire: `subscribe` hands it to the caller ONCE, and no crud path exposes * it again. Rows written before this column was `.encrypted()` still read * (a non-ciphertext value passes through); `voltro db encrypt-column * _voltro_webhook_targets.secret` converts them in place. */ readonly secret: ColumnBuilder; /** Serialised `SignatureScheme` discriminated union — see `signing.ts`. * Stored as JSON so future schemes don't require a schema migration. */ readonly signing: ColumnBuilder; /** Serialised `RetryPolicy` — see `retry.ts`. */ readonly retry: ColumnBuilder; /** * The APP's own scoping dimension. Opaque to the framework. * * Stored and returned verbatim, never interpreted — the framework does not * know what a team, a project or a workspace is, and does not need to. Reads * filter on equality against the json you wrote: * * subscribe({ event, url, scope: { teamId: 'q970…' } }) * listTargets({ scope: { teamId } }) * * It exists because `.with(tenant())` is one level too coarse for real * deployments. A deployment's endpoints are scoped to a TEAM and a tenant has * many teams; every read filters by it and every write guards on it, so * without this column the plugin cannot hold their rows at all and they keep * a parallel table. * * The precedent is `_voltro_presence.meta`, and it is worth stating because * it decided a migration: that column is json the framework stores and never * interprets, and it is the ONLY reason the same deployment's presence * migration was lossless — their three denormalised columns went straight in. * The general form: a plugin that stores rows in an app's database on the * app's behalf needs one place for the app's own dimension. */ readonly scope: ColumnBuilder; /** Optional predicate filter (subset of `Predicate`) — the engine * evaluates this against each emit's payload to decide whether * this target receives the delivery. */ readonly filter: ColumnBuilder; /** Optional custom headers merged with the framework's * Content-Type + signature header. Values larger than 2 KB are * rejected at subscribe time. */ readonly headers: ColumnBuilder; /** Per-target rate limit — at most N wire POSTs per minute to this * target, enforced by the delivery workflow via a shared-store * fixed-window counter (`_voltro_webhook_rate_windows`, so the cap * holds across replicas). Excess deliveries are DEFERRED: parked as * `status='pending'` rows and durable-slept until the next window * opens — they're never dropped silently. */ readonly rateLimitPerMinute: ColumnBuilder; /** Soft-disable without deleting the row — the dashboard's * "Pause" affordance flips this. While paused, emits against this * target accumulate as `_voltro_webhook_deliveries` rows with * status `'pending'` (no POST happens); `resumeTarget` flushes * them through the delivery workflow in emit order. */ readonly active: ColumnBuilder; /** Format the payload is delivered as. `json` is the default and * what every modern integration expects. `form` * (`application/x-www-form-urlencoded`, bracketed-key flattening) * and `xml` (`application/xml`, ``-rooted) exist for * SOAP-era partners; the delivery workflow re-encodes the payload * into this format and signs the re-encoded bytes. */ readonly format: ColumnBuilder<"json" | "form" | "xml", "text", true>; /** Auto-disable threshold — after this many CONSECUTIVE terminal * delivery failures the target is auto-paused (dead-letter guard). * `null` (the default) disables the feature. When it trips, the * target flips to `active=false` and subsequent emits QUEUE as * `status='pending'` rows (same as a manual pause — nothing is * dropped); a manual `resumeTarget` re-activates, flushes the queue, * and clears the streak. */ readonly autoDisableAfter: ColumnBuilder; /** Consecutive terminal-failure streak. Incremented on each terminal * `failed` delivery, reset to 0 on any `succeeded`. Drives * `autoDisableAfter`. Multi-replica-correct via a CAS loop * (`autoDisable.ts`). */ readonly consecutiveFailures: ColumnBuilder; /** When the auto-disable last fired (`null` = never / cleared by a * manual resume). Surfaced on the inspect panel. */ readonly autoDisabledAt: ColumnBuilder; /** The terminal failure reason that tripped the auto-disable * (`null` = not auto-disabled). Surfaced on the inspect panel. */ readonly autoDisableReason: ColumnBuilder; /** Schema version bound at subscribe time. Lets the dashboard * show which targets are still pinned to an older event version * after the producer bumps it. */ readonly payloadVersion: ColumnBuilder; /** Human-readable label surfaced in the dashboard listing. */ readonly description: ColumnBuilder; }> & { tenantId: ColumnDefinition; } & { readonly createdAt: ColumnDefinition; readonly updatedAt: ColumnDefinition; readonly createdBy: ColumnDefinition; readonly updatedBy: ColumnDefinition; }, true, never>; /** The effect name. Namespaced under `voltro.` because it is framework-owned: * an app's own `*.outbox.ts` may not claim it, and the loader's * duplicate-effect check is what enforces that. */ export declare const WEBHOOK_EMIT_EFFECT = "voltro.webhook.emit"; /** The property name the runtime's boot gate reads off a mounted handler. */ export declare const WEBHOOK_VERIFICATION_PROPERTY: "voltroWebhookVerification"; /** * Thrown by `WebhooksService.replay` when no delivery row exists for the * given `deliveryId` — the row can't be re-triggered because the * original payload + target are unknown. */ export declare class WebhookDeliveryNotFound extends WebhookDeliveryNotFound_base { } declare const WebhookDeliveryNotFound_base: Schema.TaggedErrorClass; } & { /** The delivery id that did not resolve to a row. */ deliveryId: typeof Schema.String; }>; export declare type WebhookDescriptor = OutgoingEventDescriptor | IncomingWebhookDescriptor | WebhookProviderDescriptor; /** * A target's routing filter: dotted paths INTO the emitted envelope. * * The root is `{ payload }`, so every path starts `payload.` — a path that does * not resolve reads as `undefined` and the comparison fails, which is how a * typo'd path silently routes nothing. */ export declare type WebhookFilter = Readonly>; export declare type WebhookFilterValue = string | number | boolean | null | { readonly eq?: string | number | boolean | null; readonly in?: ReadonlyArray; readonly gt?: number | string; readonly gte?: number | string; readonly lt?: number | string; readonly lte?: number | string; }; /** Stable identifier for an event or webhook. Drives the database * primary key, the inspect endpoint URL, the dashboard listing. Use * dotted-camelCase (`order.completed`, `user.signedUp`). */ export declare type WebhookId = string; /** * Thrown by `WebhooksService.emit` when the payload does not decode * against the outgoing event's declared `payload` schema (from its * declared event's `webhook:` block). The emit is rejected BEFORE any * delivery row is written or workflow triggered — a schema-violating * payload never reaches a subscriber. */ export declare class WebhookPayloadInvalid extends WebhookPayloadInvalid_base { } declare const WebhookPayloadInvalid_base: Schema.TaggedErrorClass; } & { /** The outgoing event id the payload was emitted for. */ event: typeof Schema.String; /** Tree-formatted schema decode issues (one line per violation). */ issues: typeof Schema.String; }>; /** * Raised by the delivery workflow's wire-encoder when the target's * `format` (`form` | `xml`) cannot honestly represent the payload * shape — e.g. a `form` target whose payload is a bare array/scalar * (form encoding is a flat key=value list with no top-level array * representation), or an `xml` target whose object key is not a valid * XML element name. Surfaced BEFORE any wire POST: the delivery row is * written `failed` with this reason, so a mis-formatted target never * silently ships JSON bytes under a wrong content-type. */ export declare class WebhookPayloadUnrepresentable extends WebhookPayloadUnrepresentable_base { } declare const WebhookPayloadUnrepresentable_base: Schema.TaggedErrorClass; } & { /** The wire format that could not represent the payload. */ format: Schema.Literal<["json", "form", "xml"]>; /** Human-readable detail — which shape/key defeated the encoder. */ reason: typeof Schema.String; }>; /** * Thrown by `WebhooksService.updateTargetPayloadVersion` when the * requested version is not a positive integer. */ export declare class WebhookPayloadVersionInvalid extends WebhookPayloadVersionInvalid_base { } declare const WebhookPayloadVersionInvalid_base: Schema.TaggedErrorClass; } & { /** The rejected version value (as supplied by the caller). */ version: typeof Schema.Number; }>; export declare interface WebhookProviderDescriptor { readonly _tag: 'webhookProvider'; readonly id: string; /** Display name for the dashboard. */ readonly name: string; readonly signature: SignatureScheme; readonly idempotency: NonNullable['idempotency']>; readonly bodyType: 'json' | 'form' | 'raw'; /** Optional discriminator: provider-specific event-type extraction * (e.g. Stripe's top-level `type` field). Used by typed handlers * to narrow on payload variant. */ readonly eventTypeFrom?: (body: unknown) => string | undefined; } /** * The permissions webhook delivery declares. * * `network:outbound:*` and nothing else, deliberately: this entry contributes * no interceptors, no inspect endpoints, no `extendSchema` tables (the webhook * tables ride the framework's feature-mix assembly the moment a `*.webhook.tsx` * file exists — see `cli/src/frameworkTableAssembly.ts`), so no other hook * permission would be truthful either. */ export declare const WEBHOOKS_PLUGIN_PERMISSIONS: ReadonlyArray; /** * Register webhook delivery with the plugin system. * * ```ts * // app.config.ts * import { webhooksPlugin } from '@voltro/plugin-webhooks' * * export default { * plugins: [webhooksPlugin()], * } * ``` * * Adding it changes no behavior — `*.webhook.tsx` discovery, delivery and the * incoming routes work exactly as before. What it changes is the boot audit: * webhooks now appears in the plugin permission report and the plugin manifest * with its outbound declaration, instead of being invisible to both. */ export declare const webhooksPlugin: (options?: WebhooksPluginOptions) => VoltroPlugin; export declare interface WebhooksPluginOptions { /** * Distinguishing suffix when an app registers the entry more than once * (matching the `@voltro/plugin-mail#` convention). Plugin names must * be unique in one app's plugin list. */ readonly name?: string; } export declare class WebhooksService extends WebhooksService_base { } declare const WebhooksService_base: Context.TagClass; export declare interface WebhooksServiceOptions { /** Discovered outgoing events (projected from declared events). When present, * `subscribe` resolves the event's `defaultSigning` / * `defaultRetry` / `version` before the package-global defaults, * and `emit` decodes the payload against the event's schema. */ readonly events?: ReadonlyArray>; } export declare interface WebhooksServiceShape { readonly subscribe: (input: SubscribeInput) => Promise; /** Emit an event to every subscribed target. Accepts the event id * OR the declared event itself — passing the * descriptor types `payload` against its schema at the call site. * Either way, when the descriptor is known (directly or via the * discovered-events registry) the payload is DECODED against its * schema and a mismatch throws `WebhookPayloadInvalid` before any * delivery is created. */ readonly emit:

(event: string | OutgoingEventDescriptor

| DeclaredEventLike, payload: P, options?: EmitOptions) => Promise; /** Manual re-trigger for the dashboard's "Replay" button on a * failed delivery row. Re-runs the workflow at attempt 1 with * the original payload. */ readonly replay: (deliveryId: string) => Promise; /** List currently-subscribed targets for an event, or all when * `event` is omitted. The dashboard's "Targets" page consumes * this. */ readonly listTargets: (event?: string, /** Filter to targets whose opaque `scope` equals this, key for key. The * match is exact on the keys you pass — a target with extra keys still * matches, so `{ teamId }` finds targets scoped to that team regardless of * what else the app stored beside it. */ scope?: Readonly>) => Promise>; /** Soft-disable a target without deleting the row. While paused, * emits against this target queue under * `_voltro_webhook_deliveries` with status='pending' (no POST * happens); `resumeTarget` flushes them. */ /** Every row a selector addresses. Refuses a scope that matches nothing — * an operation that silently affects zero rows is worse than an error. */ readonly resolveTargets: (selector: TargetSelector) => Promise>; readonly pauseTarget: (target: TargetSelector) => Promise; /** Re-enable a paused target AND flush its queued * `status='pending'` deliveries through the normal delivery * workflow, in emit order (`createdAt` ascending — millisecond * granularity) per target. */ readonly resumeTarget: (target: TargetSelector) => Promise; /** Hard-delete a target. Its queued `status='pending'` rows are * deleted with it (nothing left to flush); an in-flight workflow * run's `fetch-target` activity sees a null row and exits * cleanly. */ readonly deleteTarget: (target: TargetSelector) => Promise; /** Rotate the per-target signing secret. Returns the new secret * ONCE — store it client-side if you need to display it again. * In-flight retries continue with the OLD secret since signing * happens at attempt time using the row's then-current value * (acceptable: providers retry within seconds, the rotation * window is tight). */ /** * Rotate the signing secret. Addressed by a SCOPE this rotates every row of * the endpoint to the SAME new value — which is the point: N rows for one URL * must sign identically, and the previous way to achieve it was delete + * re-subscribe, minting new ids and orphaning the delivery history. */ readonly rotateSecret: (target: TargetSelector) => Promise<{ readonly secret: string; }>; /** Re-pin a target's `payloadVersion` to the event's current * version. Called from the dashboard's "Re-pin" action after * the consumer has updated their handler to accept the new * payload shape. */ readonly updateTargetPayloadVersion: (targetId: string, version: number) => Promise; /** * Edit a target in place. * * Without it, changing a URL, description, filter, headers or rate limit * means delete + re-subscribe — which ROTATES the secret (every receiver has * to be reconfigured) and ORPHANS the delivery history (the rows point at a * target id that no longer exists). Neither is what "I fixed a typo in the * URL" should cost. * * `event` and `secret` are deliberately not patchable: changing the event * makes it a different subscription, and the secret has `rotateSecret`, which * returns the new value once. */ readonly updateTarget: (target: TargetSelector, patch: TargetPatch) => Promise; /** * Send one delivery to ONE target, bypassing fan-out and the filter. * * `emit` fans out to every matching target, so there is no way to answer "is * THIS endpoint reachable" — the first button in every webhook UI. The * delivery is real: it is signed, recorded in the delivery log and retried * like any other, so what it proves is what production will do. */ readonly testTarget: (targetId: string, payload?: unknown) => Promise<{ readonly deliveryId: string; }>; /** * Read the delivery log. * * There was no service method for it, so the only way to build a management * view was to query `_voltro_webhook_deliveries` directly — and a deployment * declined, correctly: the 0.24.0 `agent_messages` rename taught them what * app code coupled to a framework table name costs. That one was survivable * because it was a rename; a column change would not be. * * The two heavy columns (`payload`, `responseBody`) are omitted here and * available from `getDelivery`, so a list view does not pull response bodies * for 200 rows. */ readonly listDeliveries: (filter?: { readonly targetId?: string; /** Every row of an endpoint's group, so a history view needs no N-way * merge-and-re-sort in the app. */ readonly scope?: Readonly>; readonly status?: DeliverySummary['status']; readonly since?: Date; readonly limit?: number; }) => Promise>; /** One delivery attempt including its payload and response body. */ readonly getDelivery: (id: string) => Promise; } /** * Thrown by `WebhooksService.subscribe` (via `validateSubscribe`) when * the subscribe input fails a policy check — a non-http(s) URL, a secret * shorter than the minimum, a degenerate retry policy, or an * out-of-range rate limit. `field` names the offending input field; * `reason` is the human-readable detail (carries the offending value). */ export declare class WebhookSubscribeInvalid extends WebhookSubscribeInvalid_base { } declare const WebhookSubscribeInvalid_base: Schema.TaggedErrorClass; } & { /** The offending subscribe-input field: `'url'` | `'event'` | * `'secret'` | `'retry'` | `'rateLimitPerMinute'`. */ field: typeof Schema.String; /** Human-readable detail, including the offending value. */ reason: typeof Schema.String; }>; /** * The three bookkeeping tables, as a TUPLE rather than a plain array. * * The distinction is load-bearing for callers: `ReadonlyArray` makes * every index access `TableLike | undefined` under `noUncheckedIndexedAccess`, * so the documented `const [targets, deliveries] = webhookTables()` typed both * as possibly-undefined — and feeding those into `databaseHandle` poisoned the * inferred types of the app's OWN tables alongside them (`database.orders is * possibly undefined`). The shipped `api-webhooks` template carried exactly * that error. A tuple says what the function already guaranteed. */ export declare const webhookTables: () => readonly [ Table<"_voltro_webhook_targets", FieldDefinitions<{ readonly id: ColumnBuilder; /** Event id from the outgoing-event descriptor (e.g. `'order.completed'`). */ readonly event: ColumnBuilder; /** Active subscription URL the delivery posts to. */ readonly url: ColumnBuilder; /** Per-target signing secret. Generated at subscribe time when not * supplied. ENCRYPTED at rest — it is the key that signs every delivery, * so a database read must not be a forgery kit — and server-only on the * wire: `subscribe` hands it to the caller ONCE, and no crud path exposes * it again. Rows written before this column was `.encrypted()` still read * (a non-ciphertext value passes through); `voltro db encrypt-column * _voltro_webhook_targets.secret` converts them in place. */ readonly secret: ColumnBuilder; /** Serialised `SignatureScheme` discriminated union — see `signing.ts`. * Stored as JSON so future schemes don't require a schema migration. */ readonly signing: ColumnBuilder; /** Serialised `RetryPolicy` — see `retry.ts`. */ readonly retry: ColumnBuilder; /** * The APP's own scoping dimension. Opaque to the framework. * * Stored and returned verbatim, never interpreted — the framework does not * know what a team, a project or a workspace is, and does not need to. Reads * filter on equality against the json you wrote: * * subscribe({ event, url, scope: { teamId: 'q970…' } }) * listTargets({ scope: { teamId } }) * * It exists because `.with(tenant())` is one level too coarse for real * deployments. A deployment's endpoints are scoped to a TEAM and a tenant has * many teams; every read filters by it and every write guards on it, so * without this column the plugin cannot hold their rows at all and they keep * a parallel table. * * The precedent is `_voltro_presence.meta`, and it is worth stating because * it decided a migration: that column is json the framework stores and never * interprets, and it is the ONLY reason the same deployment's presence * migration was lossless — their three denormalised columns went straight in. * The general form: a plugin that stores rows in an app's database on the * app's behalf needs one place for the app's own dimension. */ readonly scope: ColumnBuilder; /** Optional predicate filter (subset of `Predicate`) — the engine * evaluates this against each emit's payload to decide whether * this target receives the delivery. */ readonly filter: ColumnBuilder; /** Optional custom headers merged with the framework's * Content-Type + signature header. Values larger than 2 KB are * rejected at subscribe time. */ readonly headers: ColumnBuilder; /** Per-target rate limit — at most N wire POSTs per minute to this * target, enforced by the delivery workflow via a shared-store * fixed-window counter (`_voltro_webhook_rate_windows`, so the cap * holds across replicas). Excess deliveries are DEFERRED: parked as * `status='pending'` rows and durable-slept until the next window * opens — they're never dropped silently. */ readonly rateLimitPerMinute: ColumnBuilder; /** Soft-disable without deleting the row — the dashboard's * "Pause" affordance flips this. While paused, emits against this * target accumulate as `_voltro_webhook_deliveries` rows with * status `'pending'` (no POST happens); `resumeTarget` flushes * them through the delivery workflow in emit order. */ readonly active: ColumnBuilder; /** Format the payload is delivered as. `json` is the default and * what every modern integration expects. `form` * (`application/x-www-form-urlencoded`, bracketed-key flattening) * and `xml` (`application/xml`, ``-rooted) exist for * SOAP-era partners; the delivery workflow re-encodes the payload * into this format and signs the re-encoded bytes. */ readonly format: ColumnBuilder<"json" | "form" | "xml", "text", true>; /** Auto-disable threshold — after this many CONSECUTIVE terminal * delivery failures the target is auto-paused (dead-letter guard). * `null` (the default) disables the feature. When it trips, the * target flips to `active=false` and subsequent emits QUEUE as * `status='pending'` rows (same as a manual pause — nothing is * dropped); a manual `resumeTarget` re-activates, flushes the queue, * and clears the streak. */ readonly autoDisableAfter: ColumnBuilder; /** Consecutive terminal-failure streak. Incremented on each terminal * `failed` delivery, reset to 0 on any `succeeded`. Drives * `autoDisableAfter`. Multi-replica-correct via a CAS loop * (`autoDisable.ts`). */ readonly consecutiveFailures: ColumnBuilder; /** When the auto-disable last fired (`null` = never / cleared by a * manual resume). Surfaced on the inspect panel. */ readonly autoDisabledAt: ColumnBuilder; /** The terminal failure reason that tripped the auto-disable * (`null` = not auto-disabled). Surfaced on the inspect panel. */ readonly autoDisableReason: ColumnBuilder; /** Schema version bound at subscribe time. Lets the dashboard * show which targets are still pinned to an older event version * after the producer bumps it. */ readonly payloadVersion: ColumnBuilder; /** Human-readable label surfaced in the dashboard listing. */ readonly description: ColumnBuilder; }> & { tenantId: ColumnDefinition; } & { readonly createdAt: ColumnDefinition; readonly updatedAt: ColumnDefinition; readonly createdBy: ColumnDefinition; readonly updatedBy: ColumnDefinition; }, true, never>, Table<"_voltro_webhook_deliveries", FieldDefinitions<{ readonly id: ColumnBuilder; /** Grouping key for retries of the same delivery. See above. */ readonly deliveryId: ColumnBuilder; /** FK to `_voltro_webhook_targets.id`. */ readonly targetId: ColumnBuilder; /** Event id at emit time — denormalised so dashboard listings * don't need to JOIN through `_voltro_webhook_targets` (which * may have been deleted by the time someone audits this row). */ readonly event: ColumnBuilder; /** The emit's `eventId` (shared by every target fan-out of one * emit) — lets `resumeTarget`'s flush re-trigger a queued delivery * with its ORIGINAL event id, and correlates rows across targets. */ readonly eventId: ColumnBuilder; /** Attempt counter (1-indexed). */ readonly attempt: ColumnBuilder; readonly status: ColumnBuilder<"failed" | "pending" | "succeeded" | "inFlight" | "retryScheduled", "text", boolean>; /** Payload as sent over the wire. Stored verbatim — re-rendering * from a referenced event row would lose the snapshot if the * source event was deleted. */ readonly payload: ColumnBuilder; /** HTTP status code returned. `null` for transport errors (DNS, * TLS, timeout) — `errorMessage` carries the detail. */ readonly responseStatus: ColumnBuilder; /** Response body sample (clipped to 8 KB). Lets the dashboard * show the recipient's error reply inline. */ readonly responseBody: ColumnBuilder; /** Transport-layer error message ("ENOTFOUND", "ETIMEDOUT", TLS * handshake failure). Null on HTTP-layer errors (those carry * `responseStatus`). */ readonly errorMessage: ColumnBuilder; /** End-to-end attempt latency in ms — includes DNS, TLS, request, * response read. Useful for the dashboard's "slowest endpoint" * ranking. */ readonly latencyMs: ColumnBuilder; /** When this attempt was scheduled (NOT when it was sent — sent * time is approximately `scheduledAt + queueDelay`). */ readonly scheduledAt: ColumnBuilder; /** When the next retry is due (set ONLY when `status = * retryScheduled`). Lets the workflow's sleep block read its * wake time from the persisted row across restarts. */ readonly nextAttemptAt: ColumnBuilder; }> & { tenantId: ColumnDefinition; } & { readonly createdAt: ColumnDefinition; readonly updatedAt: ColumnDefinition; readonly createdBy: ColumnDefinition; readonly updatedBy: ColumnDefinition; }, true, never>, Table<"_voltro_webhook_rate_windows", FieldDefinitions<{ /** Deterministic `@` — always supplied explicitly by * the CAS writer (the prefix scheme only fires for omitted ids, * which never happens here). */ readonly id: ColumnBuilder; /** `target:` | `event:`. */ readonly scope: ColumnBuilder; /** Epoch-minute bucket (`floor(now / windowMs)`). */ readonly bucket: ColumnBuilder; /** Slots consumed in this window. */ readonly count: ColumnBuilder; readonly createdAt: ColumnBuilder; readonly updatedAt: ColumnBuilder; }>, true, never>, Table<"_voltro_webhook_event_stats", FieldDefinitions<{ /** * A GENERATED id. The event name is the natural key and it is deliberately * NOT reused here. * * An `id()` column is `VARCHAR(64)` on MySQL/MariaDB (`applier.ts`), while an * event name is allowed 191. Using the name as the id would make a namespaced * event longer than 64 characters fail its insert — and because the stats * write is best-effort and swallows every error, it would fail SILENTLY and * the dashboard would report "never emitted" for a live event. That is * precisely the false positive this table exists to remove, reintroduced by * its own primary key, on exactly the dialect that reported the original * defect. */ readonly id: ColumnBuilder; /** The natural key. Unique, so a concurrent double-insert loses rather than * duplicating the row. */ readonly event: ColumnBuilder; /** Total emits seen, including those that matched no target. */ readonly emitCount: ColumnBuilder; readonly lastEmitAt: ColumnBuilder; readonly createdAt: ColumnBuilder; readonly updatedAt: ColumnBuilder; }>, true, never>]; /** A target selected for management no longer exists. */ export declare class WebhookTargetNotFound extends WebhookTargetNotFound_base { } declare const WebhookTargetNotFound_base: Schema.TaggedErrorClass; } & { targetId: typeof Schema.String; }>; export declare type WireFormat = 'json' | 'form' | 'xml'; export { }