GeezerVison is showing github.com — feat(core,sdk): webhooks.hookdeck() source · triggerdotdev/trigger.dev@51b39fb
File treeExpand file treeCollapse file tree Open diff view settings internal-packages/webhook-sources
Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboard +6Lines changed: 6 additions & 0 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
| | 1 | +--- | | 2 | +"@trigger.dev/core": patch | | 3 | +"@trigger.dev/sdk": patch | | 4 | +--- | | 5 | + | | 6 | +Add `webhooks.hookdeck()`, a source for deliveries relayed through a Hookdeck connection. It verifies the `x-hookdeck-signature` header with the project's signing secret, keeps verifying while the secret is being rolled (Hookdeck sends the previous secret's signature in `x-hookdeck-signature-2`), and deduplicates retries on `x-hookdeck-eventid`. The event is the original provider's payload, so pass its type: `webhooks.hookdeck<Stripe.Event>()`. If the Hookdeck project's header prefix was changed, pass it as `webhooks.hookdeck({ headerPrefix })` so retries keep deduplicating. |
Collapse file Copy file name to clipboard +68-37Lines changed: 68 additions & 37 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
| 1 | 1 | --- | 2 | 2 | title: "Trigger tasks from Hookdeck webhooks" | 3 | 3 | sidebarTitle: "Hookdeck webhooks" | 4 | | -description: "This example demonstrates how to use Hookdeck to receive webhooks and trigger Trigger.dev tasks." | | 4 | +description: "Forward webhooks from a Hookdeck connection to a hosted Trigger.dev endpoint, verified with the Hookdeck project's signing secret, and run a task for each event." | 5 | 5 | --- | 6 | 6 |
| 7 | | -## Overview | | 7 | +[Hookdeck](https://hookdeck.com) receives webhooks from external services, then queues, logs and forwards them to a destination. Point a Hookdeck connection at a hosted webhook endpoint and Trigger.dev verifies each forwarded request with the Hookdeck project's signing secret, deduplicates Hookdeck's retries, and runs your task with the original provider's payload. | 8 | 8 |
| 9 | | -This example shows how to use [Hookdeck](https://hookdeck.com) as your webhook infrastructure to trigger Trigger.dev tasks. Hookdeck receives webhooks from external services, and forwards them directly to the Trigger.dev API. This gives you the best of both worlds: Hookdeck's webhook management, logging, and replay capabilities, combined with Trigger.dev's reliable task execution. | | 9 | +## Task code | | 10 | + | | 11 | +Declare the endpoint with `webhooks.hookdeck()` and subscribe a task to it. Hookdeck forwards the original provider's body unchanged, so pass that provider's event type. This example relays Stripe events: | | 12 | + | | 13 | +```ts /trigger/stripe-via-hookdeck.ts | | 14 | +import type Stripe from "stripe"; | | 15 | +import { webhook, webhooks } from "@trigger.dev/sdk"; | | 16 | + | | 17 | +export const stripeViaHookdeck = webhooks.endpoint.define({ | | 18 | + id: "stripe-via-hookdeck", | | 19 | + source: webhooks.hookdeck<Stripe.Event>(), | | 20 | +}); | | 21 | + | | 22 | +export const paymentSucceeded = webhook({ | | 23 | + id: "payment-succeeded", | | 24 | + endpoint: stripeViaHookdeck, | | 25 | + filter: "event.type == 'payment_intent.succeeded'", | | 26 | + onEvent: async ({ event }) => { | | 27 | + console.log("Payment succeeded", event.id); | | 28 | + }, | | 29 | +}); | | 30 | +``` | 10 | 31 |
| 11 | | -## Key features | | 32 | +Run `trigger dev` or deploy, then copy the endpoint's URL from the Webhooks page in the dashboard. | 12 | 33 |
| 13 | | -- Use Hookdeck as your webhook endpoint for external services | 14 | | -- Hookdeck forwards webhooks directly to Trigger.dev tasks via the API | 15 | | -- All webhooks are logged and replayable in Hookdeck | | 34 | +## Set up Hookdeck | 16 | 35 |
| 17 | | -## Setting up Hookdeck | | 36 | +<Steps> | | 37 | + <Step title="Create a connection"> | | 38 | + In the [Hookdeck dashboard](https://dashboard.hookdeck.com), create a [connection](https://hookdeck.com/docs/connections) from your source (for example a Stripe source) to an HTTP destination whose URL is the endpoint's URL. Keep the destination's authentication on **Hookdeck Signature**, the default for new destinations: Hookdeck only signs forwarded requests with it set. | | 39 | + </Step> | | 40 | + <Step title="Store the signing secret"> | | 41 | + Copy the signing secret from **Settings > Secrets** in Hookdeck and set it as the endpoint's signing secret in the Trigger.dev dashboard. It's one secret for the whole Hookdeck project. | | 42 | + </Step> | | 43 | +</Steps> | 18 | 44 |
| 19 | | -You'll configure everything in the [Hookdeck dashboard](https://dashboard.hookdeck.com). No code changes needed in your app. | | 45 | +## Test it | 20 | 46 |
| 21 | | -### 1. Create a destination | | 47 | +Send a webhook to your Hookdeck source URL, or trigger a real event in the upstream provider. In Hookdeck, the event shows a `200` response from the destination. In Trigger.dev, the endpoint shows a succeeded delivery and the `payment-succeeded` task has a completed run with the Stripe event as its payload. | 22 | 48 |
| 23 | | -In Hookdeck, create a new [destination](https://hookdeck.com/docs/destinations) with the following settings: | | 49 | +A request signed with a different secret gets a `400`, which Hookdeck reports as a failed attempt. | 24 | 50 |
| 25 | | -- **URL**: `https://api.trigger.dev/api/v1/tasks/<task-id>/trigger` (replace `<task-id>` with your task ID) | 26 | | -- **Method**: POST | 27 | | -- **Authentication**: Bearer token (create a named **Trigger only** environment API key and use it as `TRIGGER_SECRET_KEY`) | | 51 | +## Rotate the signing secret | 28 | 52 |
| 29 | | -### 2. Add a transformation | | 53 | +Roll the signing secret under **Settings > Secrets** in Hookdeck with an expiry instead of expiring it immediately. Until the old secret expires, Hookdeck signs each request with both secrets, sending the previous secret's signature in `X-Hookdeck-Signature-2`. The endpoint accepts either, so deliveries keep verifying while you copy the new secret to Trigger.dev. Set a grace period when you store it to keep accepting the old secret for a while. | 30 | 54 |
| 31 | | -Create a [transformation](https://hookdeck.com/docs/transformations) to wrap the webhook body in the `payload` field that Trigger.dev expects: | | 55 | +## Deduplication | 32 | 56 |
| 33 | | -```javascript | 34 | | -addHandler("transform", (request, context) => { | 35 | | - request.body = { payload: { ...request.body } }; | 36 | | - return request; | | 57 | +Hookdeck retries and manual replays of the same event carry the same `X-Hookdeck-Eventid`, and the endpoint uses it as the [deduplication](/webhooks/deliveries#deduplication) key, so a retried event runs your task once. | | 58 | + | | 59 | +If you changed the header prefix in the Hookdeck project's settings, the event id arrives under the new prefix. Pass the prefix so deduplication keeps working: | | 60 | + | | 61 | +```ts /trigger/stripe-via-hookdeck.ts | | 62 | +import type Stripe from "stripe"; | | 63 | +import { webhooks } from "@trigger.dev/sdk"; | | 64 | + | | 65 | +export const stripeViaHookdeck = webhooks.endpoint.define({ | | 66 | + id: "stripe-via-hookdeck", | | 67 | + source: webhooks.hookdeck<Stripe.Event>({ headerPrefix: "x-acme" }), | 37 | 68 | }); | 38 | 69 | ``` | 39 | 70 |
| 40 | | -### 3. Create a connection | | 71 | +The signature headers keep the `X-Hookdeck-` name whatever the prefix. | 41 | 72 |
| 42 | | -Create a [connection](https://hookdeck.com/docs/connections) that links your source (where webhooks come from) to the destination and transformation you created above. | | 73 | +## Trigger a task through the API instead | 43 | 74 |
| 44 | | -## Task code | | 75 | +You can also point a Hookdeck destination at the [trigger task API](/management/tasks/trigger) without a hosted endpoint. Hookdeck authenticates with your API key instead of a signature, and a transformation wraps the body in the `payload` field the API expects. | | 76 | + | | 77 | +1. Create a destination with the URL `https://api.trigger.dev/api/v1/tasks/<task-id>/trigger`, method `POST`, and **Bearer token** authentication using a **Trigger only** environment API key. | | 78 | +2. Add a [transformation](https://hookdeck.com/docs/transformations): | 45 | 79 |
| 46 | | -This task will be triggered when Hookdeck forwards a webhook to the Trigger.dev API. | | 80 | + ```javascript | | 81 | + addHandler("transform", (request, context) => { | | 82 | + request.body = { payload: { ...request.body } }; | | 83 | + return request; | | 84 | + }); | | 85 | + ``` | 47 | 86 |
| 48 | | -```ts trigger/webhook-handler.ts | | 87 | +3. Create a connection from your source to that destination and transformation. | | 88 | + | | 89 | +```ts /trigger/webhook-handler.ts | 49 | 90 | import { task } from "@trigger.dev/sdk"; | 50 | 91 |
| 51 | 92 | export const webhookHandler = task({ | 52 | 93 | id: "webhook-handler", | 53 | 94 | run: async (payload: Record<string, unknown>) => { | 54 | | - // The payload contains the original webhook data from the external service | 55 | 95 | console.log("Received webhook:", payload); | 56 | | - | 57 | | - // Add your custom logic here | 58 | 96 | }, | 59 | 97 | }); | 60 | 98 | ``` | 61 | 99 |
| 62 | | -## Testing your setup | 63 | | - | 64 | | -To test everything is working: | 65 | | - | 66 | | -1. Set up your destination, transformation, and connection in [Hookdeck](https://dashboard.hookdeck.com) | 67 | | -2. Send a test webhook to your Hookdeck source URL (use the Hookdeck Console or cURL) | 68 | | -3. Check the Hookdeck dashboard to verify the webhook was received and forwarded | 69 | | -4. Check the [Trigger.dev dashboard](https://cloud.trigger.dev) to see the successful run of your task | | 100 | +This path has no deduplication of Hookdeck retries; each forwarded request triggers a run. | 70 | 101 |
| 71 | | -For more information on setting up Hookdeck, refer to the [Hookdeck Documentation](https://hookdeck.com/docs). | | 102 | +For more on Hookdeck, see the [Hookdeck documentation](https://hookdeck.com/docs). |
Collapse file Copy file name to clipboardExpand all lines: docs/webhooks/deliveries.mdx +1-1Lines changed: 1 addition & 1 deletion - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -85,7 +85,7 @@ When the source maps a provider event ID, that ID becomes the key, so the provid | 85 | 85 |
| 86 | 86 | | Key | Sources | | 87 | 87 | | --- | --- | | 88 | | -| A header | GitHub (`X-GitHub-Delivery`), Svix and Autumn (`svix-id`), Standard Webhooks, Loops and Granola (`webhook-id`), Linear (`Linear-Delivery`), PagerDuty (`X-Webhook-Id`), Jira (`X-Atlassian-Webhook-Identifier`), Zendesk (`X-Zendesk-Webhook-Invocation-Id`), Attio (`Idempotency-Key`), Shopify (`X-Shopify-Webhook-Id`) | | | 88 | +| A header | GitHub (`X-GitHub-Delivery`), Svix and Autumn (`svix-id`), Standard Webhooks, Loops and Granola (`webhook-id`), Linear (`Linear-Delivery`), PagerDuty (`X-Webhook-Id`), Jira (`X-Atlassian-Webhook-Identifier`), Zendesk (`X-Zendesk-Webhook-Invocation-Id`), Attio (`Idempotency-Key`), Shopify (`X-Shopify-Webhook-Id`), Hookdeck (`X-Hookdeck-Eventid`, or `<prefix>-eventid` with a custom header prefix) | | 89 | 89 | | A body field | Stripe, Notion, Vercel, Intercom, WorkOS, Metronome, Mux, Langfuse and Plain (`id`), Square, Slack and Typeform (`event_id`), Firecrawl (`webhookId`) | | 90 | 90 |
| 91 | 91 | The other named sources (Zoom, Calendly, Sentry, DocuSign, ElevenLabs, Vapi, Retell, Tailscale, Infisical, PlanetScale, Supabase, Discord) use the fallback below. | |
Collapse file Copy file name to clipboardExpand all lines: docs/webhooks/sources.mdx +30Lines changed: 30 additions & 0 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -97,6 +97,7 @@ Each function below returns a source with its provider's verification configurat | 97 | 97 | | HMAC-SHA256 with `X-Hub-Signature-256` | `whatsapp()` | | 98 | 98 | | Shared secret in `X-Gitlab-Token` | `gitlab()` | | 99 | 99 | | Shared secret as `Authorization: Bearer` | `supabase()` | | | 100 | +| Hookdeck relay (HMAC-SHA256 with `X-Hookdeck-Signature`) | `hookdeck()` | | 100 | 101 | | Provider-specific HMAC | `calendly()`, `calCom()`, `sentry()`, `pagerduty()`, `vercel()`, `linear()`, `notion()`, `typeform()`, `docusign()`, `jira()`, `intercom()`, `zendesk()`, `attio()`, `workos()`, `elevenlabs()`, `vapi()`, `retell()`, `shopify()`, `metronome()`, `mux()`, `tailscale()`, `langfuse()`, `infisical()`, `planetscale()`, `plain()`, `firecrawl()` | | 101 | 102 |
| 102 | 103 | Provider IDs containing a hyphen use a camel case function name: the `cal-com` provider is `webhooks.calCom()`. | @@ -105,6 +106,35 @@ Supabase database webhooks aren't signed, so `supabase()` checks a shared secret | 105 | 106 |
| 106 | 107 | Slack also ships as a chat channel in `@trigger.dev/slack`. | 107 | 108 |
| | 109 | +### Hookdeck | | 110 | + | | 111 | +`hookdeck()` verifies requests that a [Hookdeck](https://hookdeck.com) connection forwards to the endpoint. Hookdeck forwards the original provider's body unchanged, so pass that provider's event type: | | 112 | + | | 113 | +```ts /trigger/stripe-via-hookdeck.ts | | 114 | +import type Stripe from "stripe"; | | 115 | +import { webhook, webhooks } from "@trigger.dev/sdk"; | | 116 | + | | 117 | +export const stripeViaHookdeck = webhooks.endpoint.define({ | | 118 | + id: "stripe-via-hookdeck", | | 119 | + source: webhooks.hookdeck<Stripe.Event>(), | | 120 | +}); | | 121 | + | | 122 | +export const paymentSucceeded = webhook({ | | 123 | + id: "payment-succeeded", | | 124 | + endpoint: stripeViaHookdeck, | | 125 | + filter: "event.type == 'payment_intent.succeeded'", | | 126 | + onEvent: async ({ event }) => { | | 127 | + console.log(event.id); | | 128 | + }, | | 129 | +}); | | 130 | +``` | | 131 | + | | 132 | +Use the endpoint URL as the connection's HTTP destination, keep the destination's authentication on Hookdeck Signature (the default for new destinations), and store the Hookdeck project's signing secret from **Settings > Secrets** as the endpoint's signing secret. | | 133 | + | | 134 | +- **Secret rotation:** when you roll the signing secret in Hookdeck with an expiry, Hookdeck also signs each request with the previous secret in `X-Hookdeck-Signature-2`, so deliveries keep verifying until you store the new secret. | | 135 | +- **Deduplication:** retries dedupe on `X-Hookdeck-Eventid`, which is the same on every attempt of a Hookdeck event. | | 136 | +- **Custom header prefix:** if you changed the header prefix in the Hookdeck project's settings, pass it with `webhooks.hookdeck({ headerPrefix: "x-acme" })`. The prefix renames the event id header; the signature headers keep the `X-Hookdeck-` name. | | 137 | + | 108 | 138 | ## Endpoint validation | 109 | 139 |
| 110 | 140 | Some sources also define a response for the provider's endpoint-validation request. These requests don't create deliveries or task runs. | |
Collapse file Copy file name to clipboardExpand all lines: internal-packages/webhook-sources/catalog/providers.json +21Lines changed: 21 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 1119 | 1119 | "producer": true | 1120 | 1120 | }, | 1121 | 1121 | "notes": "" | | 1122 | + }, | | 1123 | + { | | 1124 | + "id": "hookdeck", | | 1125 | + "label": "Hookdeck", | | 1126 | + "category": "hosting-infra", | | 1127 | + "tier": "first-class", | | 1128 | + "preset": null, | | 1129 | + "signatureScheme": "hmac-sha256", | | 1130 | + "aiRelevance": "medium", | | 1131 | + "popularity": "medium", | | 1132 | + "sampleSource": "handauthored", | | 1133 | + "docsUrl": "https://hookdeck.com/docs/authentication", | | 1134 | + "status": "complete", | | 1135 | + "owner": "hookdeck-provider", | | 1136 | + "checklist": { | | 1137 | + "registryEntry": true, | | 1138 | + "samples": true, | | 1139 | + "roundTrip": true, | | 1140 | + "producer": true | | 1141 | + }, | | 1142 | + "notes": "relay: body is the upstream provider's payload; x-hookdeck-signature-2 carries the previous secret while a rotation is in progress" | 1122 | 1143 | } | 1123 | 1144 | ] | 1124 | 1145 | } |
Collapse file Copy file name to clipboard +67Lines changed: 67 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 1 | +import { type SampleRecord } from "../sampleRecord.js"; | | 2 | + | | 3 | +/** | | 4 | + * Hookdeck samples. Hookdeck forwards the original provider's body unchanged and adds its own | | 5 | + * headers, so each sample is an upstream payload plus informational `x-hookdeck-*` headers. The | | 6 | + * signature headers are added by the composer at send time. `x-hookdeck-eventid` is left out: it is | | 7 | + * the dedupe key, so a fixed value would fold every console send of a sample into one delivery. | | 8 | + */ | | 9 | +export const samples: SampleRecord[] = [ | | 10 | + { | | 11 | + provider: "hookdeck", | | 12 | + providerLabel: "Hookdeck", | | 13 | + eventType: "payment_intent.succeeded", | | 14 | + name: "Stripe payment relayed", | | 15 | + description: "A Stripe `payment_intent.succeeded` event forwarded by a Hookdeck connection.", | | 16 | + body: { | | 17 | + id: "evt_3QhRb2LkdIwHu7ix0Wq1X9aB", | | 18 | + object: "event", | | 19 | + type: "payment_intent.succeeded", | | 20 | + created: 1792569600, | | 21 | + livemode: false, | | 22 | + data: { | | 23 | + object: { | | 24 | + id: "pi_3QhRb2LkdIwHu7ix0kT2mN4p", | | 25 | + object: "payment_intent", | | 26 | + amount: 4900, | | 27 | + currency: "usd", | | 28 | + customer: "cus_R8xQ2mLkd91Hu7", | | 29 | + status: "succeeded", | | 30 | + }, | | 31 | + }, | | 32 | + }, | | 33 | + extraHeaders: { | | 34 | + "x-hookdeck-source-name": "stripe-production", | | 35 | + "x-hookdeck-attempt-count": "1", | | 36 | + "x-hookdeck-attempt-trigger": "INITIAL", | | 37 | + }, | | 38 | + docsUrl: "https://hookdeck.com/docs/destinations", | | 39 | + provenance: { kind: "handauthored", snapshotDate: "2026-10" }, | | 40 | + }, | | 41 | + { | | 42 | + provider: "hookdeck", | | 43 | + providerLabel: "Hookdeck", | | 44 | + eventType: "order.created", | | 45 | + name: "Order created relayed", | | 46 | + description: | | 47 | + "A custom app's `order.created` event forwarded by a Hookdeck connection, on a manual retry.", | | 48 | + body: { | | 49 | + type: "order.created", | | 50 | + id: "ord_7Kp2Qw9Lm3", | | 51 | + created_at: "2026-10-21T09:00:00.000Z", | | 52 | + data: { | | 53 | + total: 129.5, | | 54 | + currency: "EUR", | | 55 | + | | 56 | + items: [{ sku: "TEE-BLK-M", quantity: 2 }], | | 57 | + }, | | 58 | + }, | | 59 | + extraHeaders: { | | 60 | + "x-hookdeck-source-name": "shop-orders", | | 61 | + "x-hookdeck-attempt-count": "2", | | 62 | + "x-hookdeck-attempt-trigger": "MANUAL", | | 63 | + }, | | 64 | + docsUrl: "https://hookdeck.com/docs/destinations", | | 65 | + provenance: { kind: "handauthored", snapshotDate: "2026-10" }, | | 66 | + }, | | 67 | +]; |
Collapse file Copy file name to clipboardExpand all lines: internal-packages/webhook-sources/src/handAuthored/index.ts +2Lines changed: 2 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -17,6 +17,7 @@ import { samples as s_elevenlabs } from "./elevenlabs.js"; | 17 | 17 | import { samples as s_firecrawl } from "./firecrawl.js"; | 18 | 18 | import { samples as s_gitlab } from "./gitlab.js"; | 19 | 19 | import { samples as s_granola } from "./granola.js"; | | 20 | +import { samples as s_hookdeck } from "./hookdeck.js"; | 20 | 21 | import { samples as s_infisical } from "./infisical.js"; | 21 | 22 | import { samples as s_intercom } from "./intercom.js"; | 22 | 23 | import { samples as s_jira } from "./jira.js"; | @@ -70,6 +71,7 @@ export const handAuthoredSamples: SampleRecord[] = [ | 70 | 71 | ...s_firecrawl, | 71 | 72 | ...s_gitlab, | 72 | 73 | ...s_granola, | | 74 | + ...s_hookdeck, | 73 | 75 | ...s_infisical, | 74 | 76 | ...s_intercom, | 75 | 77 | ...s_jira, | |
Collapse file Copy file name to clipboardExpand all lines: internal-packages/webhook-sources/src/providerSignatures.test.ts +58Lines changed: 58 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -196,6 +196,30 @@ const fixtures: Fixture[] = [ | 196 | 196 | nowMs: NOW_MS, | 197 | 197 | toleranceSeconds: 300, | 198 | 198 | }, | | 199 | + { | | 200 | + provider: "hookdeck", | | 201 | + source: "https://hookdeck.com/docs/authentication", | | 202 | + body: '{"id":"evt_hookdeck_fixture","type":"payment_intent.succeeded","note":"Zoë"}', | | 203 | + secret: "hookdeck_fixture_secret", | | 204 | + headers: { | | 205 | + "x-hookdeck-signature": "3gqjNEFqbs5p4dNxA9fIzpEkE+YKzHRHZpA1gc9Z6QM=", | | 206 | + "x-hookdeck-eventid": "evt_hookdeck_fixture_delivery", | | 207 | + }, | | 208 | + nowMs: NOW_MS, | | 209 | + }, | | 210 | + { | | 211 | + provider: "hookdeck", | | 212 | + label: "hookdeck (secret rolled with a delay: old secret in x-hookdeck-signature-2)", | | 213 | + source: "https://hookdeck.com/docs/authentication", | | 214 | + body: '{"id":"evt_hookdeck_fixture","type":"payment_intent.succeeded","note":"Zoë"}', | | 215 | + secret: "hookdeck_fixture_secret", | | 216 | + headers: { | | 217 | + "x-hookdeck-signature": "MRtBJRv3K6j8+CUcDsJuwYudgKi52qz9yfISiM3FKCw=", | | 218 | + "x-hookdeck-signature-2": "3gqjNEFqbs5p4dNxA9fIzpEkE+YKzHRHZpA1gc9Z6QM=", | | 219 | + "x-hookdeck-eventid": "evt_hookdeck_fixture_delivery", | | 220 | + }, | | 221 | + nowMs: NOW_MS, | | 222 | + }, | 199 | 223 | { | 200 | 224 | provider: "supabase", | 201 | 225 | source: "https://supabase.com/docs/guides/database/webhooks", | @@ -250,3 +274,37 @@ describe("provider signature compatibility", () => { | 250 | 274 | }); | 251 | 275 | } | 252 | 276 | }); | | 277 | + | | 278 | +describe("hookdeck headerPrefix", () => { | | 279 | + const body = '{"id":"evt_hookdeck_fixture","type":"payment_intent.succeeded","note":"Zoë"}'; | | 280 | + const run = (headerPrefix: string | undefined, headers: Record<string, string>) => | | 281 | + verify( | | 282 | + { kind: "config", config: webhookProviderConfigs.hookdeck.config({ headerPrefix }) }, | | 283 | + { | | 284 | + rawBytes: new TextEncoder().encode(body), | | 285 | + secret: "hookdeck_fixture_secret", | | 286 | + nowMs: NOW_MS, | | 287 | + headers, | | 288 | + url: "https://example.com/webhooks/v1/ingest/hookdeck_fixture", | | 289 | + } | | 290 | + ); | | 291 | + const prefixed = { | | 292 | + "x-hookdeck-signature": "MRtBJRv3K6j8+CUcDsJuwYudgKi52qz9yfISiM3FKCw=", | | 293 | + "x-hookdeck-signature-2": "3gqjNEFqbs5p4dNxA9fIzpEkE+YKzHRHZpA1gc9Z6QM=", | | 294 | + "x-acme-eventid": "evt_acme_delivery", | | 295 | + "x-acme-requestid": "req_acme", | | 296 | + }; | | 297 | + | | 298 | + it("keeps verifying the x-hookdeck signature headers and dedupes on <prefix>-eventid", () => { | | 299 | + expect(run("X-Acme-", prefixed)).toMatchObject({ | | 300 | + ok: true, | | 301 | + idempotencyKey: "evt_acme_delivery", | | 302 | + }); | | 303 | + }); | | 304 | + | | 305 | + it("without the option, still verifies but cannot read the renamed event id", () => { | | 306 | + const verdict = run(undefined, prefixed); | | 307 | + expect(verdict.ok).toBe(true); | | 308 | + expect(verdict.idempotencyKey).not.toBe("evt_acme_delivery"); | | 309 | + }); | | 310 | +}); |
|