@nestarc/webhook 0.13.0 → 0.13.2

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.
Files changed (41) hide show
  1. package/CHANGELOG.md +441 -0
  2. package/README.md +384 -367
  3. package/SECURITY.md +27 -0
  4. package/dist/adapters/prisma-delivery.repository.d.ts +1 -1
  5. package/dist/adapters/prisma-delivery.repository.d.ts.map +1 -1
  6. package/dist/adapters/prisma-delivery.repository.js +79 -18
  7. package/dist/adapters/prisma-delivery.repository.js.map +1 -1
  8. package/dist/adapters/prisma-event.repository.d.ts +2 -2
  9. package/dist/adapters/prisma-event.repository.d.ts.map +1 -1
  10. package/dist/adapters/prisma-event.repository.js +6 -6
  11. package/dist/adapters/prisma-event.repository.js.map +1 -1
  12. package/dist/index.d.ts +1 -1
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js.map +1 -1
  15. package/dist/interfaces/webhook-delivery.interface.d.ts +4 -0
  16. package/dist/interfaces/webhook-delivery.interface.d.ts.map +1 -1
  17. package/dist/interfaces/webhook-endpoint.interface.d.ts +2 -2
  18. package/dist/interfaces/webhook-endpoint.interface.d.ts.map +1 -1
  19. package/dist/interfaces/webhook-options.interface.d.ts +2 -1
  20. package/dist/interfaces/webhook-options.interface.d.ts.map +1 -1
  21. package/dist/ports/webhook-delivery.repository.d.ts +9 -1
  22. package/dist/ports/webhook-delivery.repository.d.ts.map +1 -1
  23. package/dist/ports/webhook-event.repository.d.ts +7 -3
  24. package/dist/ports/webhook-event.repository.d.ts.map +1 -1
  25. package/dist/webhook.delivery-admin.service.d.ts +3 -1
  26. package/dist/webhook.delivery-admin.service.d.ts.map +1 -1
  27. package/dist/webhook.delivery-admin.service.js +7 -3
  28. package/dist/webhook.delivery-admin.service.js.map +1 -1
  29. package/dist/webhook.service.d.ts +13 -0
  30. package/dist/webhook.service.d.ts.map +1 -1
  31. package/dist/webhook.service.js +14 -1
  32. package/dist/webhook.service.js.map +1 -1
  33. package/docs/usage.md +109 -0
  34. package/examples/quick-start/README.md +83 -0
  35. package/examples/quick-start/main.ts +247 -0
  36. package/examples/quick-start/package.json +29 -0
  37. package/examples/quick-start/prisma/schema.prisma +10 -0
  38. package/examples/quick-start/prisma.config.ts +6 -0
  39. package/examples/quick-start/tsconfig.json +15 -0
  40. package/llms.txt +12 -0
  41. package/package.json +32 -13
package/CHANGELOG.md ADDED
@@ -0,0 +1,441 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.13.2] - 2026-09-11
11
+
12
+ ### Fixed
13
+
14
+ - Persist `correlationId` without requiring an `idempotencyKey` for all publish methods. Event repository methods accept optional correlation metadata while remaining compatible with existing implementations.
15
+ - Use the module's configured `delivery.maxRetries` total attempt budget for event replay instead of always using five attempts. Custom replay repositories receive the budget as an optional third parameter.
16
+ - Prevent individual and bulk manual retries from requeueing events whose payload has been purged. Serialize retry, replay, and payload retention on event rows so concurrent cleanup cannot erase newly queued payloads.
17
+
18
+ ### Added
19
+
20
+ - An executable NestJS/Prisma 7 consumer example with an isolated PostgreSQL schema, raw-body signature verification, receiver deduplication, and delivery/attempt assertions. CI verifies it from the packed package.
21
+ - A version-aware consumer contract, AI navigation index, and packaged changelog/security/usage/example documentation.
22
+ - Public `WebhookHttpClientRequestOptions` type export for custom HTTP adapters implementing prevalidated address handling.
23
+
24
+ ### Documentation
25
+
26
+ - Correct delivery claims, queue-time secret rotation snapshots, response body limits, transport details, tenant selection, and timestamp freshness versus deduplication.
27
+ - Complete CommonJS Prisma 7 setup and ordered upgrade migrations; distinguish latest delivery state from per-attempt history and total attempts from retries.
28
+ - Clarify that the built-in Prisma adapter does not persist the optional administrative `reason` field, and retain it for custom adapters.
29
+ - Link the official usage/API references, improve package discovery metadata, and mark historical design records as non-authoritative.
30
+
31
+ ## [0.13.1] - 2026-08-30
32
+
33
+ ### Added
34
+
35
+ - Prisma 7 support for the default PostgreSQL repositories, including an exact
36
+ Prisma 7.10.0 + NestJS 11.2.1 real-database CI lane.
37
+ - A packed-artifact consumer gate that installs `@nestarc/webhook` with exact
38
+ NestJS 11.2.1 and Prisma 7.10.0 under `--strict-peer-deps`, then verifies
39
+ artifact integrity/provenance, public types, and CommonJS runtime loading.
40
+ - A Prisma 6.19.3 legacy schema and real-database CI lane so the existing
41
+ NestJS 10/11 + Prisma 6 support remains independently verified with the
42
+ classic `prisma-client-js` generator and native `new PrismaClient()` runtime.
43
+
44
+ ### Changed
45
+
46
+ - Expanded the `@prisma/client` peer range to include `^7.0.0`.
47
+ - Migrated the Prisma 7 E2E harness to the `prisma-client` generator, explicit
48
+ output, CommonJS module format, `prisma.config.ts`, and `@prisma/adapter-pg`.
49
+ - Pinned the modern development evidence tuple to NestJS 11.2.1 and Prisma
50
+ 7.10.0; CI continues to install exact legacy versions in isolated cells.
51
+
52
+ ### Fixed
53
+
54
+ - Explicitly cast retention cutoff parameters to `timestamptz`. Prisma 7's
55
+ PostgreSQL driver adapter otherwise allowed PostgreSQL to infer the date
56
+ parameter as an interval, causing retention purge queries to fail with
57
+ error `42883`.
58
+
59
+ ## [0.13.0] - 2026-06-23
60
+
61
+ ### Added
62
+
63
+ - Producer-side idempotent publish via `WebhookPublishOptions.idempotencyKey` and `WebhookEventRepository.saveEventOnceInTransaction()`.
64
+ - `WebhookDeliveryAdminService.retryFailedDeliveries()` for bounded bulk retry of failed deliveries.
65
+ - `WebhookDeliveryAdminService.replayEvent()` for replaying an existing event to currently active matching endpoints.
66
+ - `WebhookRetentionAdminService.purgeExpiredData()` for application-scheduled retention cleanup.
67
+ - Retention options for event payloads, delivery response bodies, and attempt response bodies.
68
+ - Redaction hooks for payload minimization and response body suppression before persistence.
69
+ - `WebhookSigner.verifyWithTolerance()` and `WebhookVerificationOptions` for timestamp freshness checks.
70
+ - v0.13.0 SQL migration for idempotency keys, correlation IDs, and payload purge metadata.
71
+
72
+ ### Changed
73
+
74
+ - `WebhookDeliveryRepository.retryDelivery()` now accepts optional retry metadata and grants a failed delivery at least one additional manual attempt.
75
+ - Default Prisma repositories now treat `tenant_id` as an opaque string on insert, matching the SQL schema and README examples.
76
+
77
+ ### Migration
78
+
79
+ Existing databases should run:
80
+
81
+ ```bash
82
+ psql -d your_database -f node_modules/@nestarc/webhook/src/sql/migrations/v0.13.0.sql
83
+ ```
84
+
85
+ The migration adds `webhook_events.idempotency_key`, `webhook_events.correlation_id`, `webhook_events.payload_purged_at`, and `webhook_events_idempotency_key_idx`.
86
+
87
+ ## [0.12.1] - 2026-05-03
88
+
89
+ ### Fixed
90
+
91
+ - Successful delivery circuit-breaker resets now avoid rewriting already-healthy endpoint rows, reducing `webhook_endpoints` row-lock contention during high-throughput worker scale-out.
92
+
93
+ ## [0.12.0] - 2026-05-03
94
+
95
+ ### Added
96
+
97
+ - Worker capacity controls: `polling.maxConcurrency`, `polling.drainWhileBacklogged`, `polling.maxDrainLoopsPerPoll`, and `polling.drainLoopDelayMs`.
98
+ - `workerObserver` with poll lifecycle and delivery processing metrics callbacks.
99
+ - `WebhookDeliveryRepository.getBacklogSummary()` and `DeliveryBacklogSummary` for delivery backlog diagnostics.
100
+ - Partial PostgreSQL indexes for runnable `PENDING` deliveries and stale `SENDING` recovery scans.
101
+
102
+ ### Changed
103
+
104
+ - `WebhookDeliveryWorker` now separates database claim size from in-flight dispatch concurrency while preserving previous defaults.
105
+
106
+ ## [0.11.0] - 2026-05-02
107
+
108
+ ### Added
109
+
110
+ - `onDeliveryRetryScheduled` callback option and `DeliveryRetryScheduledContext` type for internal observability after a retriable failed attempt is persisted with its next attempt time.
111
+ - `circuitBreaker.degradedThreshold`, `onEndpointDegraded`, and `EndpointDegradedContext` for observing active endpoint degradation before circuit-breaker disablement.
112
+
113
+ ### Changed
114
+
115
+ - Webhook deliveries now treat permanent receiver `4xx` responses as terminal failures instead of retrying them through the full backoff budget. `408`, `409`, `425`, and `429` remain retryable.
116
+ - Existing `onDeliveryFailed` and `onEndpointDisabled` semantics are unchanged: delivery failed remains terminal-only, and endpoint disabled remains active-to-inactive only.
117
+
118
+ ### Fixed
119
+
120
+ - Dispatcher exceptions such as URL validation and URL parse failures now update circuit-breaker failure accounting after the failed attempt is persisted.
121
+ - `WebhookDeliveryWorker` now isolates synchronous `onDeliveryFailed` callback errors as well as rejected callback promises, so notification failures cannot re-enter delivery retry handling.
122
+ - `PrismaDeliveryRepository.markFailed()` now clears `next_attempt_at` when a delivery reaches terminal `FAILED` state, keeping delivery logs from exposing stale retry schedules.
123
+
124
+ ## [0.10.0] - 2026-04-30
125
+
126
+ ### Added
127
+
128
+ - `WebhookEndpointAdminService.rotateSecret(endpointId, dto)` and `WebhookAdminService.rotateSecret(endpointId, dto)` now rotate endpoint signing secrets through the public admin API. The Prisma adapter moves the currently stored secret into `previous_secret`, encrypts the new secret through the configured `WebhookSecretVault`, and returns the new secret only once for receiver provisioning.
129
+
130
+ ### Changed
131
+
132
+ - `DEFAULT_USER_AGENT` now includes the package version (`@nestarc/webhook/<version>`) for receiver-side debugging.
133
+ - `WebhookEndpointRepository.disableEndpoint()` now returns `true` only when the endpoint actually transitions from active to inactive. Circuit-breaker notifications use this transition result instead of the raw failure count, so a failed disable attempt can still notify on a later successful disable.
134
+ - `WebhookCircuitBreaker.afterDelivery()` now requires endpoint metadata (`tenantId`, `url`) so `onEndpointDisabled` receives a real endpoint URL instead of an empty-string fallback.
135
+
136
+ ### Fixed
137
+
138
+ - Added the `WEBHOOK_SECRET_VAULT` injection token and registered/exported the configured vault provider so custom consumers can inject the active `WebhookSecretVault`.
139
+ - `onDeliveryFailed` now classifies exhausted failures without an HTTP status code as `dispatch_error` instead of `http_error`.
140
+ - Dispatch-time URL parse failures now throw `WebhookUrlValidationError` with `reason: 'parse'`, and DNS validation errors now include the original delivery URL.
141
+ - `WebhookDeliveryWorker` error logs now preserve stack traces, and shutdown waits for an active poll cycle before returning.
142
+ - Successful deliveries no longer reactivate endpoints disabled for non-circuit-breaker reasons. `resetFailures()` only clears disabled state when `disabled_reason = 'consecutive_failures_exceeded'`.
143
+ - Cooldown recovery now only reactivates endpoints disabled by the circuit breaker, preserving endpoints disabled for other reasons.
144
+
145
+ ## [0.9.0] - 2026-04-19
146
+
147
+ ### Added
148
+
149
+ - **Per-attempt audit log (`webhook_delivery_attempts`)** — records one row per delivery attempt with `attempt_number`, `status`, `response_status`, `response_body` (truncated at 4096 JavaScript string code units), `response_body_truncated`, `latency_ms`, `last_error`, and `created_at`. Enforces uniqueness on `(delivery_id, attempt_number)`.
150
+ - **`WebhookAdminService.getDeliveryAttempts(deliveryId)`** — returns attempt history ordered by `attempt_number ASC`. The same method is exposed through `WebhookDeliveryAdminService` and the `WebhookDeliveryRepository` port.
151
+ - **`DeliveryAttemptRecord` type export** — importable from the package root.
152
+ - **Endpoint snapshotting on delivery creation** — adds `endpoint_url_snapshot`, `signing_secret_snapshot`, and `secondary_signing_secret_snapshot` to `webhook_deliveries`. New deliveries persist the endpoint URL and signing secrets used at enqueue time, so retries continue using the original settings even if the endpoint is edited or secrets are rotated later.
153
+ - **Secret rotation overlap** — adds `webhook_endpoints.previous_secret` and `previous_secret_expires_at`. Until expiry, deliveries are signed with both current and previous secrets, and receivers may accept either signature.
154
+ - **`WebhookSigner.signAll(eventId, timestamp, body, secrets[])`** — creates space-separated multi-signature `v1,...` headers according to Standard Webhooks. The existing `sign()` method delegates to `signAll([secret])`.
155
+ - **`DeliveryRecord.destinationUrl`** — exposes the snapshotted destination URL in delivery log queries.
156
+
157
+ ### Changed
158
+
159
+ - `WebhookSigner.verify()` now accepts a `webhook-signature` header when **any one** of its signatures matches, using `timingSafeEqual`. Single-signature requests keep the previous behavior.
160
+ - `PrismaDeliveryRepository` pending-delivery queries now return an `additionalSecrets` array, and `WebhookDispatcher` passes it to `signAll` to generate multi-signature headers.
161
+
162
+ ### Migration
163
+
164
+ Existing databases need the following additive schema changes:
165
+
166
+ ```sql
167
+ ALTER TABLE webhook_endpoints
168
+ ADD COLUMN previous_secret TEXT,
169
+ ADD COLUMN previous_secret_expires_at TIMESTAMPTZ;
170
+
171
+ ALTER TABLE webhook_deliveries
172
+ ADD COLUMN endpoint_url_snapshot TEXT,
173
+ ADD COLUMN signing_secret_snapshot TEXT,
174
+ ADD COLUMN secondary_signing_secret_snapshot TEXT;
175
+
176
+ CREATE TABLE IF NOT EXISTS webhook_delivery_attempts (
177
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
178
+ delivery_id UUID NOT NULL REFERENCES webhook_deliveries(id) ON DELETE CASCADE,
179
+ attempt_number INT NOT NULL,
180
+ status VARCHAR(20) NOT NULL
181
+ CHECK (status IN ('PENDING', 'SENDING', 'SENT', 'FAILED')),
182
+ response_status INT,
183
+ response_body TEXT,
184
+ response_body_truncated BOOLEAN NOT NULL DEFAULT FALSE,
185
+ latency_ms INT,
186
+ last_error TEXT,
187
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
188
+ CONSTRAINT webhook_delivery_attempts_delivery_id_attempt_number_key
189
+ UNIQUE (delivery_id, attempt_number)
190
+ );
191
+
192
+ CREATE INDEX IF NOT EXISTS idx_delivery_attempts_delivery_created
193
+ ON webhook_delivery_attempts (delivery_id, created_at);
194
+ ```
195
+
196
+ Existing delivery rows keep `NULL` snapshot columns. Repositories fall back to live endpoint values with `COALESCE`; snapshots are populated for new deliveries only.
197
+
198
+ Secret rotation example:
199
+
200
+ ```sql
201
+ UPDATE webhook_endpoints
202
+ SET secret = :new_secret,
203
+ previous_secret = :old_secret,
204
+ previous_secret_expires_at = NOW() + interval '24 hours'
205
+ WHERE id = :endpoint_id;
206
+ ```
207
+
208
+ ## [0.8.0] - 2026-04-14
209
+
210
+ ### Added
211
+
212
+ - **`failureKind` in `DeliveryFailedContext`** — high-level classification of the final failure: `'url_validation' | 'dispatch_error' | 'http_error'`. Consumers can branch without parsing `lastError` strings.
213
+ - **URL validation metadata in `DeliveryFailedContext`** — when `failureKind === 'url_validation'`, the context also carries `validationReason`, `validationUrl`, and `resolvedIp` propagated from `WebhookUrlValidationError`. Previously this structured information was lost at the worker boundary.
214
+ - **`DeliveryFailureKind` type export** — machine-readable union type re-exported from the package root.
215
+
216
+ ### Changed
217
+
218
+ - `DeliveryFailedContext` gained four optional fields; existing consumers are unaffected. Hook signature unchanged.
219
+ - `WebhookDeliveryWorker` now detects `WebhookUrlValidationError` in the exception path and forwards structured metadata to `onDeliveryFailed`.
220
+
221
+ ### Migration
222
+
223
+ Before (string matching):
224
+
225
+ ```ts
226
+ onDeliveryFailed: (ctx) => {
227
+ if (ctx.lastError?.includes('private address')) {
228
+ alert.endpointMisconfigured(ctx);
229
+ }
230
+ }
231
+ ```
232
+
233
+ After (structured branching):
234
+
235
+ ```ts
236
+ onDeliveryFailed: (ctx) => {
237
+ if (ctx.failureKind === 'url_validation') {
238
+ alert.endpointMisconfigured({
239
+ endpointId: ctx.endpointId,
240
+ reason: ctx.validationReason,
241
+ resolvedIp: ctx.resolvedIp,
242
+ });
243
+ } else if (ctx.failureKind === 'http_error') {
244
+ alert.downstreamUnhealthy(ctx);
245
+ }
246
+ }
247
+ ```
248
+
249
+ ## [0.7.0] - 2026-04-14
250
+
251
+ ### Added
252
+
253
+ - **`WebhookUrlValidationError` class** — URL validation failures now throw a dedicated error class instead of a plain `Error`. Consumers can branch with `instanceof WebhookUrlValidationError` instead of matching message strings.
254
+ - **`reason` field (`WebhookUrlValidationReason`)** — exposes the validation failure cause as a structured value: `'parse' | 'scheme' | 'blocked_hostname' | 'loopback' | 'private' | 'link_local' | 'invalid_target'`.
255
+ - **`url` / `resolvedIp` fields** — include the failed input URL and DNS-resolved IP, when applicable, on the error object. This supports structured 400 responses such as `{ message, reason, resolvedIp }`.
256
+ - `resolveAndValidateHost(hostname, url?)` — adds a backward-compatible optional `url` parameter used to populate the error object's `url` field.
257
+
258
+ ### Changed
259
+
260
+ - Replaced internal `throw new Error(...)` calls in `validateWebhookUrl` / `resolveAndValidateHost`. **Message formats are unchanged**, so existing consumers using patterns such as `err.message.includes('private address')` are unaffected.
261
+
262
+ ### Migration
263
+
264
+ Before:
265
+
266
+ ```ts
267
+ } catch (err) {
268
+ if (err instanceof Error && err.message.toLowerCase().includes('invalid webhook url')) {
269
+ throw new BadRequestException(err.message);
270
+ }
271
+ throw err;
272
+ }
273
+ ```
274
+
275
+ After:
276
+
277
+ ```ts
278
+ import { WebhookUrlValidationError } from '@nestarc/webhook';
279
+
280
+ } catch (err) {
281
+ if (err instanceof WebhookUrlValidationError) {
282
+ throw new BadRequestException({ message: err.message, reason: err.reason });
283
+ }
284
+ throw err;
285
+ }
286
+ ```
287
+
288
+ ## [0.6.1] - 2026-04-12
289
+
290
+ ### Fixed
291
+
292
+ - **`onEndpointDisabled` duplicate firing** — hook now fires only at exact threshold crossing (`===`) instead of on every failure above threshold (`>=`). Prevents duplicate alerts in multi-instance environments where concurrent failures exceed the threshold.
293
+ - **`consecutiveFailures` payload** — reports the actual failure count from `incrementFailures()` instead of the static threshold value.
294
+ - **Blocking hooks** — `onDeliveryFailed` and `onEndpointDisabled` are now truly fire-and-forget (`void Promise.resolve().catch()`). Slow callbacks no longer block delivery processing or shutdown.
295
+ - **`tenantId` type** — changed from `string` to `string | null` in both `DeliveryFailedContext` and `EndpointDisabledContext`. Consumers can now distinguish global endpoints (`null`) from tenant-scoped endpoints.
296
+ - **`package-lock.json` version** — synced to match `package.json`.
297
+
298
+ ## [0.6.0] - 2026-04-12
299
+
300
+ ### Added
301
+
302
+ - **`onDeliveryFailed` callback** — called when a delivery exhausts all retry attempts. Receives `DeliveryFailedContext` with delivery ID, endpoint ID, event ID, tenant ID, attempts, last error, and response status. Fire-and-forget — errors are logged, not propagated.
303
+ - **`onEndpointDisabled` callback** — called when the circuit breaker disables an endpoint. Receives `EndpointDisabledContext` with endpoint ID, tenant ID, URL, reason, and failure count. Fire-and-forget — errors are logged, not propagated.
304
+ - **`DeliveryFailedContext` type** — context object passed to the `onDeliveryFailed` callback.
305
+ - **`EndpointDisabledContext` type** — context object passed to the `onEndpointDisabled` callback.
306
+ - **`tenant_id` in `PendingDelivery`** — enrichment query now includes the endpoint's `tenant_id`, enabling tenant-aware notification hooks.
307
+
308
+ ### Changed
309
+
310
+ - `WebhookCircuitBreaker.afterDelivery()` accepts an optional `meta` parameter (`{ tenantId, url }`) to pass endpoint context without extra DB queries.
311
+
312
+ ## [0.5.0] - 2026-04-12
313
+
314
+ ### Added
315
+
316
+ - **`polling.enabled` option** — set to `false` to disable the delivery polling loop. This allows running the webhook module in API-only mode, where a separate worker process handles delivery. Default: `true` (backward compatible).
317
+
318
+ ### How to use
319
+
320
+ Run the webhook module in two separate NestJS processes sharing the same PostgreSQL database:
321
+
322
+ - **API process:** `polling: { enabled: false }` — publishes events only.
323
+ - **Worker process:** `polling: { enabled: true }` — delivers webhooks only (use `NestFactory.createApplicationContext` for HTTP-serverless operation).
324
+
325
+ Workers scale horizontally thanks to `FOR UPDATE SKIP LOCKED`. No Redis or message queue required.
326
+
327
+ ## [0.4.1] - 2026-04-11
328
+
329
+ ### Fixed
330
+
331
+ - **UUID tenant inserts** — INSERT queries now cast `tenant_id::uuid`, allowing `tenant_id` columns of UUID type (e.g. FK to `applications.id`).
332
+
333
+ ## [0.4.0] - 2026-04-11
334
+
335
+ ### Added
336
+
337
+ - **`WebhookSecretVault` port** — new port interface for encrypting/decrypting endpoint signing secrets at rest. Implement this to provide custom encryption (e.g. AES-256-GCM).
338
+ - **`PlaintextSecretVault` adapter** — default no-op vault that passes secrets through unchanged. Maintains backward compatibility when no vault is configured.
339
+ - **`secretVault` module option** — `WebhookModuleOptions` accepts an optional `secretVault` to replace the default plaintext vault.
340
+ - **`status` CHECK constraint** — `webhook_deliveries.status` column now includes a CHECK constraint limiting values to `PENDING`, `SENDING`, `SENT`, `FAILED` in the official schema.
341
+ - **`tenant_id::text` cast** — SELECT queries now cast `tenant_id::text` for comparison, enabling future UUID FK migration without breaking existing text-based tenant IDs.
342
+
343
+ ### Changed
344
+
345
+ - `PrismaEndpointRepository` constructor accepts an optional `WebhookSecretVault` parameter; `createEndpoint()` encrypts the secret before storage.
346
+ - `PrismaDeliveryRepository` constructor accepts an optional `WebhookSecretVault` parameter; `enrichDeliveries()` decrypts secrets after retrieval.
347
+
348
+ ## [0.3.0] - 2026-04-11
349
+
350
+ ### Added
351
+
352
+ - **`sendToEndpoints(endpointIds, event)`** — send events to specific endpoint IDs instead of fan-out to all matching endpoints. Useful for SaaS platforms where API consumers specify which endpoints should receive a particular event.
353
+
354
+ ## [0.2.0] - 2026-04-11
355
+
356
+ ### Added
357
+
358
+ - **Ports/adapters architecture** — all services depend on port interfaces (`WebhookEventRepository`, `WebhookEndpointRepository`, `WebhookDeliveryRepository`, `WebhookHttpClient`) instead of Prisma directly. Default Prisma and fetch adapters are provided.
359
+ - **Custom adapter injection** — `WebhookModuleOptions` accepts `eventRepository`, `endpointRepository`, `deliveryRepository`, `httpClient` to replace defaults.
360
+ - **`WebhookEndpointAdminService`** — endpoint CRUD + test events (split from `WebhookAdminService`).
361
+ - **`WebhookDeliveryAdminService`** — delivery logs + manual retry (split from `WebhookAdminService`).
362
+ - **`WebhookDispatcher`** — signing + HTTP dispatch extracted from delivery worker.
363
+ - **`WebhookRetryPolicy`** — backoff calculation extracted from delivery worker.
364
+ - **Dispatch-time DNS validation** — `resolveAndValidateHost()` prevents DNS rebinding SSRF by validating resolved IPs before every POST, not only at registration.
365
+ - **IPv4-mapped IPv6 detection** — blocks `::ffff:10.0.0.1` style bypass in both literal and hex-normalized forms.
366
+ - **Async DNS resolution at registration** — hostnames like `*.nip.io` resolving to private IPs are rejected.
367
+ - **HTTP redirect blocking** — `FetchHttpClient` uses `redirect: 'manual'` to prevent SSRF via 3xx.
368
+ - **`allowPrivateUrls` option** — permits private/internal URLs for development and testing environments.
369
+ - **Stale SENDING lease recovery** — `claimed_at` column tracks when a delivery was claimed; stale recovery uses lease expiry instead of `next_attempt_at`.
370
+ - **`polling.staleSendingMinutes` option** — configures the stale delivery reaper threshold (default: 5 minutes).
371
+ - **Bounded exception retries** — dispatch/persistence exceptions increment `attempts` and apply backoff instead of blindly resetting to PENDING.
372
+ - **Post-persist state isolation** — circuit breaker failures after `markSent`/`markFailed` no longer revert delivery state.
373
+ - **`WebhookEvent` LSP guard** — throws immediately if a subclass omits `static readonly eventType`.
374
+ - **Secret exposure prevention** — `EndpointRecord` excludes `secret`; only `createEndpoint` returns `EndpointRecordWithSecret`.
375
+ - **`pgcrypto` extension** — migration SQL includes `CREATE EXTENSION IF NOT EXISTS pgcrypto` for PostgreSQL < 13.
376
+ - **CI/CD** — GitHub Actions CI (lint → test matrix → pack) and Release (verify → build → npm publish with OIDC provenance).
377
+ - **`EndpointRecordWithSecret` type** — typed internal record for contexts that need the signing secret.
378
+ - **`resolveAndValidateHost` export** — reusable DNS validation function.
379
+
380
+ ### Changed
381
+
382
+ - **BREAKING:** `WebhookAdminService` is deprecated. Use `WebhookEndpointAdminService` and `WebhookDeliveryAdminService` instead. The facade remains available for 0.x compatibility and will be removed in v1.0.0.
383
+ - **BREAKING:** `EndpointRecord` no longer includes `secret`. Use `EndpointRecordWithSecret` for creation responses.
384
+ - **BREAKING:** `WebhookModuleOptions.prisma` is now optional (not needed if all custom repositories are provided).
385
+ - `WebhookDeliveryWorker` reduced from 280 lines / 7 responsibilities to a thin orchestrator.
386
+ - `WebhookCircuitBreaker` depends on `WebhookEndpointRepository` port instead of Prisma directly.
387
+ - `WebhookService` depends on three repository ports instead of raw Prisma.
388
+ - All `SELECT *` / `RETURNING *` queries replaced with explicit column aliases for correct camelCase mapping.
389
+ - `validateWebhookUrl` is now async (performs DNS resolution).
390
+
391
+ ### Removed
392
+
393
+ - `resetToPending()` from `WebhookDeliveryRepository` — replaced by bounded retry accounting in catch paths.
394
+ - `SigningOptions` interface and `signing` config field — HMAC-SHA256 with Standard Webhooks headers is fixed.
395
+
396
+ ### Fixed
397
+
398
+ - Endpoint records returned snake_case fields (`tenant_id`, `consecutive_failures`) instead of camelCase (`tenantId`, `consecutiveFailures`).
399
+ - Delivery log records returned snake_case fields (`event_id`, `endpoint_id`, `max_attempts`) instead of camelCase.
400
+ - Circuit breaker recovery only ran when pending deliveries existed — now runs every poll cycle.
401
+ - Poll cycles could overlap via `setInterval` — `isPolling` guard prevents concurrent execution.
402
+ - Event save and delivery creation were not atomic — wrapped in `$transaction()`.
403
+ - Exception path reset deliveries to PENDING without incrementing attempts — enabled unbounded retry loops.
404
+ - `markSent()` success followed by `afterDelivery()` failure reverted delivery to PENDING — caused duplicate sends.
405
+
406
+ ## [0.1.0] - 2026-04-11
407
+
408
+ ### Added
409
+
410
+ - `WebhookModule` with `forRoot()` and `forRootAsync()` registration.
411
+ - `WebhookEvent` abstract base class with `static eventType` and `toPayload()`.
412
+ - `WebhookService` with `send()` and `sendToTenant()` for event fan-out.
413
+ - `WebhookDeliveryWorker` with polling-based async delivery.
414
+ - HMAC-SHA256 signing compatible with Standard Webhooks headers.
415
+ - Exponential backoff retry (30s → 5m → 30m → 2h → 24h) with jitter.
416
+ - Circuit breaker with auto-disable and cooldown-based recovery.
417
+ - Dead letter queue (FAILED status after max retries).
418
+ - `WebhookAdminService` for endpoint CRUD, delivery logs, manual retry, test events.
419
+ - `FOR UPDATE SKIP LOCKED` for multi-instance safe delivery claiming.
420
+ - Graceful shutdown with active delivery drain.
421
+ - PostgreSQL migration SQL for 3 tables.
422
+ - Base64 secret validation (minimum 16 bytes).
423
+
424
+ [Unreleased]: https://github.com/nestarc/webhook/compare/v0.13.1...HEAD
425
+ [0.13.1]: https://github.com/nestarc/webhook/compare/v0.13.0...v0.13.1
426
+ [0.13.0]: https://github.com/nestarc/webhook/compare/v0.12.1...v0.13.0
427
+ [0.12.1]: https://github.com/nestarc/webhook/compare/v0.12.0...v0.12.1
428
+ [0.12.0]: https://github.com/nestarc/webhook/compare/v0.11.0...v0.12.0
429
+ [0.11.0]: https://github.com/nestarc/webhook/compare/v0.10.0...v0.11.0
430
+ [0.10.0]: https://github.com/nestarc/webhook/compare/v0.9.0...v0.10.0
431
+ [0.9.0]: https://github.com/nestarc/webhook/compare/v0.8.0...v0.9.0
432
+ [0.8.0]: https://github.com/nestarc/webhook/compare/v0.7.0...v0.8.0
433
+ [0.7.0]: https://github.com/nestarc/webhook/compare/v0.6.1...v0.7.0
434
+ [0.6.1]: https://github.com/nestarc/webhook/compare/01b8e737c65e1fb39418e5b388bafa7b6459cead...v0.6.1
435
+ [0.6.0]: https://github.com/nestarc/webhook/compare/v0.5.0...01b8e737c65e1fb39418e5b388bafa7b6459cead
436
+ [0.5.0]: https://github.com/nestarc/webhook/compare/v0.4.1...v0.5.0
437
+ [0.4.1]: https://github.com/nestarc/webhook/compare/v0.4.0...v0.4.1
438
+ [0.4.0]: https://github.com/nestarc/webhook/compare/v0.3.0...v0.4.0
439
+ [0.3.0]: https://github.com/nestarc/webhook/compare/v0.2.0...v0.3.0
440
+ [0.2.0]: https://github.com/nestarc/webhook/compare/91331c91cb1463e8912ef9ed795497a2fa8e4b41...v0.2.0
441
+ [0.1.0]: https://github.com/nestarc/webhook/commit/91331c91cb1463e8912ef9ed795497a2fa8e4b41