@fabricorg/webhook-delivery 0.1.2 → 0.2.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.
- package/CHANGELOG.md +45 -0
- package/README.md +184 -15
- package/dist/index.cjs +10093 -6414
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2566 -861
- package/dist/index.d.ts +2566 -861
- package/dist/index.js +10090 -6412
- package/dist/index.js.map +1 -1
- package/dist/node.cjs +1089 -746
- package/dist/node.cjs.map +1 -1
- package/dist/node.d.cts +102 -32
- package/dist/node.d.ts +102 -32
- package/dist/node.js +1088 -744
- package/dist/node.js.map +1 -1
- package/package.json +8 -7
- package/dist/destination-joCBtgvY.d.cts +0 -1444
- package/dist/destination-joCBtgvY.d.ts +0 -1444
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,50 @@
|
|
|
1
1
|
# @fabricorg/webhook-delivery
|
|
2
2
|
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- ac87d29: Add atomic, scoped `redactEventRecords` to remove persisted payload and admission facts, fence outstanding workers, and scrub late inserts using durable erasure markers. PostgreSQL requires schema migration 4 via `ensureSchema()` before the new version serves any traffic: all job and intent inserts read the new marker table. The migration also indexes scoped event lookups; that index is built without `CONCURRENTLY`, so on an existing deployment all intent writes wait until the build finishes, for a time that grows with the intents table.
|
|
8
|
+
|
|
9
|
+
Compatibility: applications may supply custom `WebhookDeliveryStore` implementations through the public port; those implementations break and must add this method. Wire format and existing method signatures are unchanged. Jobs expose an additive `erased` flag; undecided subscriptions receive the additive `event_erased` denial code without calling admission. Admission ports may also return that code. Markers are retained indefinitely because accepted future-dated events invalidate timestamp-only marker expiry. Do not call `redactEventRecords` until every instance runs this version: older instances ignore markers and the `erased` flag, so they can write the payload back, deliver it, or ask admission with cleared facts. Repeating redaction scrubs rows they wrote but cannot recall deliveries; no-op repeats preserve fences. Requests already handed to transport cannot be recalled, and an attempt begun before erasure can still send its old body afterward. Its receipt can record the outcome, but settlement at the advanced fence is refused; the pending intent can be re-sent metadata-only under at-least-once delivery. Audit attempts and evidence are preserved.
|
|
10
|
+
|
|
11
|
+
- 828a579: Add opt-in governed historical replay with `replay_events`, metadata-only `WebhookReplayRequested`, immutable request receipts, retained per-event outcomes, current-subscription reads and `runWebhookReplayCycle()`. Replay snapshots retained webhook projections, checks current eligibility and admission, and serializes pair settlement with erasure and the retention horizon. Ordinary fan-out completion stays final for its historical selection.
|
|
12
|
+
|
|
13
|
+
Deploy PostgreSQL migration 5 through `ensureSchema()` before enabling replay. All relevant writers, erasure handlers and purgers must support this contract before enablement; the migration lock does not certify mixed-version behavior. The new tables and indexes are created without `CONCURRENTLY`, so source writes can wait during index creation.
|
|
14
|
+
|
|
15
|
+
Compatibility: replay requires explicit module configuration and separate capabilities; existing custom base stores and insert signatures are unchanged. New action-name, event-type and refusal-code union members may affect exhaustive consumer switches. Replay receipts and work items contain metadata only and are never removed by this package; terminal outcomes live on the items. Items carry bounded acceptance-time interval/filter context so processing does not read or scan request membership. Replay honors the shared fan-out worker defaults and callback semantics, including `dlv_` UUIDs, batch size 25 and configured retry jitter/custom delays. Because both workers now share that option validation, `runWebhookFanOutCycle` given two or more invalid options may report a different one first; a single invalid option reports the same error as before.
|
|
16
|
+
|
|
17
|
+
Limits: replay covers retained webhook projections, not the canonical ledger or discarded fields. Completed legacy jobs with an absent actor cannot replay to subscriptions requiring identity. Over-cap requests are refused without partial acceptance; same-timestamp density can exceed the cap even for a narrow range. Replay never creates a second intent for an existing pair and never overrides a denial. Dispatch remains at-least-once. Subscription changes after the worker's last read can race insertion; subscription and delivery stores need not share a transaction.
|
|
18
|
+
|
|
19
|
+
### Patch Changes
|
|
20
|
+
|
|
21
|
+
- d69dd7a: README: recommend Node 22.23.4, 24.22.1, or 26.12.1 or newer in the same release line for the
|
|
22
|
+
Brotli decoder fix in CVE-2026-111497, and note that Node 20 is end-of-life with no fixed release.
|
|
23
|
+
The `engines` range is unchanged (`>=20`).
|
|
24
|
+
|
|
25
|
+
## 0.1.3
|
|
26
|
+
|
|
27
|
+
### Patch Changes
|
|
28
|
+
|
|
29
|
+
- 8d87a36: Accept `@fabricorg/platform-host` 8 as a peer. Host 8 changes the meaning of some failure statuses but no API these packages use, so they work with 7 and 8 unchanged.
|
|
30
|
+
- 939bcad: Build packages with tsdown in place of tsup. Preserve the published ESM/CommonJS
|
|
31
|
+
entry paths, ES2022 target, declaration filenames, and raw external module
|
|
32
|
+
specifier spellings, including the existing Node built-in spellings.
|
|
33
|
+
|
|
34
|
+
Generated JavaScript, source-map contents, declaration formatting, and internal
|
|
35
|
+
chunk layout change. Comparison with the saved tsup build found unchanged named
|
|
36
|
+
runtime exports and public declaration signatures. Declaration maps remain
|
|
37
|
+
unpublished. Facade maps are no longer published for Platform's root, evidence,
|
|
38
|
+
and policies entries, or experience-runtime's contracts entry (both `.js.map`
|
|
39
|
+
and `.cjs.map`). Their new JavaScript output has no source-map URL comment.
|
|
40
|
+
|
|
41
|
+
CommonJS interop: 29 of 72 baseline CJS files expose an `__esModule` marker;
|
|
42
|
+
0 of 57 tsdown CJS files do. The compared package entries have no default runtime
|
|
43
|
+
export, and the emitted `.d.cts` files declare none. Typed consumers of the
|
|
44
|
+
unchanged declared exports are unaffected. For entries whose marker was removed,
|
|
45
|
+
an untyped CJS `import X from` compiled with a Babel-style default-interop helper
|
|
46
|
+
now yields the namespace instead of `undefined`.
|
|
47
|
+
|
|
3
48
|
## 0.1.2
|
|
4
49
|
|
|
5
50
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -14,7 +14,8 @@ pnpm add @fabricorg/webhook-delivery@^0.1.0 @fabricorg/platform@^1.7.0 @fabricor
|
|
|
14
14
|
submission. Every operator action declares the check, so against an older Host the actions
|
|
15
15
|
refuse at execution as `admission_not_enforced` ([Secrets and parameters](#secrets-and-parameters)).
|
|
16
16
|
`@fabricorg/platform` is imported for types only.
|
|
17
|
-
- **Runtime.** Node.js 20 or newer.
|
|
17
|
+
- **Runtime.** Node.js 20 or newer; run 22.23.4, 24.22.1, or 26.12.1 or newer in the same release line in production for
|
|
18
|
+
the Brotli decoder fix in [CVE-2026-111497](https://nodejs.org/en/blog/vulnerability/october-2026-security-releases); Node 20 is end-of-life and has no fixed release. The package ships ESM and CommonJS builds with two entry
|
|
18
19
|
points: `@fabricorg/webhook-delivery` and `@fabricorg/webhook-delivery/node` (the Node transport
|
|
19
20
|
and DNS lookup). It has no runtime dependencies; the PostgreSQL stores take an injected pool
|
|
20
21
|
such as `pg.Pool`.
|
|
@@ -175,14 +176,85 @@ need a horizon), and per-record retention clocks (two clocks for one decision).
|
|
|
175
176
|
| Fan-out job (holds the actor source for admission) | terminal and older than the horizon | purge |
|
|
176
177
|
| Intent and its tombstone | terminal and older than the horizon | purge |
|
|
177
178
|
| Denial and its tombstone | older than the horizon | purge |
|
|
179
|
+
| Event erasure markers | retained indefinitely (future-dated inserts are accepted) | none |
|
|
178
180
|
| Eligibility revisions | never removed | none |
|
|
179
181
|
| Delivery configuration versions | never removed | none |
|
|
182
|
+
| Replay request receipts and work items (including terminal outcomes) | never removed by the package (audit/idempotency) | application retention policy |
|
|
180
183
|
| Attempt log, requeue evidence | never removed by the package (audit) | application retention policy |
|
|
181
184
|
|
|
182
185
|
The rejection is visible at the store, but the publisher (PR 6) decides what Host sees. A
|
|
183
186
|
`past_horizon` from `insertFanOutJob` should fail the publish with a fixed code, so the Host
|
|
184
187
|
outbox record dead-letters rather than counting as delivered.
|
|
185
188
|
|
|
189
|
+
### Event erasure
|
|
190
|
+
|
|
191
|
+
`redactEventRecords({ tenantId, spaceId, eventIds }, options)` atomically clears
|
|
192
|
+
job payloads (`null`), projection flags (`false`) and admission facts (`{}`), and
|
|
193
|
+
replaces intent bodies with metadata-only JSON using the shared serializer. It
|
|
194
|
+
covers pending and terminal records. It keeps metadata and the identifier-only
|
|
195
|
+
actor. Input accepts 1–100 distinct identifiers, using the identifier validation
|
|
196
|
+
below; empty lists and duplicates are refused. Results count newly marked events
|
|
197
|
+
and rows whose content changed. A repeat scrubs payload reintroduced by older writers;
|
|
198
|
+
a no-op repeat returns zero counts and preserves fences and leases. Jobs carry the
|
|
199
|
+
additive `erased` flag. After the tombstone check, fan-out denies undecided pairs with
|
|
200
|
+
`event_erased` without calling admission. Decided pairs keep their metadata-only
|
|
201
|
+
intents (`tests/fan-out.test.ts`, “erased events”). `event_erased` is an additive
|
|
202
|
+
admission denial code; application admission ports may return it too.
|
|
203
|
+
|
|
204
|
+
For changed rows, the job fence and pending intent fences advance, and their leases are cleared for
|
|
205
|
+
immediate reclaim, matching actor enrichment. Old fan-out writes return `lease_lost`;
|
|
206
|
+
old delivery `beginAttempt` returns `{ begun: false }`. A request already handed to
|
|
207
|
+
transport before commit cannot be recalled. The fence check linearizes at
|
|
208
|
+
`beginAttempt`: if begin commits first, its receipt remains valid. A PostgreSQL
|
|
209
|
+
response may arrive after redaction commits, so this store-only port cannot promise
|
|
210
|
+
that such an already-authorized handoff occurs before the erasure commit. Stronger
|
|
211
|
+
transport exclusion requires coordination beyond this port. Status, attempt budgets, availability,
|
|
212
|
+
attempt logs, outcomes, denials and requeue evidence are preserved. Requeue and actor
|
|
213
|
+
enrichment keep the redacted fields. These operations do not erase the application's
|
|
214
|
+
event store or deliveries already received by subscribers. An in-flight attempt can
|
|
215
|
+
still append its outcome using its receipt, but settlement at the old fence is
|
|
216
|
+
refused. Its pending intent can then be sent again with the metadata-only body,
|
|
217
|
+
consistent with at-least-once delivery.
|
|
218
|
+
|
|
219
|
+
A scoped marker is recorded even when no job exists. Inserts check it inside their
|
|
220
|
+
atomic step and scrub the new draft/body. PostgreSQL migration 4 adds
|
|
221
|
+
`webhook_event_erasures` and an intent index on `(tenant_id, space_id, event_id)`.
|
|
222
|
+
Run `ensureSchema()` before the new version serves any traffic: job and intent
|
|
223
|
+
inserts now read the marker table even when redaction is unused. The migration builds
|
|
224
|
+
the intent index without `CONCURRENTLY`, so on an existing deployment every intent
|
|
225
|
+
write (claims, attempts, settlements, inserts, cancellations) waits until the build
|
|
226
|
+
finishes, for a time that grows with the intents table. Plan the upgrade for that
|
|
227
|
+
pause. Redaction takes
|
|
228
|
+
the horizon EXCLUSIVE lock first, then reads the clock and locks matching jobs in
|
|
229
|
+
`event_id` order and intents in `delivery_id` order using set-based queries.
|
|
230
|
+
Cancellation also locks intents in `delivery_id` order. Claims use ordered
|
|
231
|
+
`FOR UPDATE SKIP LOCKED`, which does not wait on held rows. Ordinary horizon-checked
|
|
232
|
+
writers continue to use SHARE. The live “check-to-write serialization” erasure cases
|
|
233
|
+
exercise both lock orders against inserts.
|
|
234
|
+
|
|
235
|
+
Do not call `redactEventRecords` until every instance runs this version. Older
|
|
236
|
+
instances ignore markers and the `erased` flag: they can write the payload back after
|
|
237
|
+
an erasure, deliver it before any repair, and ask admission with the cleared facts,
|
|
238
|
+
which is the fail-open path `event_erased` closes. Repeating redaction scrubs rows
|
|
239
|
+
an older instance wrote (`tests/store-contract.ts`, “repeat erasure repairs legacy
|
|
240
|
+
rows”, which runs against PostgreSQL), but it cannot recall what was already delivered.
|
|
241
|
+
Custom `WebhookDeliveryStore` implementations break at the type boundary and must
|
|
242
|
+
implement the new method and its atomicity, marker and fail-closed semantics.
|
|
243
|
+
|
|
244
|
+
Markers are retained indefinitely. The proposed timestamp expiry proof would require
|
|
245
|
+
`event.recordedAt <= redaction time`, but the store accepts future-dated jobs (shared
|
|
246
|
+
contract: “job claim respects availableAt, lease expiry and limit, oldest first”).
|
|
247
|
+
A pre-recorded marker could otherwise expire before that event's horizon. Purge does
|
|
248
|
+
not remove markers or count them in `removed`. Applications should budget this storage
|
|
249
|
+
until a future contract supplies a safe upper bound on event time.
|
|
250
|
+
|
|
251
|
+
The shared `tests/store-contract.ts` erasure cases cover scoped late inserts, terminal
|
|
252
|
+
and pending data, stale fences, idempotence, and validation. Attempts, outcomes, denials
|
|
253
|
+
and requeue evidence contain identifiers, timestamps and closed diagnostic fields,
|
|
254
|
+
not payload or admission facts (`delivery-store-core.ts` input readers and evidence
|
|
255
|
+
builders). Actor evidence retains identifiers; this API is payload erasure, not removal
|
|
256
|
+
of identifying metadata.
|
|
257
|
+
|
|
186
258
|
### Identifiers and stored fields
|
|
187
259
|
|
|
188
260
|
Every identifier a store takes is refused as `input_invalid` if it is empty, longer than 256
|
|
@@ -319,9 +391,9 @@ writer a clock that has not crossed the horizon and the purge one that has, so o
|
|
|
319
391
|
floor can refuse the writer, which is the case the lock exists for. The tests then check the
|
|
320
392
|
outcome:
|
|
321
393
|
|
|
322
|
-
-
|
|
394
|
+
- Host outbox re-publication paused after reading the cutoff races a purge. It finds the job
|
|
323
395
|
(`already_present`), then the purge removes it.
|
|
324
|
-
- A purge paused before its commit races
|
|
396
|
+
- A purge paused before its commit races Host outbox re-publication. The insert waits, then sees the raised floor
|
|
325
397
|
(`past_horizon`).
|
|
326
398
|
- A late intent insert races a purge. It cannot re-create the purged intent or its tombstone.
|
|
327
399
|
- A requeue races a purge that has raised the floor past the record. The requeue is refused
|
|
@@ -388,10 +460,16 @@ before the clock is read or anything is written. It also runs at `written`, afte
|
|
|
388
460
|
before the commit. A call that returns a refusal does not reach `written`. Leave both unset
|
|
389
461
|
outside tests.
|
|
390
462
|
|
|
391
|
-
**Running the live suite.**
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
463
|
+
**Running the live suite.** From the repository root, run `pnpm build`, then
|
|
464
|
+
`pnpm test:postgres`. The runner starts a throwaway `postgres:16` database on a free localhost
|
|
465
|
+
port and removes it in its cleanup block after success, failure, Ctrl-C, or SIGTERM. Docker
|
|
466
|
+
must be installed and usable. `FABRIC_POSTGRES_TEST_IMAGE` overrides the image, and
|
|
467
|
+
`FABRIC_POSTGRES_TEST_TIMEOUT_SECONDS` overrides the 60-second readiness timeout. An existing
|
|
468
|
+
`FABRIC_POSTGRES_TEST_URL` bypasses Docker and is passed unchanged, as in the
|
|
469
|
+
`postgres-certification` CI job. The runner reports missing builds with `Run pnpm build first`;
|
|
470
|
+
rebuild after source changes. See the Live certification section in the repository file
|
|
471
|
+
`docs/operations/releasing.md` for cleanup limits and runner tests. Direct Vitest runs skip this file without the URL.
|
|
472
|
+
The suite creates one database per run and one schema per store pair.
|
|
395
473
|
|
|
396
474
|
### Contract suite
|
|
397
475
|
|
|
@@ -780,8 +858,8 @@ to `dlv_` plus a random UUID.
|
|
|
780
858
|
|
|
781
859
|
**Decision.** A job decides an event's eligibility once, after the event's eligibility has
|
|
782
860
|
settled, and the job is complete when `completeFanOutJob` succeeds. Eligibility is settled when
|
|
783
|
-
the worker's clock is at or past `recordedAt + eligibilitySettleMs`.
|
|
784
|
-
|
|
861
|
+
the worker's clock is at or past `recordedAt + eligibilitySettleMs`. Ordinary fan-out completion stays final for its historical eligibility selection. A governed
|
|
862
|
+
historical replay request has its own fixed membership and can decide previously undecided pairs.
|
|
785
863
|
|
|
786
864
|
**What it rests on.** Two assumptions about clocks that the package cannot enforce:
|
|
787
865
|
|
|
@@ -805,8 +883,9 @@ backs each:
|
|
|
805
883
|
| A revision written after an event settled does not contain its `recordedAt` | both clock assumptions above, plus a new revision starting at the store's clock and never before the previous one (`mathMax(nowMs, current.effectiveFromMs)` in the memory store's `#transition`; the `WebhookSubscriptionWriter` contract for other stores) | "a pattern change after recordedAt does not remove the event …", "… does not add the event …" (both run with one clock, so they do not test the assumptions) |
|
|
806
884
|
| A late revision does not reopen a completed job | claims take only `pending` jobs; a job requeue accepts only dead letters | "a revision stamped behind an already-settled event …" |
|
|
807
885
|
| The revision is chosen by `recordedAt`, not by when the job runs | `recordedAt: job.event.recordedAt` passed to `listEligibilityRevisions` | the pattern change, pause and resume cases |
|
|
808
|
-
|
|
|
809
|
-
|
|
|
886
|
+
| Ordinary fan-out completion is final for its historical selection | claims take only `pending` jobs (`claimFrom`, `src/memory-delivery-store.ts`); a job requeue accepts only dead letters | ordinary fan-out test "after completion no eligibility change adds or removes an intent …" |
|
|
887
|
+
| Governed replay can decide previously undecided pairs without reopening the source job | `createReplayRequest` fixes separate membership; `settleReplayJob` preserves existing pair tombstones | replay contract "historical event for a later subscription …", "existing pair denial is retained …" |
|
|
888
|
+
| Re-publishing through the Host outbox or bridge adds no second source job | `insertFanOutJob` is insert-if-absent by `eventId`; intents and denials are insert-if-absent by (subscriptionId, eventId) | "replay: …", "concurrent relays: …", the bridge overlap cases |
|
|
810
889
|
| A worker that lost its lease adds nothing | the parent fence check in `insertDeliveryIntent` / `recordAdmissionDenial`, and `return` on `lease_lost` in `processJob` | "concurrent workers: …" |
|
|
811
890
|
| Purged records stay purged | the replay horizon floor (see [Replay horizon](#replay-horizon-r5)) | the store contract's race cases |
|
|
812
891
|
|
|
@@ -822,7 +901,9 @@ first, so a pattern change neither removes the event from a subscription that ma
|
|
|
822
901
|
to one that did not. A pause written after `recordedAt` stops delivery, not fan-out: the governed
|
|
823
902
|
action's `cancelPendingIntents` and the delivery cycle's pre-handoff `readSubscription` check
|
|
824
903
|
([Delivery](#delivery)) act on the intent. A pause effective before `recordedAt` makes the revision paused, so the
|
|
825
|
-
event gets no intent or denial
|
|
904
|
+
event gets no intent or denial in ordinary fan-out; a later resume does not change that historical
|
|
905
|
+
selection. A new governed replay request after resume may decide still-undecided pairs. Remaining
|
|
906
|
+
items of a request accepted before a pause are skipped under its original active-interval rule.
|
|
826
907
|
|
|
827
908
|
**Bridge overlap.** Covered above: the first insert of an `eventId` wins and every later one is a
|
|
828
909
|
no-op, before and after the job completes.
|
|
@@ -831,8 +912,8 @@ no-op, before and after the job completes.
|
|
|
831
912
|
dead-lettered with a diagnostic code: a visible failure that the fan-out-job requeue (PR 9)
|
|
832
913
|
recovers. That is R1's "visible recoverable failure".
|
|
833
914
|
|
|
834
|
-
**Rejected.** Completing without waiting and sweeping later for
|
|
835
|
-
|
|
915
|
+
**Rejected.** Completing ordinary fan-out without waiting and automatically sweeping later for
|
|
916
|
+
late-effective revisions: that would undermine its historical selection boundary and could not withdraw an
|
|
836
917
|
intent a late revision excludes. Pinning eligibility at Host's commit: that is in-transaction
|
|
837
918
|
fan-out, which ADR 0018 rejects. Settling on Host's clock: it is not the clock that stamps
|
|
838
919
|
revisions.
|
|
@@ -959,7 +1040,9 @@ letter, not an endless loop.
|
|
|
959
1040
|
|
|
960
1041
|
### Pause and resume are enforced at dispatch
|
|
961
1042
|
|
|
962
|
-
ADR 0018 decision 3: pause cancels pending intents, and resume does not resurrect
|
|
1043
|
+
ADR 0018 decision 3: pause cancels pending intents, and resume does not resurrect those existing
|
|
1044
|
+
intents. Governed replay also preserves existing pair decisions; a new request after resume may
|
|
1045
|
+
decide previously undecided pairs, while pre-pause requests retain their original active interval. The
|
|
963
1046
|
property the package holds is stronger than "the pause's cleanup ran": once a pause or delete
|
|
964
1047
|
revision is committed, no intent bound to an eligibility revision before it is handed to the
|
|
965
1048
|
transport after a dispatch check has seen that revision. That holds whether the action's
|
|
@@ -1309,6 +1392,7 @@ The namespace defaults to `webhook-delivery` (option `namespace`). Every action
|
|
|
1309
1392
|
| `rotate_secret_emergency` | `subscriptionId`, `secretRef`, `expectedVersion?` | `replaceDeliveryConfiguration` | `WebhookSecretRotated` (`mode: "emergency"`, …, `previousSecretRetainUntil: null`) |
|
|
1310
1393
|
| `requeue_delivery_intent` | `deliveryId`, `expectedFence` | `requeueDeliveryIntent` | `WebhookDeliveryIntentRequeued` (`deliveryId`, `subscriptionId`, `eventId`, `priorFence`, `priorAttemptCount`, `processingDeadline`) |
|
|
1311
1394
|
| `requeue_fan_out_job` | `eventId`, `expectedFence` | `requeueFanOutJob` | `WebhookFanOutJobRequeued` (`eventId`, `priorFence`, `priorAttemptCount`, `processingDeadline`) |
|
|
1395
|
+
| `replay_events` | `subscriptionId`, `from`, `to`, optional `eventTypePatterns` | opt-in `createReplayRequest` | `WebhookReplayRequested` (range, count, acceptance and deadline metadata) |
|
|
1312
1396
|
| `enrich_fan_out_job_actor` | `eventId`, `expectedFence` | `enrichFanOutJobActor` | `WebhookFanOutJobActorEnriched` (`eventId`, `priorFence`, `priorAttemptCount`, `processingDeadline`) |
|
|
1313
1397
|
|
|
1314
1398
|
Events carry ids, numbers, patterns and timestamps only: no destination URL, no secret reference,
|
|
@@ -1692,3 +1776,88 @@ package's tests with `node:crypto` only. They are not part of the packed package
|
|
|
1692
1776
|
Ports are passed to an instance, not registered in module scope. The Boundary Guard's module-state
|
|
1693
1777
|
scan (G5) rejects module-scope `let` bindings, `Map` and `Set` registries and import-time
|
|
1694
1778
|
environment reads in this package (`boundary/tiers.json`, `moduleStateScan`).
|
|
1779
|
+
|
|
1780
|
+
|
|
1781
|
+
## Historical replay
|
|
1782
|
+
|
|
1783
|
+
Enable replay explicitly at the composition root:
|
|
1784
|
+
|
|
1785
|
+
```ts
|
|
1786
|
+
const module = createWebhookDeliveryModule({
|
|
1787
|
+
...operatorOptions,
|
|
1788
|
+
replay: { store: deliveries, subscriptions, maxEvents: 10_000, processingLifetimeMs: 3_600_000 },
|
|
1789
|
+
});
|
|
1790
|
+
```
|
|
1791
|
+
|
|
1792
|
+
The built-in memory and PostgreSQL stores implement `WebhookReplayStore`; both subscription
|
|
1793
|
+
stores implement `WebhookReplaySubscriptionReader.readReplaySubscription`. These are separate
|
|
1794
|
+
capabilities: existing custom base ports and ordinary fan-out method checks are unchanged. Missing
|
|
1795
|
+
enabled methods fail module construction. Without configuration, `replay_events` refuses
|
|
1796
|
+
`replay_not_supported` before asking the operator policy. The authorization input optionally includes
|
|
1797
|
+
normalized `replay` range/filter metadata.
|
|
1798
|
+
|
|
1799
|
+
Submit `<namespace>.replay_events` through Platform Host with `subscriptionId`, canonical ISO
|
|
1800
|
+
`from` (inclusive), `to` (exclusive), and optional `eventTypePatterns`. The range must be nonempty,
|
|
1801
|
+
inside the store's authoritative horizon and end no later than its clock. Patterns use the existing
|
|
1802
|
+
exact, prefix and wildcard grammar. Both the requested filter and acceptance-time subscription
|
|
1803
|
+
patterns must match each candidate; package-owned events are excluded. Scope and requester come
|
|
1804
|
+
from trusted Host context. The operator policy still requires literal `true`.
|
|
1805
|
+
|
|
1806
|
+
Default `maxEvents` is 10,000; default `processingLifetimeMs` is one hour. These composition settings
|
|
1807
|
+
must be positive safe integers. Requests over the cap are refused with `replay_limit_exceeded`,
|
|
1808
|
+
without a receipt or partial work. Many events can share one timestamp and type, so narrowing the
|
|
1809
|
+
range need not make it fit. No pagination across changing source membership is implied.
|
|
1810
|
+
|
|
1811
|
+
Deploy PostgreSQL migration 5 with `ensureSchema()` before enabling replay. Upgrade relevant
|
|
1812
|
+
writers, erasure handlers and purgers before enablement. Migration locking alone does not establish
|
|
1813
|
+
mixed-version behavioral compatibility. The migration adds receipt/item tables and source-range,
|
|
1814
|
+
claim and listing indexes; index creation can block writes while it runs.
|
|
1815
|
+
|
|
1816
|
+
Acceptance atomically fixes membership from **retained webhook projections**, not the canonical
|
|
1817
|
+
ledger. Events never published/bridged or already purged are absent. Payload and facts are not copied
|
|
1818
|
+
to replay records. Each item carries its acceptance-time `observedStatusSinceRevision`, normalized
|
|
1819
|
+
request `eventTypePatterns` (default `["*"]`) and `observedEventTypePatterns`. These pattern lists are
|
|
1820
|
+
bounded by the existing 64-pattern limit. The worker reads and validates only that item context,
|
|
1821
|
+
without reading, copying or scanning the receipt's `selected` membership per item. A receipt exists
|
|
1822
|
+
even for zero matches. Reusing the trusted `actionInvocationId`
|
|
1823
|
+
returns the original receipt before mutable checks; conflicting action parameters refuse
|
|
1824
|
+
`replay_request_conflict`. A later publication cannot expand that receipt. `WebhookReplayRequested`
|
|
1825
|
+
means durable acceptance, not delivery. The webhook transaction and Host event append are separate;
|
|
1826
|
+
a lost response or Host append failure can leave accepted work without that event. As with requeue,
|
|
1827
|
+
the webhook store cannot fence overlapping Host attempts using Host's lease.
|
|
1828
|
+
|
|
1829
|
+
Run `runWebhookReplayCycle()` with the replay capability, current subscription reader, ordinary
|
|
1830
|
+
delivery store, admission, payload policy, clock and worker identity. Worker options shared with
|
|
1831
|
+
fan-out use the same resolver: default batch size 25, lease 60,000 ms, and `dlv_` plus UUID delivery
|
|
1832
|
+
IDs. A custom `retryDelay` is honored; otherwise pass `random` for the configured schedule's jitter.
|
|
1833
|
+
The delivery-ID callback receives a frozen argument without a receiver, and must return a string.
|
|
1834
|
+
`eligibilitySettleMs` is omitted: replay uses fixed membership and current eligibility rather than
|
|
1835
|
+
waiting for the historical fan-out selection boundary. It shares actor-requirement,
|
|
1836
|
+
matching, admission normalization and rendering behavior with ordinary fan-out. Existing pair
|
|
1837
|
+
markers bypass admission; replay never creates a second intent for an existing pair and never
|
|
1838
|
+
overrides a denial. Dispatch remains at-least-once using the normal delivery worker, current
|
|
1839
|
+
destination/configuration checks, authorization, signing and attempt receipts.
|
|
1840
|
+
|
|
1841
|
+
The worker enforces the acceptance-time active interval. Pause/resume ends that interval; deletion,
|
|
1842
|
+
inactivity or narrowed patterns skip remaining items without permanently denying the pair. Pattern
|
|
1843
|
+
broadening does not expand membership. Subscription changes after the worker's last read can race
|
|
1844
|
+
insertion because subscription and delivery stores can be independent. Dispatch retains its existing
|
|
1845
|
+
pause/resume checks; it does not recheck event patterns.
|
|
1846
|
+
|
|
1847
|
+
Admission uses the original event actor, not the requesting operator. Suppressed/legacy absence
|
|
1848
|
+
remains unavailable when identity is required. In particular, completed legacy source jobs cannot
|
|
1849
|
+
be enriched by the existing action. Replay reapplies current payload policy and scope predicates to
|
|
1850
|
+
retained projected data; it cannot restore discarded fields or promise chronological HTTP delivery.
|
|
1851
|
+
|
|
1852
|
+
Settlement checks replay ownership, deadline, source scope/fence, horizon, erasure marker and pair
|
|
1853
|
+
uniqueness atomically. A changed source requires fresh evaluation. An erased undecided pair gets
|
|
1854
|
+
`event_erased`. Original event `recordedAt` anchors the horizon; admission near its edge is best
|
|
1855
|
+
effort. Purge can remove a terminal source while replay remains pending: expiry is reported from
|
|
1856
|
+
the stored anchor; a missing source inside the horizon is visibly dead-lettered as `source_missing`.
|
|
1857
|
+
Intent delivery receives its ordinary finite deadline. Reclaims do not extend replay's persisted
|
|
1858
|
+
deadline. Unknown settlement writes end the current pass without a contradictory follow-up write.
|
|
1859
|
+
|
|
1860
|
+
Inspect `readReplayRequest()` and scoped, event-ID-paged `listReplayItems()`. Each terminal item
|
|
1861
|
+
keeps its outcome once: intent plus delivery ID, denial plus code, already decided, skip plus reason,
|
|
1862
|
+
horizon expiry, or dead letter plus diagnostic. Evaluated revision is retained for pair decisions.
|
|
1863
|
+
Receipts and items are never removed by package purge; operators own their eventual audit retention.
|