@10x-media/webhooks 0.1.0-beta.0 → 0.1.0-beta.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.
Files changed (67) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +21 -149
  3. package/dist/collections/deliveries.js +99 -0
  4. package/dist/collections/deliveries.js.map +1 -0
  5. package/dist/collections/subscriptions.js +123 -0
  6. package/dist/collections/subscriptions.js.map +1 -0
  7. package/dist/constants.js +29 -0
  8. package/dist/constants.js.map +1 -0
  9. package/dist/delivery/DeliveryStatusCell.d.ts +10 -0
  10. package/dist/delivery/DeliveryStatusCell.js +42 -0
  11. package/dist/delivery/DeliveryStatusCell.js.map +1 -0
  12. package/dist/delivery/RedeliverButton.d.ts +6 -0
  13. package/dist/delivery/RedeliverButton.js +43 -0
  14. package/dist/delivery/RedeliverButton.js.map +1 -0
  15. package/dist/delivery/buildPayload.js +26 -0
  16. package/dist/delivery/buildPayload.js.map +1 -0
  17. package/dist/delivery/deliver.js +36 -0
  18. package/dist/delivery/deliver.js.map +1 -0
  19. package/dist/delivery/deliverTask.js +80 -0
  20. package/dist/delivery/deliverTask.js.map +1 -0
  21. package/dist/delivery/deriveDeliveryStatus.js +10 -0
  22. package/dist/delivery/deriveDeliveryStatus.js.map +1 -0
  23. package/dist/delivery/redeliver.js +83 -0
  24. package/dist/delivery/redeliver.js.map +1 -0
  25. package/dist/delivery/sendDelivery.js +32 -0
  26. package/dist/delivery/sendDelivery.js.map +1 -0
  27. package/dist/delivery/sign.js +10 -0
  28. package/dist/delivery/sign.js.map +1 -0
  29. package/dist/events/eventTypes.js +27 -0
  30. package/dist/events/eventTypes.js.map +1 -0
  31. package/dist/events/hooks.js +132 -0
  32. package/dist/events/hooks.js.map +1 -0
  33. package/dist/exports/client.d.ts +3 -14
  34. package/dist/exports/client.js +2 -81
  35. package/dist/exports/i18n.d.ts +3 -37
  36. package/dist/exports/i18n.js +2 -2
  37. package/dist/exports/types.d.ts +1 -1
  38. package/dist/index.d.ts +20 -2
  39. package/dist/index.js +3 -817
  40. package/dist/index.js.map +1 -1
  41. package/dist/{index-CpsTtHO1.d.ts → options.d.ts} +10 -18
  42. package/dist/options.js +15 -0
  43. package/dist/options.js.map +1 -0
  44. package/dist/plugin/registerTranslations.js +19 -0
  45. package/dist/plugin/registerTranslations.js.map +1 -0
  46. package/dist/plugin/registerWebhooks.js +109 -0
  47. package/dist/plugin/registerWebhooks.js.map +1 -0
  48. package/dist/plugin/resolveMode.js +15 -0
  49. package/dist/plugin/resolveMode.js.map +1 -0
  50. package/dist/plugin/resolveSubscriptions.js +53 -0
  51. package/dist/plugin/resolveSubscriptions.js.map +1 -0
  52. package/dist/{translations-CnkwrCJ3.js → translations/en.js} +3 -22
  53. package/dist/translations/en.js.map +1 -0
  54. package/dist/translations/index.d.ts +14 -0
  55. package/dist/translations/index.js +26 -0
  56. package/dist/translations/index.js.map +1 -0
  57. package/dist/translations/keys.d.ts +31 -0
  58. package/dist/{keys-BdNiD3rC.js → translations/keys.js} +2 -2
  59. package/dist/translations/keys.js.map +1 -0
  60. package/dist/translations/server.js +13 -0
  61. package/dist/translations/server.js.map +1 -0
  62. package/dist/translations/useTranslation.js +12 -0
  63. package/dist/translations/useTranslation.js.map +1 -0
  64. package/package.json +4 -3
  65. package/dist/exports/client.js.map +0 -1
  66. package/dist/keys-BdNiD3rC.js.map +0 -1
  67. package/dist/translations-CnkwrCJ3.js.map +0 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # @10x-media/webhooks
2
2
 
3
+ ## 0.1.0-beta.1
4
+
5
+ ### Minor Changes
6
+
7
+ - Add a typed `translations` option to every plugin factory and make translation keys a stable public API. Each plugin's `./i18n` subpath now exports the `keys` object, the `TranslationKey` union, and the `TranslationsOption` shape. Overrides are flat and per-locale: values win over the built-in locales key-by-key, locales a plugin does not ship are added whole, and app-level `i18n.translations` still wins over everything.
8
+
9
+ ```ts
10
+ import { analytics } from "@10x-media/analytics";
11
+ import { keys } from "@10x-media/analytics/i18n";
12
+
13
+ analytics({
14
+ adapters: [nativeAdapter()],
15
+ translations: {
16
+ de: { [keys.pluginName]: "Analytik" },
17
+ },
18
+ });
19
+ ```
20
+
21
+ A typo'd key inside `translations` is a compile error.
22
+
23
+ ### Patch Changes
24
+
25
+ - Restructure README: features, quick start, and links into the documentation site at https://docs.10xmedia.de. Long-form documentation moved out of the package README.
26
+
27
+ - Update README documentation links: the docs site now serves from the domain root, so `docs.10xmedia.de/docs/<plugin>` links became `docs.10xmedia.de/<plugin>`.
28
+
29
+ - Ship per-file dist output instead of bundled chunks. Bundling merged client components into shared chunks and dropped their 'use client' directives, so Next.js lost the RSC boundary and the admin panel crashed with "useRef only works in Client Components" when rendering components imported through such a chunk (for analytics: every chart-based dashboard widget). Dist now mirrors src one file at a time, directives stay exactly where they were authored, and file names are stable across releases. A repo-level `check:dist` verification (directive parity, no inlined dependencies, exports resolution, publint) now runs in CI so this class of regression cannot ship again.
30
+
3
31
  ## 0.1.0-beta.0
4
32
 
5
33
  ### Minor Changes
package/README.md CHANGED
@@ -1,26 +1,29 @@
1
1
  # @10x-media/webhooks
2
2
 
3
- Outbound webhook subscriptions for Payload v3. Opt any collection in, let subscribers register URLs in the admin panel, and deliver signed HTTP POSTs on every create/update/delete -- inline or via Payload's built-in jobs queue.
3
+ Outbound webhook subscriptions for Payload v3. Opt collections in, let subscribers register URLs in the admin panel (or in code), and deliver signed HTTP POSTs on every create, update, and delete, inline or through Payload's jobs queue, with retries and an append-only delivery log.
4
4
 
5
5
  [![npm](https://img.shields.io/npm/v/@10x-media/webhooks?style=flat-square)](https://www.npmjs.com/package/@10x-media/webhooks)
6
6
 
7
7
  Part of the [@10x-media Payload plugins](https://github.com/10x-media/payload-plugins) collection. In beta: published under the `beta` dist-tag until a stable 1.0.
8
8
 
9
- ## Requirements
9
+ ## Features
10
10
 
11
- - Payload v3 (peer: `payload@^3.82.0`)
12
- - React 19 (peer)
13
- - Node 22.18+
11
+ - **Opt-in collections** with per-collection operations, `previousData` on updates, and a `transform` to reshape or redact payloads.
12
+ - **Two subscription sources**: admin-managed records and hard-coded code subscriptions, merged at delivery time.
13
+ - **Delivery modes**: queued through Payload jobs (retries, timeouts) or inline; `auto` detects whether a runner exists.
14
+ - **HMAC-SHA256 signing** with reveal-once secrets and id/event/timestamp headers on every request.
15
+ - **Delivery log** with status, response code, duration, the exact body sent, and one-click redelivery.
16
+ - **Jobs family interop**: installing `@10x-media/jobs` switches delivery to the queue automatically.
17
+ - **Typed translations** with per-key overrides via `@10x-media/webhooks/i18n`.
14
18
 
15
- ## Installation
19
+ ## Quick start
16
20
 
17
21
  ```bash
18
22
  pnpm add @10x-media/webhooks
19
23
  ```
20
24
 
21
- ## Usage
22
-
23
25
  ```ts
26
+ // payload.config.ts
24
27
  import { buildConfig } from 'payload'
25
28
  import { webhooks } from '@10x-media/webhooks'
26
29
 
@@ -36,150 +39,19 @@ export default buildConfig({
36
39
  })
37
40
  ```
38
41
 
39
- That is the minimum config. The plugin adds two collections to your admin panel:
40
-
41
- - **Webhook Subscriptions** -- admins create endpoint records (URL, events, optional secret).
42
- - **Webhook Deliveries** -- an append-only log with status, response code, and a redeliver button.
43
-
44
- ## Options
45
-
46
- | Option | Type | Default | Description |
47
- |---|---|---|---|
48
- | `disabled` | `boolean` | `false` | When `true`, returns the config unchanged. |
49
- | `collections` | `Record<string, true \| CollectionWebhookConfig>` | `{}` | Collections that emit webhook events. |
50
- | `subscriptions` | `CodeSubscription[]` | `[]` | Hard-coded subscriptions (no admin record needed). |
51
- | `delivery` | `DeliveryMode \| DeliveryOptions` | `'auto'` | How deliveries are executed. |
52
- | `subscriptionsCollection` | `{ slug?: string; hidden?: boolean }` | -- | Override the subscriptions collection slug or hide it from the admin panel. |
53
- | `deliveriesLog` | `{ slug?: string; hidden?: boolean }` | -- | Override the deliveries collection slug or hide it from the admin panel. |
54
-
55
- ### `CollectionWebhookConfig`
56
-
57
- ```ts
58
- type CollectionWebhookConfig = {
59
- operations?: ('create' | 'update' | 'delete')[]
60
- includePreviousData?: boolean
61
- transform?: (args: {
62
- doc: Record<string, unknown>
63
- previousDoc?: Record<string, unknown>
64
- operation: 'create' | 'update' | 'delete'
65
- req: PayloadRequest
66
- }) => unknown
67
- }
68
- ```
69
-
70
- `true` is shorthand for all three operations with no transform.
71
-
72
- `transform` lets you redact fields or reshape the payload before it is sent. Return `undefined` to suppress delivery for that document.
73
-
74
- `includePreviousData` adds a `previousData` key to the body on `update` events.
75
-
76
- ### Delivery modes
77
-
78
- | Mode | Behavior |
79
- |---|---|
80
- | `'auto'` (default) | Queued when `config.jobs.autoRun` is set or `@10x-media/jobs` is installed; inline otherwise. |
81
- | `'queue'` | Always enqueued as a Payload job. A worker must run `payload.jobs.run()`. |
82
- | `'inline'` | Awaited in the `afterChange`/`afterDelete` hook. Simple but adds latency to every write. |
83
-
84
- Pass a full `DeliveryOptions` object to tune the queue and timeout:
85
-
86
- ```ts
87
- webhooks({
88
- collections: { posts: true },
89
- delivery: {
90
- mode: 'queue',
91
- timeoutMs: 5_000,
92
- retries: 3,
93
- queue: 'webhooks',
94
- },
95
- })
96
- ```
97
-
98
- Defaults: `timeoutMs: 10000`, `retries: 4`, `queue: 'default'`.
99
-
100
- ### Code subscriptions
101
-
102
- Register subscriptions in code when you do not want them managed through the admin panel:
103
-
104
- ```ts
105
- webhooks({
106
- collections: { orders: true },
107
- subscriptions: [
108
- {
109
- id: 'my-crm',
110
- url: 'https://crm.example.com/hooks/orders',
111
- events: ['orders.created', 'orders.updated'],
112
- secret: process.env.WEBHOOK_SECRET,
113
- },
114
- ],
115
- })
116
- ```
117
-
118
- Code subscriptions are merged with admin-managed ones at delivery time.
119
-
120
- ## Webhook payload
121
-
122
- Every delivery POSTs a JSON body:
123
-
124
- ```json
125
- {
126
- "id": "<delivery-id>",
127
- "event": "posts.created",
128
- "collection": "posts",
129
- "operation": "create",
130
- "occurredAt": "2025-01-01T00:00:00.000Z",
131
- "data": { ... }
132
- }
133
- ```
134
-
135
- `previousData` is included on `update` events when `includePreviousData: true`.
136
-
137
- ## Signature verification
138
-
139
- When a subscription has a `secret`, each request carries an `X-Webhook-Signature` header:
140
-
141
- ```
142
- X-Webhook-Signature: v1=<hex>
143
- ```
144
-
145
- The signature is HMAC-SHA256 over `${timestamp}.${rawBody}`, where `timestamp` is the Unix second from the `X-Webhook-Timestamp` header.
146
-
147
- Verify in your receiver:
148
-
149
- ```ts
150
- import { createHmac } from 'node:crypto'
151
-
152
- function verify(secret: string, timestamp: string, rawBody: string, signature: string): boolean {
153
- const expected = createHmac('sha256', secret)
154
- .update(`${timestamp}.${rawBody}`)
155
- .digest('hex')
156
- return signature === `v1=${expected}`
157
- }
158
- ```
159
-
160
- Additional headers sent on every request: `X-Webhook-Id`, `X-Webhook-Event`, `X-Webhook-Timestamp`, `User-Agent: 10x-media-webhooks`. Subscriptions can inject extra headers via the admin panel's **Headers** array field.
161
-
162
- ## Admin panel
163
-
164
- The plugin adds a **Webhooks** group with two collections.
165
-
166
- **Webhook Subscriptions**: create and manage endpoint records. A random 48-character hex secret is auto-generated on create and shown in full **exactly once** on that create response -- copy it to your receiver then. On every later read it is masked (`__redacted__`); the raw value is still used internally to sign deliveries but is never returned through the API or admin again. Rotate by deleting and recreating the record.
167
-
168
- **Webhook Deliveries**: an append-only delivery log. Each row shows the event, subscription, status, HTTP response code, and a **Redeliver** button that replays the original payload to the original URL. Access requires a logged-in admin.
169
-
170
- ## Composing with `@10x-media/jobs`
171
-
172
- When `@10x-media/jobs` is installed, `delivery` mode auto-resolves to `'queue'` and the delivery task runs under the jobs worker. No extra config is needed -- the plugin detects the sibling plugin automatically.
173
-
174
- ## Composing with `@10x-media/automations`
42
+ Then create a subscription in the admin (**Webhooks** group), copy its reveal-once secret to your receiver, and write to an opted-in collection.
175
43
 
176
- When `@10x-media/automations` is installed, the plugin contributes a `webhook` trigger slug to the automations catalog, reserving it for future inbound-webhook support. This plugin is outbound-only today and does not yet fire that trigger -- the contribution simply registers the slug so a later inbound phase can use it. No extra config is needed.
44
+ ## Documentation
177
45
 
178
- ## Security
46
+ Full documentation at [docs.10xmedia.de](https://docs.10xmedia.de/webhooks):
179
47
 
180
- - **Signing secrets are reveal-once.** A subscription's secret is shown in full only on the create response and masked (`__redacted__`) on every read thereafter; the raw value is used internally to sign deliveries and is never returned via REST, GraphQL, or the admin again. It is currently stored unencrypted at rest -- encryption-at-rest is planned.
181
- - **Outbound requests target operator-supplied URLs (SSRF).** Deliveries POST to whatever URL a subscription specifies, including private or internal hosts. Subscriptions are created by authenticated admins, so treat that as a trusted operation; if your admins are not fully trusted, restrict outbound egress at the network layer or front your receivers with an allowlist.
182
- - **Admin access is the trust boundary.** Both collections require a logged-in user, and the redeliver endpoint authorizes by login only (any authenticated user may redeliver any delivery). Tighten the collections with your own access control if you need finer-grained permissions.
48
+ - [Overview](https://docs.10xmedia.de/webhooks)
49
+ - [Quick start](https://docs.10xmedia.de/webhooks/quick-start)
50
+ - [Subscriptions](https://docs.10xmedia.de/webhooks/subscriptions)
51
+ - [Deliveries](https://docs.10xmedia.de/webhooks/deliveries)
52
+ - [Signing and secrets](https://docs.10xmedia.de/webhooks/signing)
53
+ - [Security model](https://docs.10xmedia.de/webhooks/security)
54
+ - [i18n](https://docs.10xmedia.de/webhooks/i18n)
183
55
 
184
56
  ## License
185
57
 
@@ -0,0 +1,99 @@
1
+ import { keys } from "../translations/keys.js";
2
+ import { ADMIN_GROUP } from "../constants.js";
3
+ import { labelForKey } from "../translations/server.js";
4
+ //#region src/collections/deliveries.ts
5
+ const STATUS_CELL = "@10x-media/webhooks/client#DeliveryStatusCell";
6
+ const loggedIn = ({ req }) => Boolean(req.user);
7
+ /** Runner-written delivery audit log; read-only in admin, server writes use overrideAccess. */
8
+ const buildDeliveriesCollection = (args) => ({
9
+ slug: args.slug,
10
+ labels: {
11
+ singular: labelForKey(keys.deliverySingular),
12
+ plural: labelForKey(keys.deliveryPlural)
13
+ },
14
+ admin: {
15
+ group: ADMIN_GROUP,
16
+ useAsTitle: "event",
17
+ defaultColumns: [
18
+ "event",
19
+ "endpoint",
20
+ "status",
21
+ "responseStatus",
22
+ "attempt",
23
+ "createdAt"
24
+ ],
25
+ hidden: args.hidden
26
+ },
27
+ access: {
28
+ read: loggedIn,
29
+ create: () => false,
30
+ update: () => false,
31
+ delete: loggedIn
32
+ },
33
+ fields: [
34
+ {
35
+ name: "subscriptionId",
36
+ type: "text",
37
+ index: true
38
+ },
39
+ {
40
+ name: "endpoint",
41
+ type: "text"
42
+ },
43
+ {
44
+ name: "event",
45
+ type: "text",
46
+ index: true
47
+ },
48
+ {
49
+ name: "status",
50
+ type: "select",
51
+ defaultValue: "pending",
52
+ options: [
53
+ "pending",
54
+ "success",
55
+ "failed",
56
+ "dead"
57
+ ],
58
+ admin: { components: { Cell: STATUS_CELL } }
59
+ },
60
+ {
61
+ name: "attempt",
62
+ type: "number",
63
+ defaultValue: 0
64
+ },
65
+ {
66
+ name: "responseStatus",
67
+ type: "number"
68
+ },
69
+ {
70
+ name: "responseBody",
71
+ type: "textarea"
72
+ },
73
+ {
74
+ name: "error",
75
+ type: "textarea"
76
+ },
77
+ {
78
+ name: "durationMs",
79
+ type: "number"
80
+ },
81
+ {
82
+ name: "jobId",
83
+ type: "text"
84
+ },
85
+ {
86
+ name: "payload",
87
+ type: "json"
88
+ },
89
+ {
90
+ name: "redeliver",
91
+ type: "ui",
92
+ admin: { components: { Field: "@10x-media/webhooks/client#RedeliverButton" } }
93
+ }
94
+ ]
95
+ });
96
+ //#endregion
97
+ export { buildDeliveriesCollection };
98
+
99
+ //# sourceMappingURL=deliveries.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deliveries.js","names":[],"sources":["../../src/collections/deliveries.ts"],"sourcesContent":["import type { CollectionConfig } from 'payload'\n\nimport { ADMIN_GROUP } from '../constants'\nimport { keys } from '../translations/keys'\nimport { labelForKey } from '../translations/server'\n\nconst STATUS_CELL = '@10x-media/webhooks/client#DeliveryStatusCell'\n\nconst loggedIn = ({ req }: { req: { user?: unknown } }) => Boolean(req.user)\n\n/** Runner-written delivery audit log; read-only in admin, server writes use overrideAccess. */\nexport const buildDeliveriesCollection = (args: {\n\tslug: string\n\thidden: boolean\n}): CollectionConfig => ({\n\tslug: args.slug,\n\tlabels: {\n\t\tsingular: labelForKey(keys.deliverySingular),\n\t\tplural: labelForKey(keys.deliveryPlural),\n\t},\n\tadmin: {\n\t\tgroup: ADMIN_GROUP,\n\t\tuseAsTitle: 'event',\n\t\tdefaultColumns: ['event', 'endpoint', 'status', 'responseStatus', 'attempt', 'createdAt'],\n\t\thidden: args.hidden,\n\t},\n\taccess: { read: loggedIn, create: () => false, update: () => false, delete: loggedIn },\n\tfields: [\n\t\t{ name: 'subscriptionId', type: 'text', index: true },\n\t\t{ name: 'endpoint', type: 'text' },\n\t\t{ name: 'event', type: 'text', index: true },\n\t\t{\n\t\t\tname: 'status',\n\t\t\ttype: 'select',\n\t\t\tdefaultValue: 'pending',\n\t\t\toptions: ['pending', 'success', 'failed', 'dead'],\n\t\t\tadmin: { components: { Cell: STATUS_CELL } },\n\t\t},\n\t\t{ name: 'attempt', type: 'number', defaultValue: 0 },\n\t\t{ name: 'responseStatus', type: 'number' },\n\t\t{ name: 'responseBody', type: 'textarea' },\n\t\t{ name: 'error', type: 'textarea' },\n\t\t{ name: 'durationMs', type: 'number' },\n\t\t{ name: 'jobId', type: 'text' },\n\t\t{ name: 'payload', type: 'json' },\n\t\t{\n\t\t\tname: 'redeliver',\n\t\t\ttype: 'ui',\n\t\t\tadmin: { components: { Field: '@10x-media/webhooks/client#RedeliverButton' } },\n\t\t},\n\t],\n})\n"],"mappings":";;;;AAMA,MAAM,cAAc;AAEpB,MAAM,YAAY,EAAE,UAAuC,QAAQ,IAAI,IAAI;;AAG3E,MAAa,6BAA6B,UAGjB;CACxB,MAAM,KAAK;CACX,QAAQ;EACP,UAAU,YAAY,KAAK,gBAAgB;EAC3C,QAAQ,YAAY,KAAK,cAAc;CACxC;CACA,OAAO;EACN,OAAO;EACP,YAAY;EACZ,gBAAgB;GAAC;GAAS;GAAY;GAAU;GAAkB;GAAW;EAAW;EACxF,QAAQ,KAAK;CACd;CACA,QAAQ;EAAE,MAAM;EAAU,cAAc;EAAO,cAAc;EAAO,QAAQ;CAAS;CACrF,QAAQ;EACP;GAAE,MAAM;GAAkB,MAAM;GAAQ,OAAO;EAAK;EACpD;GAAE,MAAM;GAAY,MAAM;EAAO;EACjC;GAAE,MAAM;GAAS,MAAM;GAAQ,OAAO;EAAK;EAC3C;GACC,MAAM;GACN,MAAM;GACN,cAAc;GACd,SAAS;IAAC;IAAW;IAAW;IAAU;GAAM;GAChD,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,EAAE;EAC5C;EACA;GAAE,MAAM;GAAW,MAAM;GAAU,cAAc;EAAE;EACnD;GAAE,MAAM;GAAkB,MAAM;EAAS;EACzC;GAAE,MAAM;GAAgB,MAAM;EAAW;EACzC;GAAE,MAAM;GAAS,MAAM;EAAW;EAClC;GAAE,MAAM;GAAc,MAAM;EAAS;EACrC;GAAE,MAAM;GAAS,MAAM;EAAO;EAC9B;GAAE,MAAM;GAAW,MAAM;EAAO;EAChC;GACC,MAAM;GACN,MAAM;GACN,OAAO,EAAE,YAAY,EAAE,OAAO,6CAA6C,EAAE;EAC9E;CACD;AACD"}
@@ -0,0 +1,123 @@
1
+ import { keys } from "../translations/keys.js";
2
+ import { ADMIN_GROUP, SECRET_MASK, SECRET_REVEAL_CONTEXT } from "../constants.js";
3
+ import { labelForKey } from "../translations/server.js";
4
+ import { randomBytes } from "node:crypto";
5
+ //#region src/collections/subscriptions.ts
6
+ const generateSecret = ({ data, operation, req }) => {
7
+ if (operation === "create" && !data.secret) {
8
+ req.context[SECRET_REVEAL_CONTEXT.once] = true;
9
+ return {
10
+ ...data,
11
+ secret: randomBytes(24).toString("hex")
12
+ };
13
+ }
14
+ return data;
15
+ };
16
+ /**
17
+ * The DB always stores the raw secret; this mask only shapes read output. It runs even under
18
+ * `overrideAccess` (field hooks are not gated by access), so the raw value reaches a reader only
19
+ * when a reveal flag is set: `once` on the create response, `forSigning` on internal delivery reads.
20
+ */
21
+ const maskSecret = ({ value, req }) => {
22
+ if (req.context[SECRET_REVEAL_CONTEXT.forSigning] || req.context[SECRET_REVEAL_CONTEXT.once]) return value;
23
+ return value == null ? value : SECRET_MASK;
24
+ };
25
+ const clearRevealOnce = ({ doc, req }) => {
26
+ req.context[SECRET_REVEAL_CONTEXT.once] = false;
27
+ return doc;
28
+ };
29
+ const loggedIn = ({ req }) => Boolean(req.user);
30
+ /** Admin-managed subscriptions collection; `events` options come from the catalog. */
31
+ const buildSubscriptionsCollection = (args) => ({
32
+ slug: args.slug,
33
+ labels: {
34
+ singular: labelForKey(keys.subscriptionSingular),
35
+ plural: labelForKey(keys.subscriptionPlural)
36
+ },
37
+ admin: {
38
+ group: ADMIN_GROUP,
39
+ useAsTitle: "name",
40
+ defaultColumns: [
41
+ "name",
42
+ "url",
43
+ "enabled"
44
+ ],
45
+ hidden: args.hidden
46
+ },
47
+ access: {
48
+ read: loggedIn,
49
+ create: loggedIn,
50
+ update: loggedIn,
51
+ delete: loggedIn
52
+ },
53
+ hooks: {
54
+ beforeChange: [generateSecret],
55
+ afterChange: [clearRevealOnce]
56
+ },
57
+ fields: [
58
+ {
59
+ name: "name",
60
+ type: "text",
61
+ required: true,
62
+ label: labelForKey(keys.fieldName)
63
+ },
64
+ {
65
+ name: "url",
66
+ type: "text",
67
+ required: true,
68
+ label: labelForKey(keys.fieldUrl)
69
+ },
70
+ {
71
+ name: "enabled",
72
+ type: "checkbox",
73
+ defaultValue: true,
74
+ label: labelForKey(keys.fieldEnabled)
75
+ },
76
+ {
77
+ name: "events",
78
+ type: "select",
79
+ hasMany: true,
80
+ label: labelForKey(keys.fieldEvents),
81
+ options: args.events.length ? args.events.map((e) => ({
82
+ label: e,
83
+ value: e
84
+ })) : [{
85
+ label: "(none)",
86
+ value: "__none__"
87
+ }]
88
+ },
89
+ {
90
+ name: "secret",
91
+ type: "text",
92
+ label: labelForKey(keys.fieldSecret),
93
+ admin: {
94
+ readOnly: true,
95
+ description: labelForKey(keys.fieldSecretHelp)
96
+ },
97
+ access: { update: () => false },
98
+ hooks: { afterRead: [maskSecret] }
99
+ },
100
+ {
101
+ name: "headers",
102
+ type: "array",
103
+ label: labelForKey(keys.fieldHeaders),
104
+ fields: [{
105
+ name: "key",
106
+ type: "text",
107
+ required: true
108
+ }, {
109
+ name: "value",
110
+ type: "text"
111
+ }]
112
+ },
113
+ {
114
+ name: "description",
115
+ type: "textarea",
116
+ label: labelForKey(keys.fieldDescription)
117
+ }
118
+ ]
119
+ });
120
+ //#endregion
121
+ export { buildSubscriptionsCollection };
122
+
123
+ //# sourceMappingURL=subscriptions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"subscriptions.js","names":[],"sources":["../../src/collections/subscriptions.ts"],"sourcesContent":["import { randomBytes } from 'node:crypto'\nimport type {\n\tCollectionAfterChangeHook,\n\tCollectionBeforeChangeHook,\n\tCollectionConfig,\n\tFieldHook,\n} from 'payload'\n\nimport { ADMIN_GROUP, SECRET_MASK, SECRET_REVEAL_CONTEXT } from '../constants'\nimport { keys } from '../translations/keys'\nimport { labelForKey } from '../translations/server'\n\nconst generateSecret: CollectionBeforeChangeHook = ({ data, operation, req }) => {\n\tif (operation === 'create' && !data.secret) {\n\t\treq.context[SECRET_REVEAL_CONTEXT.once] = true\n\t\treturn { ...data, secret: randomBytes(24).toString('hex') }\n\t}\n\treturn data\n}\n\n/**\n * The DB always stores the raw secret; this mask only shapes read output. It runs even under\n * `overrideAccess` (field hooks are not gated by access), so the raw value reaches a reader only\n * when a reveal flag is set: `once` on the create response, `forSigning` on internal delivery reads.\n */\nconst maskSecret: FieldHook = ({ value, req }) => {\n\tif (req.context[SECRET_REVEAL_CONTEXT.forSigning] || req.context[SECRET_REVEAL_CONTEXT.once]) {\n\t\treturn value\n\t}\n\treturn value == null ? value : SECRET_MASK\n}\n\nconst clearRevealOnce: CollectionAfterChangeHook = ({ doc, req }) => {\n\treq.context[SECRET_REVEAL_CONTEXT.once] = false\n\treturn doc\n}\n\nconst loggedIn = ({ req }: { req: { user?: unknown } }) => Boolean(req.user)\n\n/** Admin-managed subscriptions collection; `events` options come from the catalog. */\nexport const buildSubscriptionsCollection = (args: {\n\tslug: string\n\tevents: string[]\n\thidden: boolean\n}): CollectionConfig => ({\n\tslug: args.slug,\n\tlabels: {\n\t\tsingular: labelForKey(keys.subscriptionSingular),\n\t\tplural: labelForKey(keys.subscriptionPlural),\n\t},\n\tadmin: {\n\t\tgroup: ADMIN_GROUP,\n\t\tuseAsTitle: 'name',\n\t\tdefaultColumns: ['name', 'url', 'enabled'],\n\t\thidden: args.hidden,\n\t},\n\taccess: { read: loggedIn, create: loggedIn, update: loggedIn, delete: loggedIn },\n\thooks: { beforeChange: [generateSecret], afterChange: [clearRevealOnce] },\n\tfields: [\n\t\t{ name: 'name', type: 'text', required: true, label: labelForKey(keys.fieldName) },\n\t\t{ name: 'url', type: 'text', required: true, label: labelForKey(keys.fieldUrl) },\n\t\t{\n\t\t\tname: 'enabled',\n\t\t\ttype: 'checkbox',\n\t\t\tdefaultValue: true,\n\t\t\tlabel: labelForKey(keys.fieldEnabled),\n\t\t},\n\t\t{\n\t\t\tname: 'events',\n\t\t\ttype: 'select',\n\t\t\thasMany: true,\n\t\t\tlabel: labelForKey(keys.fieldEvents),\n\t\t\toptions: args.events.length\n\t\t\t\t? args.events.map((e) => ({ label: e, value: e }))\n\t\t\t\t: [{ label: '(none)', value: '__none__' }],\n\t\t},\n\t\t{\n\t\t\tname: 'secret',\n\t\t\ttype: 'text',\n\t\t\tlabel: labelForKey(keys.fieldSecret),\n\t\t\tadmin: { readOnly: true, description: labelForKey(keys.fieldSecretHelp) },\n\t\t\taccess: { update: () => false },\n\t\t\thooks: { afterRead: [maskSecret] },\n\t\t},\n\t\t{\n\t\t\tname: 'headers',\n\t\t\ttype: 'array',\n\t\t\tlabel: labelForKey(keys.fieldHeaders),\n\t\t\tfields: [\n\t\t\t\t{ name: 'key', type: 'text', required: true },\n\t\t\t\t{ name: 'value', type: 'text' },\n\t\t\t],\n\t\t},\n\t\t{ name: 'description', type: 'textarea', label: labelForKey(keys.fieldDescription) },\n\t],\n})\n"],"mappings":";;;;;AAYA,MAAM,kBAA8C,EAAE,MAAM,WAAW,UAAU;CAChF,IAAI,cAAc,YAAY,CAAC,KAAK,QAAQ;EAC3C,IAAI,QAAQ,sBAAsB,QAAQ;EAC1C,OAAO;GAAE,GAAG;GAAM,QAAQ,YAAY,EAAE,EAAE,SAAS,KAAK;EAAE;CAC3D;CACA,OAAO;AACR;;;;;;AAOA,MAAM,cAAyB,EAAE,OAAO,UAAU;CACjD,IAAI,IAAI,QAAQ,sBAAsB,eAAe,IAAI,QAAQ,sBAAsB,OACtF,OAAO;CAER,OAAO,SAAS,OAAO,QAAQ;AAChC;AAEA,MAAM,mBAA8C,EAAE,KAAK,UAAU;CACpE,IAAI,QAAQ,sBAAsB,QAAQ;CAC1C,OAAO;AACR;AAEA,MAAM,YAAY,EAAE,UAAuC,QAAQ,IAAI,IAAI;;AAG3E,MAAa,gCAAgC,UAIpB;CACxB,MAAM,KAAK;CACX,QAAQ;EACP,UAAU,YAAY,KAAK,oBAAoB;EAC/C,QAAQ,YAAY,KAAK,kBAAkB;CAC5C;CACA,OAAO;EACN,OAAO;EACP,YAAY;EACZ,gBAAgB;GAAC;GAAQ;GAAO;EAAS;EACzC,QAAQ,KAAK;CACd;CACA,QAAQ;EAAE,MAAM;EAAU,QAAQ;EAAU,QAAQ;EAAU,QAAQ;CAAS;CAC/E,OAAO;EAAE,cAAc,CAAC,cAAc;EAAG,aAAa,CAAC,eAAe;CAAE;CACxE,QAAQ;EACP;GAAE,MAAM;GAAQ,MAAM;GAAQ,UAAU;GAAM,OAAO,YAAY,KAAK,SAAS;EAAE;EACjF;GAAE,MAAM;GAAO,MAAM;GAAQ,UAAU;GAAM,OAAO,YAAY,KAAK,QAAQ;EAAE;EAC/E;GACC,MAAM;GACN,MAAM;GACN,cAAc;GACd,OAAO,YAAY,KAAK,YAAY;EACrC;EACA;GACC,MAAM;GACN,MAAM;GACN,SAAS;GACT,OAAO,YAAY,KAAK,WAAW;GACnC,SAAS,KAAK,OAAO,SAClB,KAAK,OAAO,KAAK,OAAO;IAAE,OAAO;IAAG,OAAO;GAAE,EAAE,IAC/C,CAAC;IAAE,OAAO;IAAU,OAAO;GAAW,CAAC;EAC3C;EACA;GACC,MAAM;GACN,MAAM;GACN,OAAO,YAAY,KAAK,WAAW;GACnC,OAAO;IAAE,UAAU;IAAM,aAAa,YAAY,KAAK,eAAe;GAAE;GACxE,QAAQ,EAAE,cAAc,MAAM;GAC9B,OAAO,EAAE,WAAW,CAAC,UAAU,EAAE;EAClC;EACA;GACC,MAAM;GACN,MAAM;GACN,OAAO,YAAY,KAAK,YAAY;GACpC,QAAQ,CACP;IAAE,MAAM;IAAO,MAAM;IAAQ,UAAU;GAAK,GAC5C;IAAE,MAAM;IAAS,MAAM;GAAO,CAC/B;EACD;EACA;GAAE,MAAM;GAAe,MAAM;GAAY,OAAO,YAAY,KAAK,gBAAgB;EAAE;CACpF;AACD"}
@@ -0,0 +1,29 @@
1
+ //#region src/constants.ts
2
+ const DEFAULT_SUBSCRIPTIONS_SLUG = "webhook-subscriptions";
3
+ const DEFAULT_DELIVERIES_SLUG = "webhook-deliveries";
4
+ const ADMIN_GROUP = "Webhooks";
5
+ const WEBHOOK_DELIVER_TASK = "webhooksDeliver";
6
+ const DEFAULT_DELIVERY_QUEUE = "default";
7
+ const DEFAULT_TIMEOUT_MS = 1e4;
8
+ /** Placeholder returned for the signing secret on every read after its single create reveal. */
9
+ const SECRET_MASK = "__redacted__";
10
+ /**
11
+ * `req.context` flags that opt a subscription read into seeing the raw signing secret.
12
+ * The field `afterRead` mask runs even under `overrideAccess`, so internal signing reads
13
+ * must set `revealSecretForSigning` to recover the raw value; `revealSecretOnce` is set by
14
+ * the create `beforeChange` so the create response shows the secret exactly once.
15
+ */
16
+ const SECRET_REVEAL_CONTEXT = {
17
+ forSigning: "webhooksRevealSecretForSigning",
18
+ once: "webhooksRevealSecretOnce"
19
+ };
20
+ const RESERVED_SLUGS = [
21
+ "payload-jobs",
22
+ "payload-locks",
23
+ "payload-preferences",
24
+ "payload-migrations"
25
+ ];
26
+ //#endregion
27
+ export { ADMIN_GROUP, DEFAULT_DELIVERIES_SLUG, DEFAULT_DELIVERY_QUEUE, DEFAULT_SUBSCRIPTIONS_SLUG, DEFAULT_TIMEOUT_MS, RESERVED_SLUGS, SECRET_MASK, SECRET_REVEAL_CONTEXT, WEBHOOK_DELIVER_TASK };
28
+
29
+ //# sourceMappingURL=constants.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.js","names":[],"sources":["../src/constants.ts"],"sourcesContent":["export const DEFAULT_SUBSCRIPTIONS_SLUG = 'webhook-subscriptions'\nexport const DEFAULT_DELIVERIES_SLUG = 'webhook-deliveries'\nexport const ADMIN_GROUP = 'Webhooks'\nexport const WEBHOOK_DELIVER_TASK = 'webhooksDeliver'\nexport const DEFAULT_DELIVERY_QUEUE = 'default'\nexport const DEFAULT_TIMEOUT_MS = 10_000\nexport const DEFAULT_RETRIES = 4\n\n/** Placeholder returned for the signing secret on every read after its single create reveal. */\nexport const SECRET_MASK = '__redacted__'\n\n/**\n * `req.context` flags that opt a subscription read into seeing the raw signing secret.\n * The field `afterRead` mask runs even under `overrideAccess`, so internal signing reads\n * must set `revealSecretForSigning` to recover the raw value; `revealSecretOnce` is set by\n * the create `beforeChange` so the create response shows the secret exactly once.\n */\nexport const SECRET_REVEAL_CONTEXT = {\n\tforSigning: 'webhooksRevealSecretForSigning',\n\tonce: 'webhooksRevealSecretOnce',\n} as const\nexport const RESERVED_SLUGS = [\n\t'payload-jobs',\n\t'payload-locks',\n\t'payload-preferences',\n\t'payload-migrations',\n] as const\n"],"mappings":";AAAA,MAAa,6BAA6B;AAC1C,MAAa,0BAA0B;AACvC,MAAa,cAAc;AAC3B,MAAa,uBAAuB;AACpC,MAAa,yBAAyB;AACtC,MAAa,qBAAqB;;AAIlC,MAAa,cAAc;;;;;;;AAQ3B,MAAa,wBAAwB;CACpC,YAAY;CACZ,MAAM;AACP;AACA,MAAa,iBAAiB;CAC7B;CACA;CACA;CACA;AACD"}
@@ -0,0 +1,10 @@
1
+ import { DefaultCellComponentProps } from "payload";
2
+
3
+ //#region src/delivery/DeliveryStatusCell.d.ts
4
+ /** List cell rendering a delivery's status as a native Payload Pill. */
5
+ declare const DeliveryStatusCell: ({
6
+ cellData
7
+ }: DefaultCellComponentProps) => import("react/jsx-runtime").JSX.Element;
8
+ //#endregion
9
+ export { DeliveryStatusCell };
10
+ //# sourceMappingURL=DeliveryStatusCell.d.ts.map
@@ -0,0 +1,42 @@
1
+ "use client";
2
+ import { keys } from "../translations/keys.js";
3
+ import { useTranslation as useTranslation$1 } from "../translations/useTranslation.js";
4
+ import { Pill } from "@payloadcms/ui";
5
+ import { jsx } from "react/jsx-runtime";
6
+ //#region src/delivery/DeliveryStatusCell.tsx
7
+ const META = {
8
+ pending: {
9
+ labelKey: keys.statusPending,
10
+ pillStyle: "light"
11
+ },
12
+ success: {
13
+ labelKey: keys.statusSuccess,
14
+ pillStyle: "success"
15
+ },
16
+ failed: {
17
+ labelKey: keys.statusFailed,
18
+ pillStyle: "warning"
19
+ },
20
+ dead: {
21
+ labelKey: keys.statusDead,
22
+ pillStyle: "error"
23
+ }
24
+ };
25
+ const FALLBACK = {
26
+ labelKey: keys.statusPending,
27
+ pillStyle: "light"
28
+ };
29
+ /** List cell rendering a delivery's status as a native Payload Pill. */
30
+ const DeliveryStatusCell = ({ cellData }) => {
31
+ const { t } = useTranslation$1();
32
+ const meta = META[String(cellData)] ?? FALLBACK;
33
+ return /* @__PURE__ */ jsx(Pill, {
34
+ pillStyle: meta.pillStyle,
35
+ size: "small",
36
+ children: t(meta.labelKey)
37
+ });
38
+ };
39
+ //#endregion
40
+ export { DeliveryStatusCell };
41
+
42
+ //# sourceMappingURL=DeliveryStatusCell.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DeliveryStatusCell.js","names":["useTranslation"],"sources":["../../src/delivery/DeliveryStatusCell.tsx"],"sourcesContent":["'use client'\n\nimport { Pill } from '@payloadcms/ui'\nimport type { DefaultCellComponentProps } from 'payload'\n\nimport { keys, type TranslationKey } from '../translations/keys'\nimport { useTranslation } from '../translations/useTranslation'\n\ntype MetaEntry = { labelKey: TranslationKey; pillStyle: 'error' | 'light' | 'success' | 'warning' }\n\nconst META: Record<string, MetaEntry> = {\n\tpending: { labelKey: keys.statusPending, pillStyle: 'light' },\n\tsuccess: { labelKey: keys.statusSuccess, pillStyle: 'success' },\n\tfailed: { labelKey: keys.statusFailed, pillStyle: 'warning' },\n\tdead: { labelKey: keys.statusDead, pillStyle: 'error' },\n}\n\nconst FALLBACK: MetaEntry = { labelKey: keys.statusPending, pillStyle: 'light' }\n\n/** List cell rendering a delivery's status as a native Payload Pill. */\nexport const DeliveryStatusCell = ({ cellData }: DefaultCellComponentProps) => {\n\tconst { t } = useTranslation()\n\tconst meta = META[String(cellData)] ?? FALLBACK\n\treturn (\n\t\t<Pill pillStyle={meta.pillStyle} size=\"small\">\n\t\t\t{t(meta.labelKey)}\n\t\t</Pill>\n\t)\n}\n"],"mappings":";;;;;;AAUA,MAAM,OAAkC;CACvC,SAAS;EAAE,UAAU,KAAK;EAAe,WAAW;CAAQ;CAC5D,SAAS;EAAE,UAAU,KAAK;EAAe,WAAW;CAAU;CAC9D,QAAQ;EAAE,UAAU,KAAK;EAAc,WAAW;CAAU;CAC5D,MAAM;EAAE,UAAU,KAAK;EAAY,WAAW;CAAQ;AACvD;AAEA,MAAM,WAAsB;CAAE,UAAU,KAAK;CAAe,WAAW;AAAQ;;AAG/E,MAAa,sBAAsB,EAAE,eAA0C;CAC9E,MAAM,EAAE,MAAMA,iBAAe;CAC7B,MAAM,OAAO,KAAK,OAAO,QAAQ,MAAM;CACvC,OACC,oBAAC,MAAD;EAAM,WAAW,KAAK;EAAW,MAAK;YACpC,EAAE,KAAK,QAAQ;CACX,CAAA;AAER"}
@@ -0,0 +1,6 @@
1
+ //#region src/delivery/RedeliverButton.d.ts
2
+ /** Doc-view action that POSTs to the deliveries redeliver endpoint. */
3
+ declare const RedeliverButton: () => import("react/jsx-runtime").JSX.Element | null;
4
+ //#endregion
5
+ export { RedeliverButton };
6
+ //# sourceMappingURL=RedeliverButton.d.ts.map
@@ -0,0 +1,43 @@
1
+ "use client";
2
+ import { keys } from "../translations/keys.js";
3
+ import { useTranslation as useTranslation$1 } from "../translations/useTranslation.js";
4
+ import { Button, toast, useConfig, useDocumentInfo } from "@payloadcms/ui";
5
+ import { jsx } from "react/jsx-runtime";
6
+ import { useState } from "react";
7
+ //#region src/delivery/RedeliverButton.tsx
8
+ /** Doc-view action that POSTs to the deliveries redeliver endpoint. */
9
+ const RedeliverButton = () => {
10
+ const { id, collectionSlug } = useDocumentInfo();
11
+ const { config } = useConfig();
12
+ const { t } = useTranslation$1();
13
+ const [busy, setBusy] = useState(false);
14
+ if (!id || !collectionSlug) return null;
15
+ const apiRoute = config.routes?.api ?? "/api";
16
+ const serverURL = config.serverURL ?? "";
17
+ const onClick = async () => {
18
+ setBusy(true);
19
+ try {
20
+ const res = await fetch(`${serverURL}${apiRoute}/${collectionSlug}/${encodeURIComponent(String(id))}/redeliver`, {
21
+ method: "POST",
22
+ credentials: "include"
23
+ });
24
+ if (!res.ok) throw new Error(String(res.status));
25
+ toast.success(t(keys.redeliverDone));
26
+ } catch {
27
+ toast.error(t(keys.redeliver));
28
+ } finally {
29
+ setBusy(false);
30
+ }
31
+ };
32
+ return /* @__PURE__ */ jsx(Button, {
33
+ buttonStyle: "secondary",
34
+ disabled: busy,
35
+ onClick,
36
+ size: "small",
37
+ children: t(keys.redeliver)
38
+ });
39
+ };
40
+ //#endregion
41
+ export { RedeliverButton };
42
+
43
+ //# sourceMappingURL=RedeliverButton.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RedeliverButton.js","names":["useTranslation"],"sources":["../../src/delivery/RedeliverButton.tsx"],"sourcesContent":["'use client'\n\nimport { Button, toast, useConfig, useDocumentInfo } from '@payloadcms/ui'\nimport { useState } from 'react'\n\nimport { keys } from '../translations/keys'\nimport { useTranslation } from '../translations/useTranslation'\n\n/** Doc-view action that POSTs to the deliveries redeliver endpoint. */\nexport const RedeliverButton = () => {\n\tconst { id, collectionSlug } = useDocumentInfo()\n\tconst { config } = useConfig()\n\tconst { t } = useTranslation()\n\tconst [busy, setBusy] = useState(false)\n\n\tif (!id || !collectionSlug) {\n\t\treturn null\n\t}\n\n\tconst apiRoute = config.routes?.api ?? '/api'\n\tconst serverURL = config.serverURL ?? ''\n\n\tconst onClick = async () => {\n\t\tsetBusy(true)\n\t\ttry {\n\t\t\tconst res = await fetch(\n\t\t\t\t`${serverURL}${apiRoute}/${collectionSlug}/${encodeURIComponent(String(id))}/redeliver`,\n\t\t\t\t{ method: 'POST', credentials: 'include' }\n\t\t\t)\n\t\t\tif (!res.ok) {\n\t\t\t\tthrow new Error(String(res.status))\n\t\t\t}\n\t\t\ttoast.success(t(keys.redeliverDone))\n\t\t} catch {\n\t\t\ttoast.error(t(keys.redeliver))\n\t\t} finally {\n\t\t\tsetBusy(false)\n\t\t}\n\t}\n\n\treturn (\n\t\t<Button buttonStyle=\"secondary\" disabled={busy} onClick={onClick} size=\"small\">\n\t\t\t{t(keys.redeliver)}\n\t\t</Button>\n\t)\n}\n"],"mappings":";;;;;;;;AASA,MAAa,wBAAwB;CACpC,MAAM,EAAE,IAAI,mBAAmB,gBAAgB;CAC/C,MAAM,EAAE,WAAW,UAAU;CAC7B,MAAM,EAAE,MAAMA,iBAAe;CAC7B,MAAM,CAAC,MAAM,WAAW,SAAS,KAAK;CAEtC,IAAI,CAAC,MAAM,CAAC,gBACX,OAAO;CAGR,MAAM,WAAW,OAAO,QAAQ,OAAO;CACvC,MAAM,YAAY,OAAO,aAAa;CAEtC,MAAM,UAAU,YAAY;EAC3B,QAAQ,IAAI;EACZ,IAAI;GACH,MAAM,MAAM,MAAM,MACjB,GAAG,YAAY,SAAS,GAAG,eAAe,GAAG,mBAAmB,OAAO,EAAE,CAAC,EAAE,aAC5E;IAAE,QAAQ;IAAQ,aAAa;GAAU,CAC1C;GACA,IAAI,CAAC,IAAI,IACR,MAAM,IAAI,MAAM,OAAO,IAAI,MAAM,CAAC;GAEnC,MAAM,QAAQ,EAAE,KAAK,aAAa,CAAC;EACpC,QAAQ;GACP,MAAM,MAAM,EAAE,KAAK,SAAS,CAAC;EAC9B,UAAU;GACT,QAAQ,KAAK;EACd;CACD;CAEA,OACC,oBAAC,QAAD;EAAQ,aAAY;EAAY,UAAU;EAAe;EAAS,MAAK;YACrE,EAAE,KAAK,SAAS;CACV,CAAA;AAEV"}
@@ -0,0 +1,26 @@
1
+ import { eventId } from "../events/eventTypes.js";
2
+ //#region src/delivery/buildPayload.ts
3
+ /** Build the outbound body, applying any per-collection transform/redaction. */
4
+ const buildPayload = (args) => {
5
+ const { deliveryId, collection, operation, doc, previousDoc, occurredAt, config, req } = args;
6
+ const data = config?.transform ? config.transform({
7
+ doc,
8
+ previousDoc,
9
+ operation,
10
+ req
11
+ }) : doc;
12
+ const body = {
13
+ id: deliveryId,
14
+ event: eventId(collection, operation),
15
+ collection,
16
+ operation,
17
+ occurredAt,
18
+ data
19
+ };
20
+ if (operation === "update" && config?.includePreviousData && previousDoc) body.previousData = previousDoc;
21
+ return body;
22
+ };
23
+ //#endregion
24
+ export { buildPayload };
25
+
26
+ //# sourceMappingURL=buildPayload.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"buildPayload.js","names":[],"sources":["../../src/delivery/buildPayload.ts"],"sourcesContent":["import type { PayloadRequest } from 'payload'\n\nimport { eventId } from '../events/eventTypes'\nimport type { CollectionWebhookConfig, WebhookOperation } from '../options'\n\n/** The JSON body POSTed to a subscriber. */\nexport type WebhookBody = {\n\tid: string\n\tevent: string\n\tcollection: string\n\toperation: WebhookOperation\n\toccurredAt: string\n\tdata: unknown\n\tpreviousData?: unknown\n}\n\n/** Build the outbound body, applying any per-collection transform/redaction. */\nexport const buildPayload = (args: {\n\tdeliveryId: string\n\tcollection: string\n\toperation: WebhookOperation\n\tdoc: Record<string, unknown>\n\tpreviousDoc?: Record<string, unknown>\n\toccurredAt: string\n\tconfig?: CollectionWebhookConfig\n\treq: PayloadRequest\n}): WebhookBody => {\n\tconst { deliveryId, collection, operation, doc, previousDoc, occurredAt, config, req } = args\n\tconst data = config?.transform ? config.transform({ doc, previousDoc, operation, req }) : doc\n\tconst body: WebhookBody = {\n\t\tid: deliveryId,\n\t\tevent: eventId(collection, operation),\n\t\tcollection,\n\t\toperation,\n\t\toccurredAt,\n\t\tdata,\n\t}\n\tif (operation === 'update' && config?.includePreviousData && previousDoc) {\n\t\tbody.previousData = previousDoc\n\t}\n\treturn body\n}\n"],"mappings":";;;AAiBA,MAAa,gBAAgB,SASV;CAClB,MAAM,EAAE,YAAY,YAAY,WAAW,KAAK,aAAa,YAAY,QAAQ,QAAQ;CACzF,MAAM,OAAO,QAAQ,YAAY,OAAO,UAAU;EAAE;EAAK;EAAa;EAAW;CAAI,CAAC,IAAI;CAC1F,MAAM,OAAoB;EACzB,IAAI;EACJ,OAAO,QAAQ,YAAY,SAAS;EACpC;EACA;EACA;EACA;CACD;CACA,IAAI,cAAc,YAAY,QAAQ,uBAAuB,aAC5D,KAAK,eAAe;CAErB,OAAO;AACR"}