@fabricorg/webhook-delivery 0.1.3 → 0.2.1
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 +35 -0
- package/README.md +174 -11
- package/dist/index.cjs +8545 -7218
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +410 -153
- package/dist/index.d.ts +410 -153
- package/dist/index.js +8544 -7219
- package/dist/index.js.map +1 -1
- package/dist/node.cjs.map +1 -1
- package/dist/node.js.map +1 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
# @fabricorg/webhook-delivery
|
|
2
2
|
|
|
3
|
+
## 0.2.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 71d306f: Republish workspace dependents against the action finalization contracts. Finalizing actions require the new Host protocol and atomic transaction capabilities; compatible durable preparation supports checkpoint replay under the Host recovery safeguards.
|
|
8
|
+
|
|
9
|
+
The compiler now flags adding or removing the finalization protocol as a breaking action-contract difference, so compatibility review includes the new lifecycle requirement.
|
|
10
|
+
|
|
11
|
+
- f32aae6: Republish Host dependents for durable finalization checkpoint recovery. Checkpoint writers require migration 10; unknown adapter outcomes and incompatible preparation retain effect-aware refusal.
|
|
12
|
+
- 3d24330: Permit bounded leased retries of non-idempotent actions after retryable pre-effect governance errors. The additive deferredBeforeEffectAttempt record field proves only the immediately preceding claim deferred before handler or adapter effects. Missing or stale proof retains interruption failure. Marker-write failure retains terminal failure where persistence succeeds; lease loss preserves fencing errors. Approval resume, inline execution, exhausted budgets, and errors after the governance window retain their existing refusal semantics. Workspace dependents receive compatible patch releases.
|
|
13
|
+
|
|
14
|
+
PostgreSQL migration 9 (`pre_effect_governance_deferral`) adds the nullable marker column. Run `ensureSchema()` before deploying Hosts that may defer non-idempotent actions. Invocation updates that do not carry the marker omit its column, preserving writes on schemas without migration 9.
|
|
15
|
+
|
|
16
|
+
## 0.2.0
|
|
17
|
+
|
|
18
|
+
### Minor Changes
|
|
19
|
+
|
|
20
|
+
- 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.
|
|
21
|
+
|
|
22
|
+
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.
|
|
23
|
+
|
|
24
|
+
- 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.
|
|
25
|
+
|
|
26
|
+
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.
|
|
27
|
+
|
|
28
|
+
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.
|
|
29
|
+
|
|
30
|
+
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.
|
|
31
|
+
|
|
32
|
+
### Patch Changes
|
|
33
|
+
|
|
34
|
+
- d69dd7a: README: recommend Node 22.23.4, 24.22.1, or 26.12.1 or newer in the same release line for the
|
|
35
|
+
Brotli decoder fix in CVE-2026-111497, and note that Node 20 is end-of-life with no fixed release.
|
|
36
|
+
The `engines` range is unchanged (`>=20`).
|
|
37
|
+
|
|
3
38
|
## 0.1.3
|
|
4
39
|
|
|
5
40
|
### 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
|
|
@@ -786,8 +858,8 @@ to `dlv_` plus a random UUID.
|
|
|
786
858
|
|
|
787
859
|
**Decision.** A job decides an event's eligibility once, after the event's eligibility has
|
|
788
860
|
settled, and the job is complete when `completeFanOutJob` succeeds. Eligibility is settled when
|
|
789
|
-
the worker's clock is at or past `recordedAt + eligibilitySettleMs`.
|
|
790
|
-
|
|
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.
|
|
791
863
|
|
|
792
864
|
**What it rests on.** Two assumptions about clocks that the package cannot enforce:
|
|
793
865
|
|
|
@@ -811,8 +883,9 @@ backs each:
|
|
|
811
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) |
|
|
812
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 …" |
|
|
813
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 |
|
|
814
|
-
|
|
|
815
|
-
|
|
|
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 |
|
|
816
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: …" |
|
|
817
890
|
| Purged records stay purged | the replay horizon floor (see [Replay horizon](#replay-horizon-r5)) | the store contract's race cases |
|
|
818
891
|
|
|
@@ -828,7 +901,9 @@ first, so a pattern change neither removes the event from a subscription that ma
|
|
|
828
901
|
to one that did not. A pause written after `recordedAt` stops delivery, not fan-out: the governed
|
|
829
902
|
action's `cancelPendingIntents` and the delivery cycle's pre-handoff `readSubscription` check
|
|
830
903
|
([Delivery](#delivery)) act on the intent. A pause effective before `recordedAt` makes the revision paused, so the
|
|
831
|
-
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.
|
|
832
907
|
|
|
833
908
|
**Bridge overlap.** Covered above: the first insert of an `eventId` wins and every later one is a
|
|
834
909
|
no-op, before and after the job completes.
|
|
@@ -837,8 +912,8 @@ no-op, before and after the job completes.
|
|
|
837
912
|
dead-lettered with a diagnostic code: a visible failure that the fan-out-job requeue (PR 9)
|
|
838
913
|
recovers. That is R1's "visible recoverable failure".
|
|
839
914
|
|
|
840
|
-
**Rejected.** Completing without waiting and sweeping later for
|
|
841
|
-
|
|
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
|
|
842
917
|
intent a late revision excludes. Pinning eligibility at Host's commit: that is in-transaction
|
|
843
918
|
fan-out, which ADR 0018 rejects. Settling on Host's clock: it is not the clock that stamps
|
|
844
919
|
revisions.
|
|
@@ -965,7 +1040,9 @@ letter, not an endless loop.
|
|
|
965
1040
|
|
|
966
1041
|
### Pause and resume are enforced at dispatch
|
|
967
1042
|
|
|
968
|
-
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
|
|
969
1046
|
property the package holds is stronger than "the pause's cleanup ran": once a pause or delete
|
|
970
1047
|
revision is committed, no intent bound to an eligibility revision before it is handed to the
|
|
971
1048
|
transport after a dispatch check has seen that revision. That holds whether the action's
|
|
@@ -1315,6 +1392,7 @@ The namespace defaults to `webhook-delivery` (option `namespace`). Every action
|
|
|
1315
1392
|
| `rotate_secret_emergency` | `subscriptionId`, `secretRef`, `expectedVersion?` | `replaceDeliveryConfiguration` | `WebhookSecretRotated` (`mode: "emergency"`, …, `previousSecretRetainUntil: null`) |
|
|
1316
1393
|
| `requeue_delivery_intent` | `deliveryId`, `expectedFence` | `requeueDeliveryIntent` | `WebhookDeliveryIntentRequeued` (`deliveryId`, `subscriptionId`, `eventId`, `priorFence`, `priorAttemptCount`, `processingDeadline`) |
|
|
1317
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) |
|
|
1318
1396
|
| `enrich_fan_out_job_actor` | `eventId`, `expectedFence` | `enrichFanOutJobActor` | `WebhookFanOutJobActorEnriched` (`eventId`, `priorFence`, `priorAttemptCount`, `processingDeadline`) |
|
|
1319
1397
|
|
|
1320
1398
|
Events carry ids, numbers, patterns and timestamps only: no destination URL, no secret reference,
|
|
@@ -1698,3 +1776,88 @@ package's tests with `node:crypto` only. They are not part of the packed package
|
|
|
1698
1776
|
Ports are passed to an instance, not registered in module scope. The Boundary Guard's module-state
|
|
1699
1777
|
scan (G5) rejects module-scope `let` bindings, `Map` and `Set` registries and import-time
|
|
1700
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.
|