@voltro/plugin-webhooks 0.28.0 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,7 +5,7 @@ property of its respective copyright holders and is used under the terms of
5
5
  its license. This file is provided for attribution; it grants no rights in
6
6
  @voltro/plugin-webhooks itself, which is proprietary (see LICENSE).
7
7
 
8
- Generated from the resolved runtime dependency closure (49 packages).
8
+ Generated from the resolved runtime dependency closure (52 packages).
9
9
 
10
10
  ---
11
11
 
@@ -3956,6 +3956,34 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
3956
3956
  THE SOFTWARE.
3957
3957
  ```
3958
3958
 
3959
+ ## isomorphic.js@0.2.5
3960
+
3961
+ License: MIT
3962
+
3963
+ ```
3964
+ The MIT License (MIT)
3965
+
3966
+ Copyright (c) 2020 Kevin Jahns <kevin.jahns@protonmail.com>.
3967
+
3968
+ Permission is hereby granted, free of charge, to any person obtaining a copy
3969
+ of this software and associated documentation files (the "Software"), to deal
3970
+ in the Software without restriction, including without limitation the rights
3971
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
3972
+ copies of the Software, and to permit persons to whom the Software is
3973
+ furnished to do so, subject to the following conditions:
3974
+
3975
+ The above copyright notice and this permission notice shall be included in all
3976
+ copies or substantial portions of the Software.
3977
+
3978
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
3979
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
3980
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
3981
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
3982
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
3983
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
3984
+ SOFTWARE.
3985
+ ```
3986
+
3959
3987
  ## jose@6.2.4
3960
3988
 
3961
3989
  License: MIT
@@ -4018,6 +4046,34 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
4018
4046
  SOFTWARE.
4019
4047
  ```
4020
4048
 
4049
+ ## lib0@0.2.117
4050
+
4051
+ License: MIT
4052
+
4053
+ ```
4054
+ The MIT License (MIT)
4055
+
4056
+ Copyright (c) 2019 Kevin Jahns <kevin.jahns@protonmail.com>.
4057
+
4058
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4059
+ of this software and associated documentation files (the "Software"), to deal
4060
+ in the Software without restriction, including without limitation the rights
4061
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
4062
+ copies of the Software, and to permit persons to whom the Software is
4063
+ furnished to do so, subject to the following conditions:
4064
+
4065
+ The above copyright notice and this permission notice shall be included in all
4066
+ copies or substantial portions of the Software.
4067
+
4068
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
4069
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
4070
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
4071
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
4072
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
4073
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
4074
+ SOFTWARE.
4075
+ ```
4076
+
4021
4077
  ## mime@3.0.0
4022
4078
 
4023
4079
  License: MIT
@@ -4566,3 +4622,33 @@ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
4566
4622
  CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
4567
4623
  ```
4568
4624
 
4625
+ ## yjs@13.6.31
4626
+
4627
+ License: MIT
4628
+
4629
+ ```
4630
+ The MIT License (MIT)
4631
+
4632
+ Copyright (c) 2023
4633
+ - Kevin Jahns <kevin.jahns@protonmail.com>.
4634
+ - Chair of Computer Science 5 (Databases & Information Systems), RWTH Aachen University, Germany
4635
+
4636
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4637
+ of this software and associated documentation files (the "Software"), to deal
4638
+ in the Software without restriction, including without limitation the rights
4639
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
4640
+ copies of the Software, and to permit persons to whom the Software is
4641
+ furnished to do so, subject to the following conditions:
4642
+
4643
+ The above copyright notice and this permission notice shall be included in all
4644
+ copies or substantial portions of the Software.
4645
+
4646
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
4647
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
4648
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
4649
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
4650
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
4651
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
4652
+ SOFTWARE.
4653
+ ```
4654
+
package/dist/index.d.ts CHANGED
@@ -7,11 +7,13 @@ import { Effect } from 'effect';
7
7
  import { EventDescriptor } from '@voltro/protocol';
8
8
  import { EventWebhookSpec } from '@voltro/protocol';
9
9
  import { FieldDefinitions } from '@voltro/database';
10
+ import { OutboxHandlerDefinition } from '@voltro/runtime';
10
11
  import { Schema } from 'effect';
11
12
  import { Table } from '@voltro/database';
12
13
  import { Workflow } from '@effect/workflow';
13
14
  import { WorkflowEngine } from '@effect/workflow/WorkflowEngine';
14
15
  import { WorkflowInstance } from '@effect/workflow/WorkflowEngine';
16
+ import { WorkflowRunHandle } from '@voltro/protocol';
15
17
 
16
18
  /**
17
19
  * Acquire one slot in EVERY scope for the window containing `now`
@@ -63,14 +65,6 @@ export declare const buildDeliverWebhookExecute: (ctx: AppContext, options?: Del
63
65
  attempts: number;
64
66
  }, never, WorkflowEngine | WorkflowInstance>;
65
67
 
66
- /**
67
- * Build a `WebhooksServiceShape` that uses the in-process store
68
- * directly + an injected `trigger` for delivery. The `trigger` is
69
- * the seam where the framework wires the actual @effect/workflow
70
- * runner (stage 4). For tests and the very first dev-mode loop we
71
- * accept a simple async function that resolves when the workflow
72
- * has been kicked off (NOT when it completes).
73
- */
74
68
  export declare const buildWebhooksService: (ctx: AppContext, trigger: (input: {
75
69
  readonly deliveryId: string;
76
70
  readonly targetId: string;
@@ -131,6 +125,13 @@ export declare const defaultOutgoingSignature: () => HmacSignatureScheme;
131
125
  * 640s. Total ~17 minutes before giving up. */
132
126
  export declare const defaultRetryPolicy: () => RetryPolicy;
133
127
 
128
+ /** The row payload. Deliberately flat and JSON-only — see the header. */
129
+ export declare interface DeferredEmitPayload {
130
+ readonly event: string;
131
+ readonly payload: unknown;
132
+ readonly tenantId: string | null;
133
+ }
134
+
134
135
  export declare const defineIncomingWebhook: <Body>(spec: Omit<IncomingWebhookDescriptor<Body>, "_tag">) => IncomingWebhookDescriptor<Body>;
135
136
 
136
137
  export declare const defineWebhookProvider: (spec: Omit<WebhookProviderDescriptor, "_tag">) => WebhookProviderDescriptor;
@@ -219,6 +220,30 @@ export declare type DurationLiteral = `${number}${'ms' | 's' | 'm' | 'h' | 'd'}`
219
220
  export declare interface EmitOptions {
220
221
  /** Only deliver to targets of this tenant. Absent ⇒ every target. */
221
222
  readonly tenantId?: string | null;
223
+ /**
224
+ * Dispatch NOW instead of after the enclosing transaction commits.
225
+ *
226
+ * Inside a mutation, `emit` defaults to a transactional enqueue: the intent
227
+ * commits with your rows or rolls back with them, and the POST goes out
228
+ * afterwards. That is the right default and it is not the right answer
229
+ * everywhere — a diagnostic ping, or an emit whose receiver the caller is
230
+ * about to poll, wants the request on the wire immediately.
231
+ *
232
+ * It does NOT make the emit safe. A rollback after an immediate emit still
233
+ * tells a subscriber about a change that did not happen. The point of the flag
234
+ * is that the trade is written at the call site: the old behaviour was
235
+ * un-transactional by accident, and nothing said so.
236
+ *
237
+ * Outside a mutation this is a no-op — there is no transaction to skip.
238
+ *
239
+ * **Read by `ctx.webhooks`, not by this service.** The deferral lives in the
240
+ * request-scoped proxy (`cli/src/requestWebhooks.ts`), because only the
241
+ * request knows whether there is a transaction; the boot-level service is
242
+ * always immediate by construction. The option is declared here because this
243
+ * is the type a caller sees — but calling the boot service directly with it
244
+ * changes nothing, and that is the truthful behaviour rather than a gap.
245
+ */
246
+ readonly immediate?: boolean;
222
247
  }
223
248
 
224
249
  export declare interface EmitResult {
@@ -410,12 +435,7 @@ export declare interface IncomingWebhookDescriptor<Body> {
410
435
  }
411
436
 
412
437
  export declare interface IncomingWorkflowFacade {
413
- start<Payload = unknown>(workflowName: string, payload: Payload): Promise<{
414
- readonly id: string;
415
- readonly workflowName: string;
416
- readonly executionId: string;
417
- readonly status: 'running';
418
- }>;
438
+ start<Payload = unknown>(workflowName: string, payload: Payload): Promise<WorkflowRunHandle>;
419
439
  signal(target: {
420
440
  readonly id?: string;
421
441
  readonly executionId?: string;
@@ -442,6 +462,33 @@ export declare interface IncomingWorkflowFacade {
442
462
  * must default-export one of these. */
443
463
  export declare const isWebhookDescriptor: (value: unknown) => value is WebhookDescriptor;
444
464
 
465
+ /**
466
+ * Build the handler that performs a deferred emit after commit.
467
+ *
468
+ * `emitter` is the BOOT-level service — the same object `ctx.webhooks` proxies.
469
+ * By the time this runs the transaction is gone, so the tenant cannot be read
470
+ * from a request subject and travels in the row instead.
471
+ *
472
+ * `maxAttempts` is 1 on purpose and it is not a shortcut. The work here is
473
+ * "resolve targets and start a delivery workflow"; the DELIVERY itself already
474
+ * has its own retry policy per target, with backoff, a delivery-history table
475
+ * and a dashboard replay button. Retrying the fan-out on top would multiply the
476
+ * two schedules together and produce duplicate delivery rows for one emit —
477
+ * retrying a retry is how a webhook storm starts. What the outbox adds here is
478
+ * the guarantee that the fan-out HAPPENS at all, which is precisely the crash
479
+ * window the after-commit callback left open.
480
+ */
481
+ export declare const makeDeferredEmitHandler: (
482
+ /**
483
+ * Resolved LAZILY, and it has to be. `voltro dev` starts the outbox runner
484
+ * before it builds the webhooks service (the service needs a trigger context
485
+ * that needs the workflow engine), while `voltro serve` builds them the other
486
+ * way round. A thunk is the one shape both orders can register, so the two
487
+ * boot paths cannot disagree about whether this handler exists — which is the
488
+ * drift class this repo keeps re-learning.
489
+ */
490
+ emitter: () => Pick<WebhooksServiceShape, "emit"> | undefined) => OutboxHandlerDefinition;
491
+
445
492
  /** Evaluate a target's filter against a payload. Returns `true`
446
493
  * when the target should receive this delivery. Supports the
447
494
  * simple key-path form `'payload.field': value` for v1; future
@@ -592,6 +639,7 @@ forEvent?: string, sharedSecret?: string) => {
592
639
  readonly autoDisableReason: string | null;
593
640
  readonly payloadVersion: number;
594
641
  readonly description: string | null;
642
+ readonly tenantId: string | null;
595
643
  };
596
644
 
597
645
  export declare interface RetryDecision {
@@ -654,6 +702,30 @@ export declare const stripeSignature: (header?: string) => HmacSignatureScheme;
654
702
  export declare interface SubscribeInput {
655
703
  /** A single event. Use `events` for a multi-event subscription. */
656
704
  readonly event?: string;
705
+ /**
706
+ * The tenant this subscription belongs to.
707
+ *
708
+ * **You should not normally pass this.** `ctx.webhooks.subscribe(...)` binds it
709
+ * from the acting subject, exactly as `emit` binds `EmitOptions.tenantId` — it
710
+ * is here for the same reason the read side has it, and for the admin tooling
711
+ * the error message names.
712
+ *
713
+ * It exists because the write path had NO binding at all and no way to supply
714
+ * one. `_voltro_webhook_targets` carries `.with(tenant())`, the mixin scopes by
715
+ * the REQUEST subject, and this service is built once at boot with the app-level
716
+ * store and no subject. So `subscribe` from an authenticated executor died with
717
+ * `TenantScopeViolation: cannot insert into tenant-scoped table without an
718
+ * authenticated tenant` — and both escapes that message offered were
719
+ * unreachable: you cannot "authenticate first" (the service is subject-less by
720
+ * construction) and you could not "pass tenantId explicitly" (this field did not
721
+ * exist).
722
+ *
723
+ * A consumer hit it on every subscribe for the life of the feature. The
724
+ * consequence worth recording: their `count(*) FROM _voltro_webhook_targets = 0`
725
+ * did not mean "unused", it meant "never worked" — and both sides read that zero
726
+ * as reassurance.
727
+ */
728
+ readonly tenantId?: string | null;
657
729
  readonly url: string;
658
730
  /** Secret used to sign deliveries to this target. Auto-generated
659
731
  * when omitted (32-byte hex). The caller receives it ONCE in the
@@ -857,7 +929,7 @@ export declare const _voltroWebhookDeliveriesTable: Table<"_voltro_webhook_deliv
857
929
  readonly eventId: ColumnBuilder<string | null, "text", boolean>;
858
930
  /** Attempt counter (1-indexed). */
859
931
  readonly attempt: ColumnBuilder<number, "integer", boolean>;
860
- readonly status: ColumnBuilder<"failed" | "succeeded" | "pending" | "inFlight" | "retryScheduled", "text", boolean>;
932
+ readonly status: ColumnBuilder<"failed" | "succeeded" | "inFlight" | "pending" | "retryScheduled", "text", boolean>;
861
933
  /** Payload as sent over the wire. Stored verbatim — re-rendering
862
934
  * from a referenced event row would lose the snapshot if the
863
935
  * source event was deleted. */
@@ -892,6 +964,60 @@ export declare const _voltroWebhookDeliveriesTable: Table<"_voltro_webhook_deliv
892
964
  readonly updatedBy: ColumnDefinition<string | null, "reference", boolean>;
893
965
  }, true, never>;
894
966
 
967
+ /**
968
+ * ONE row per declared event, stamped every time `emit` runs.
969
+ *
970
+ * **Why this is not derivable from `_voltro_webhook_deliveries`, which is the
971
+ * whole reason the table exists.** A delivery row is written when an emit MATCHES
972
+ * a target. So "no delivery rows" conflates three different facts:
973
+ *
974
+ * 1. no `emit(...)` call site exists, or none ever ran ← the defect
975
+ * 2. it ran, but nobody was subscribed yet
976
+ * 3. it ran, but every target's `filter` excluded the payload, or every
977
+ * target was paused
978
+ *
979
+ * Only (1) is a bug, and it is the one a consumer spent a week finding by hand:
980
+ * seven of eleven advertised events had no emit call site anywhere. Reading (2)
981
+ * or (3) as (1) turns a working integration into a false alarm; reading (1) as
982
+ * (2) hides it. Delivery history also ages out — `_voltro_webhook_deliveries`
983
+ * carries a 90-day retention — so an event emitted correctly and quietly can
984
+ * decay into looking dead.
985
+ *
986
+ * This row is written REGARDLESS of whether any target matched, which is
987
+ * precisely the axis history cannot see. The dashboard shows both, labelled,
988
+ * and their disagreement is itself the useful signal: emitted but never
989
+ * delivered means every target is paused, filtered out, or failing.
990
+ *
991
+ * Deliberately NOT tenant-scoped. The question is "does this event have a live
992
+ * call site in this deployment", which is a property of the CODE, not of a
993
+ * tenant's data — and scoping it per tenant would make an event look dead for
994
+ * every tenant that has not happened to trigger it yet.
995
+ */
996
+ export declare const _voltroWebhookEventStatsTable: Table<"_voltro_webhook_event_stats", FieldDefinitions<{
997
+ /**
998
+ * A GENERATED id. The event name is the natural key and it is deliberately
999
+ * NOT reused here.
1000
+ *
1001
+ * An `id()` column is `VARCHAR(64)` on MySQL/MariaDB (`applier.ts`), while an
1002
+ * event name is allowed 191. Using the name as the id would make a namespaced
1003
+ * event longer than 64 characters fail its insert — and because the stats
1004
+ * write is best-effort and swallows every error, it would fail SILENTLY and
1005
+ * the dashboard would report "never emitted" for a live event. That is
1006
+ * precisely the false positive this table exists to remove, reintroduced by
1007
+ * its own primary key, on exactly the dialect that reported the original
1008
+ * defect.
1009
+ */
1010
+ readonly id: ColumnBuilder<string, "id", boolean>;
1011
+ /** The natural key. Unique, so a concurrent double-insert loses rather than
1012
+ * duplicating the row. */
1013
+ readonly event: ColumnBuilder<string, "text", boolean>;
1014
+ /** Total emits seen, including those that matched no target. */
1015
+ readonly emitCount: ColumnBuilder<number, "integer", true>;
1016
+ readonly lastEmitAt: ColumnBuilder<Date, "timestamp", boolean>;
1017
+ readonly createdAt: ColumnBuilder<Date, "timestamp", boolean>;
1018
+ readonly updatedAt: ColumnBuilder<Date, "timestamp", boolean>;
1019
+ }>, true, never>;
1020
+
895
1021
  /**
896
1022
  * Fixed-window rate-limit counters — one row per (scope, minute
897
1023
  * bucket), where scope is `target:<targetId>` (per-target
@@ -1029,6 +1155,11 @@ export declare const _voltroWebhookTargetsTable: Table<"_voltro_webhook_targets"
1029
1155
  readonly updatedBy: ColumnDefinition<string | null, "reference", boolean>;
1030
1156
  }, true, never>;
1031
1157
 
1158
+ /** The effect name. Namespaced under `voltro.` because it is framework-owned:
1159
+ * an app's own `*.outbox.ts` may not claim it, and the loader's
1160
+ * duplicate-effect check is what enforces that. */
1161
+ export declare const WEBHOOK_EMIT_EFFECT = "voltro.webhook.emit";
1162
+
1032
1163
  /**
1033
1164
  * Thrown by `WebhooksService.replay` when no delivery row exists for the
1034
1165
  * given `deliveryId` — the row can't be re-triggered because the
@@ -1409,7 +1540,7 @@ export declare const webhookTables: () => readonly [ Table<"_voltro_webhook_targ
1409
1540
  readonly eventId: ColumnBuilder<string | null, "text", boolean>;
1410
1541
  /** Attempt counter (1-indexed). */
1411
1542
  readonly attempt: ColumnBuilder<number, "integer", boolean>;
1412
- readonly status: ColumnBuilder<"failed" | "succeeded" | "pending" | "inFlight" | "retryScheduled", "text", boolean>;
1543
+ readonly status: ColumnBuilder<"failed" | "succeeded" | "inFlight" | "pending" | "retryScheduled", "text", boolean>;
1413
1544
  /** Payload as sent over the wire. Stored verbatim — re-rendering
1414
1545
  * from a referenced event row would lose the snapshot if the
1415
1546
  * source event was deleted. */
@@ -1455,6 +1586,29 @@ export declare const webhookTables: () => readonly [ Table<"_voltro_webhook_targ
1455
1586
  readonly count: ColumnBuilder<number, "integer", true>;
1456
1587
  readonly createdAt: ColumnBuilder<Date, "timestamp", boolean>;
1457
1588
  readonly updatedAt: ColumnBuilder<Date, "timestamp", boolean>;
1589
+ }>, true, never>, Table<"_voltro_webhook_event_stats", FieldDefinitions<{
1590
+ /**
1591
+ * A GENERATED id. The event name is the natural key and it is deliberately
1592
+ * NOT reused here.
1593
+ *
1594
+ * An `id()` column is `VARCHAR(64)` on MySQL/MariaDB (`applier.ts`), while an
1595
+ * event name is allowed 191. Using the name as the id would make a namespaced
1596
+ * event longer than 64 characters fail its insert — and because the stats
1597
+ * write is best-effort and swallows every error, it would fail SILENTLY and
1598
+ * the dashboard would report "never emitted" for a live event. That is
1599
+ * precisely the false positive this table exists to remove, reintroduced by
1600
+ * its own primary key, on exactly the dialect that reported the original
1601
+ * defect.
1602
+ */
1603
+ readonly id: ColumnBuilder<string, "id", boolean>;
1604
+ /** The natural key. Unique, so a concurrent double-insert loses rather than
1605
+ * duplicating the row. */
1606
+ readonly event: ColumnBuilder<string, "text", boolean>;
1607
+ /** Total emits seen, including those that matched no target. */
1608
+ readonly emitCount: ColumnBuilder<number, "integer", true>;
1609
+ readonly lastEmitAt: ColumnBuilder<Date, "timestamp", boolean>;
1610
+ readonly createdAt: ColumnBuilder<Date, "timestamp", boolean>;
1611
+ readonly updatedAt: ColumnBuilder<Date, "timestamp", boolean>;
1458
1612
  }>, true, never>];
1459
1613
 
1460
1614
  export declare type WireFormat = 'json' | 'form' | 'xml';