import { ColumnBuilder } from '@voltro/database'; import { ColumnDefinition } from '@voltro/database'; import { FieldDefinitions } from '@voltro/database'; import { Table } from '@voltro/database'; /** * 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 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>]; export { }