@voyant-travel/webhook-delivery 0.3.4 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,41 @@
1
+ import type { EventEnvelope } from "@voyant-travel/core";
2
+ import type { ExternalWebhookEventContract } from "./contracts.js";
3
+ export interface AppWebhookDeliveryEnvelope {
4
+ schema: "voyant.app-webhook.delivery.v1";
5
+ deliveryId: string;
6
+ installationId: string;
7
+ appId: string;
8
+ event: {
9
+ type: string;
10
+ schemaVersion: string;
11
+ occurredAt: string;
12
+ deliveredAt: string;
13
+ };
14
+ attempt: {
15
+ number: number;
16
+ maxRetries: number;
17
+ idempotencyKey: string;
18
+ originalDeliveryId: string | null;
19
+ parentDeliveryId: string | null;
20
+ };
21
+ subject: {
22
+ module: string | null;
23
+ id: string | null;
24
+ };
25
+ payload: unknown;
26
+ metadata: Record<string, unknown>;
27
+ }
28
+ export declare function createAppWebhookDeliveryEnvelope(input: {
29
+ deliveryId: string;
30
+ installationId: string;
31
+ appId: string;
32
+ event: EventEnvelope;
33
+ contract: ExternalWebhookEventContract;
34
+ deliveredAt: Date;
35
+ attemptNumber: number;
36
+ maxRetries: number;
37
+ idempotencyKey: string;
38
+ originalDeliveryId?: string | null;
39
+ parentDeliveryId?: string | null;
40
+ }): AppWebhookDeliveryEnvelope;
41
+ export declare function isAppWebhookDeliveryEnvelope(value: unknown): value is AppWebhookDeliveryEnvelope;
@@ -0,0 +1,54 @@
1
+ export function createAppWebhookDeliveryEnvelope(input) {
2
+ const subject = sourceEntity(input.event.data);
3
+ return {
4
+ schema: "voyant.app-webhook.delivery.v1",
5
+ deliveryId: input.deliveryId,
6
+ installationId: input.installationId,
7
+ appId: input.appId,
8
+ event: {
9
+ type: input.event.name,
10
+ schemaVersion: input.contract.eventVersion,
11
+ occurredAt: input.event.emittedAt,
12
+ deliveredAt: input.deliveredAt.toISOString(),
13
+ },
14
+ attempt: {
15
+ number: input.attemptNumber,
16
+ maxRetries: input.maxRetries,
17
+ idempotencyKey: input.idempotencyKey,
18
+ originalDeliveryId: input.originalDeliveryId ?? null,
19
+ parentDeliveryId: input.parentDeliveryId ?? null,
20
+ },
21
+ subject,
22
+ payload: input.event.data,
23
+ metadata: input.event.metadata ?? {},
24
+ };
25
+ }
26
+ export function isAppWebhookDeliveryEnvelope(value) {
27
+ return (isRecord(value) &&
28
+ value.schema === "voyant.app-webhook.delivery.v1" &&
29
+ typeof value.deliveryId === "string" &&
30
+ typeof value.installationId === "string" &&
31
+ typeof value.appId === "string" &&
32
+ isRecord(value.event) &&
33
+ typeof value.event.type === "string" &&
34
+ typeof value.event.schemaVersion === "string" &&
35
+ typeof value.event.occurredAt === "string" &&
36
+ typeof value.event.deliveredAt === "string" &&
37
+ isRecord(value.attempt) &&
38
+ typeof value.attempt.number === "number" &&
39
+ typeof value.attempt.idempotencyKey === "string" &&
40
+ "payload" in value);
41
+ }
42
+ function sourceEntity(data) {
43
+ if (!isRecord(data))
44
+ return { module: null, id: null };
45
+ const module = data.entityModule ?? data.entity_module;
46
+ const id = data.entityId ?? data.entity_id;
47
+ return {
48
+ module: typeof module === "string" ? module : null,
49
+ id: typeof id === "string" ? id : null,
50
+ };
51
+ }
52
+ function isRecord(value) {
53
+ return value !== null && typeof value === "object" && !Array.isArray(value);
54
+ }
package/dist/index.d.ts CHANGED
@@ -1,8 +1,11 @@
1
+ export type { AppWebhookDeliveryEnvelope } from "./app-envelope.js";
2
+ export { createAppWebhookDeliveryEnvelope, isAppWebhookDeliveryEnvelope } from "./app-envelope.js";
1
3
  export type { ExternalWebhookEventContract, WebhookSubscriptionEventInput, } from "./contracts.js";
2
4
  export { assertWebhookSubscriptionCreateEvents, assertWebhookSubscriptionUpdateEvents, prepareExternalWebhookEvent, } from "./contracts.js";
3
5
  export type { OutboundWebhookDeliveryEnqueuer, OutboundWebhookEnqueueProvider, ResolveOutboundWebhookDeliveryEnqueuerOptions, } from "./provider.js";
4
6
  export { resolveOutboundWebhookDeliveryEnqueuer } from "./provider.js";
5
- export { hashWebhookPayload, redactWebhookHeaders, signWebhookPayload, webhookBodyExcerpt, } from "./security.js";
7
+ export type { WebhookSigningKey } from "./security.js";
8
+ export { assertOutboundWebhookEndpointUrl, hashWebhookPayload, redactWebhookHeaders, signWebhookPayload, verifyWebhookPayloadSignature, webhookBodyExcerpt, } from "./security.js";
6
9
  export type { CreateSelectedExternalWebhookQueueOptions } from "./selected-queue.js";
7
10
  export { createSelectedExternalWebhookQueue, externalContractFromEventMetadata, } from "./selected-queue.js";
8
11
  export type { WebhookSubscriptionMutationStore, WebhookSubscriptionService, } from "./subscriptions.js";
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
+ export { createAppWebhookDeliveryEnvelope, isAppWebhookDeliveryEnvelope } from "./app-envelope.js";
1
2
  export { assertWebhookSubscriptionCreateEvents, assertWebhookSubscriptionUpdateEvents, prepareExternalWebhookEvent, } from "./contracts.js";
2
3
  export { resolveOutboundWebhookDeliveryEnqueuer } from "./provider.js";
3
- export { hashWebhookPayload, redactWebhookHeaders, signWebhookPayload, webhookBodyExcerpt, } from "./security.js";
4
+ export { assertOutboundWebhookEndpointUrl, hashWebhookPayload, redactWebhookHeaders, signWebhookPayload, verifyWebhookPayloadSignature, webhookBodyExcerpt, } from "./security.js";
4
5
  export { createSelectedExternalWebhookQueue, externalContractFromEventMetadata, } from "./selected-queue.js";
5
6
  export { createWebhookSubscriptionService } from "./subscriptions.js";
6
7
  export { createWebhookDeliveryWorker } from "./worker.js";
@@ -151,6 +151,7 @@ function toWebhookSubscription(row) {
151
151
  id: row.id,
152
152
  url: row.url,
153
153
  secret: row.secret,
154
+ keyId: null,
154
155
  headers: row.headers ?? null,
155
156
  maxRetries: row.maxRetries,
156
157
  active: row.active,
@@ -201,7 +202,7 @@ function requireSubscription(row, operation) {
201
202
  }
202
203
  function pendingAttemptValues(input) {
203
204
  return {
204
- id: newId("webhook_deliveries"),
205
+ id: input.id ?? newId("webhook_deliveries"),
205
206
  sourceModule: input.sourceModule,
206
207
  sourceEvent: input.sourceEvent,
207
208
  sourceEntityModule: input.sourceEntityModule,
@@ -1,4 +1,23 @@
1
1
  export declare function signWebhookPayload(secret: string, timestamp: string, body: string): string;
2
+ export interface WebhookSigningKey {
3
+ id: string;
4
+ secret: string;
5
+ }
6
+ export declare function verifyWebhookPayloadSignature(input: {
7
+ body: string;
8
+ timestamp: string;
9
+ signature: string;
10
+ keys: readonly WebhookSigningKey[];
11
+ now?: Date;
12
+ toleranceSeconds?: number;
13
+ }): {
14
+ ok: true;
15
+ keyId: string;
16
+ } | {
17
+ ok: false;
18
+ reason: string;
19
+ };
2
20
  export declare function hashWebhookPayload(body: string): string;
21
+ export declare function assertOutboundWebhookEndpointUrl(value: string): void;
3
22
  export declare function redactWebhookHeaders(headers: Record<string, string> | undefined): Record<string, string> | null;
4
23
  export declare function webhookBodyExcerpt(body: unknown, maxBytes?: number): string | null;
package/dist/security.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { createHash, createHmac } from "node:crypto";
2
+ import { isIP } from "node:net";
2
3
  const DEFAULT_EXCERPT_BYTES = 4 * 1024;
3
4
  const REDACTION_MARKER = "[REDACTED]";
4
5
  const REDACTED_HEADERS = new Set([
@@ -53,9 +54,45 @@ const PHONE_PATTERN = /\+?\d[\d\s().-]{6,}\d/g;
53
54
  export function signWebhookPayload(secret, timestamp, body) {
54
55
  return `sha256=${createHmac("sha256", secret).update(`${timestamp}.${body}`).digest("hex")}`;
55
56
  }
57
+ export function verifyWebhookPayloadSignature(input) {
58
+ const timestampSeconds = Number(input.timestamp);
59
+ if (!Number.isInteger(timestampSeconds))
60
+ return { ok: false, reason: "invalid_timestamp" };
61
+ const nowSeconds = Math.floor((input.now ?? new Date()).getTime() / 1_000);
62
+ const tolerance = input.toleranceSeconds ?? 300;
63
+ if (Math.abs(nowSeconds - timestampSeconds) > tolerance) {
64
+ return { ok: false, reason: "timestamp_outside_tolerance" };
65
+ }
66
+ for (const key of input.keys) {
67
+ if (signWebhookPayload(key.secret, input.timestamp, input.body) === input.signature) {
68
+ return { ok: true, keyId: key.id };
69
+ }
70
+ }
71
+ return { ok: false, reason: "signature_mismatch" };
72
+ }
56
73
  export function hashWebhookPayload(body) {
57
74
  return createHash("sha256").update(body).digest("hex");
58
75
  }
76
+ export function assertOutboundWebhookEndpointUrl(value) {
77
+ const url = new URL(value);
78
+ if (url.protocol !== "https:") {
79
+ throw new Error(`Webhook URL must use HTTPS: ${value}`);
80
+ }
81
+ const hostname = url.hostname.toLowerCase();
82
+ const address = hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
83
+ if (hostname === "localhost" ||
84
+ hostname.endsWith(".localhost") ||
85
+ hostname === "metadata.google.internal") {
86
+ throw new Error(`Webhook URL host is not allowed: ${hostname}`);
87
+ }
88
+ const ipVersion = isIP(address);
89
+ if (ipVersion === 4 && isPrivateIpv4(address)) {
90
+ throw new Error(`Webhook URL IP is not allowed: ${address}`);
91
+ }
92
+ if (ipVersion === 6 && isPrivateIpv6(address)) {
93
+ throw new Error(`Webhook URL IP is not allowed: ${address}`);
94
+ }
95
+ }
59
96
  export function redactWebhookHeaders(headers) {
60
97
  if (!headers)
61
98
  return null;
@@ -109,3 +146,20 @@ function redactValue(value) {
109
146
  function redactString(value) {
110
147
  return value.replace(EMAIL_PATTERN, REDACTION_MARKER).replace(PHONE_PATTERN, REDACTION_MARKER);
111
148
  }
149
+ function isPrivateIpv4(value) {
150
+ const [a = 0, b = 0] = value.split(".").map((part) => Number(part));
151
+ return (a === 10 ||
152
+ a === 127 ||
153
+ (a === 169 && b === 254) ||
154
+ (a === 172 && b >= 16 && b <= 31) ||
155
+ (a === 192 && b === 168) ||
156
+ a === 0);
157
+ }
158
+ function isPrivateIpv6(value) {
159
+ const normalized = value.toLowerCase();
160
+ return (normalized === "::1" ||
161
+ normalized === "::" ||
162
+ normalized.startsWith("fc") ||
163
+ normalized.startsWith("fd") ||
164
+ normalized.startsWith("fe80:"));
165
+ }
@@ -1,4 +1,6 @@
1
1
  import { isExternalWebhookPayloadSchema } from "@voyant-travel/core/project";
2
+ import { newId } from "@voyant-travel/db/lib/typeid";
3
+ import { createAppWebhookDeliveryEnvelope } from "./app-envelope.js";
2
4
  import { prepareExternalWebhookEvent } from "./contracts.js";
3
5
  import { hashWebhookPayload, redactWebhookHeaders, webhookBodyExcerpt } from "./security.js";
4
6
  export function createSelectedExternalWebhookQueue(options) {
@@ -12,11 +14,28 @@ export function createSelectedExternalWebhookQueue(options) {
12
14
  throw new Error(`Event "${input.name}" is not in the selected external catalog.`);
13
15
  }
14
16
  const event = prepareExternalWebhookEvent(input, contract);
15
- const body = JSON.stringify(event);
16
17
  const subscriptions = await options.store.listActiveSubscriptions(event.name);
17
- return Promise.all(subscriptions.map(async (subscription) => {
18
+ return Promise.all(subscriptions
19
+ .filter((subscription) => !subscription.app || subscription.app.eventVersion === contract.eventVersion)
20
+ .map(async (subscription) => {
21
+ const deliveryId = newId("webhook_deliveries");
18
22
  const idempotencyKey = `graph-webhook:${eventId}:${subscription.id}`;
23
+ const payload = subscription.app
24
+ ? createAppWebhookDeliveryEnvelope({
25
+ deliveryId,
26
+ installationId: subscription.app.installationId,
27
+ appId: subscription.app.appId,
28
+ event,
29
+ contract,
30
+ deliveredAt: now(),
31
+ attemptNumber: 1,
32
+ maxRetries: subscription.maxRetries,
33
+ idempotencyKey,
34
+ })
35
+ : event;
36
+ const body = JSON.stringify(payload);
19
37
  const enqueued = await options.store.enqueueAttempt({
38
+ id: deliveryId,
20
39
  sourceModule: stringMetadata(input, "graphEventSourceModule") ?? "graph-outbound-webhooks",
21
40
  sourceEvent: event.name,
22
41
  ...sourceEntity(event.data),
@@ -34,7 +53,7 @@ export function createSelectedExternalWebhookQueue(options) {
34
53
  }) ?? {},
35
54
  requestBodyHash: hashWebhookPayload(body),
36
55
  requestBodyExcerpt: webhookBodyExcerpt(body),
37
- requestPayload: event,
56
+ requestPayload: payload,
38
57
  deliveryContract: contract,
39
58
  attemptNumber: 1,
40
59
  parentDeliveryId: null,
package/dist/types.d.ts CHANGED
@@ -1,15 +1,23 @@
1
1
  import type { EventEnvelope } from "@voyant-travel/core";
2
2
  import type { InfraWebhookDelivery } from "@voyant-travel/db/schema/infra";
3
+ import type { AppWebhookDeliveryEnvelope } from "./app-envelope.js";
3
4
  import type { ExternalWebhookEventContract } from "./contracts.js";
4
5
  export interface WebhookSubscription {
5
6
  id: string;
6
7
  url: string;
7
8
  secret: string;
9
+ keyId?: string | null;
8
10
  headers: Record<string, string> | null;
9
11
  maxRetries: number;
10
12
  active: boolean;
13
+ app?: {
14
+ installationId: string;
15
+ appId: string;
16
+ eventVersion: string;
17
+ };
11
18
  }
12
19
  export interface EnqueueWebhookAttemptInput {
20
+ id?: string;
13
21
  sourceModule: string;
14
22
  sourceEvent: string;
15
23
  sourceEntityModule: string | null;
@@ -20,7 +28,7 @@ export interface EnqueueWebhookAttemptInput {
20
28
  requestHeaders: Record<string, string>;
21
29
  requestBodyHash: string;
22
30
  requestBodyExcerpt: string | null;
23
- requestPayload: EventEnvelope;
31
+ requestPayload: EventEnvelope | AppWebhookDeliveryEnvelope;
24
32
  deliveryContract: ExternalWebhookEventContract;
25
33
  attemptNumber: number;
26
34
  parentDeliveryId: string | null;
package/dist/worker.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import { isExternalWebhookPayloadSchema } from "@voyant-travel/core/project";
2
- import { hashWebhookPayload, redactWebhookHeaders, signWebhookPayload, webhookBodyExcerpt, } from "./security.js";
2
+ import { newId } from "@voyant-travel/db/lib/typeid";
3
+ import { createAppWebhookDeliveryEnvelope, isAppWebhookDeliveryEnvelope, } from "./app-envelope.js";
4
+ import { assertOutboundWebhookEndpointUrl, hashWebhookPayload, redactWebhookHeaders, signWebhookPayload, webhookBodyExcerpt, } from "./security.js";
3
5
  const DEFAULT_BASE_DELAY_MS = 1_000;
4
6
  const DEFAULT_MAX_DELAY_MS = 60_000;
5
7
  const DEFAULT_REQUEST_TIMEOUT_MS = 15_000;
@@ -45,7 +47,7 @@ export function createWebhookDeliveryWorker(options) {
45
47
  if (!subscription?.active) {
46
48
  return abandon(delivery, startedAt, "webhook subscription is unavailable or inactive");
47
49
  }
48
- const body = JSON.stringify(hydrated.event);
50
+ const body = JSON.stringify(hydrated.payload);
49
51
  if (hashWebhookPayload(body) !== delivery.requestBodyHash) {
50
52
  return abandon(delivery, startedAt, "persisted webhook payload hash does not match");
51
53
  }
@@ -67,7 +69,7 @@ export function createWebhookDeliveryWorker(options) {
67
69
  }
68
70
  const mayRetry = result.retryable && delivery.attemptNumber <= subscription.maxRetries;
69
71
  if (mayRetry) {
70
- const retry = retryInput(delivery, hydrated.event, hydrated.contract, subscription, body, finishedAt, baseDelayMs, maxDelayMs);
72
+ const retry = retryInput(delivery, hydrated.payload, hydrated.contract, subscription, finishedAt, baseDelayMs, maxDelayMs);
71
73
  const persisted = await options.store.completeAndEnqueueRetry({ ...completion, status: "failed" }, retry);
72
74
  const outcome = {
73
75
  status: "retry_scheduled",
@@ -114,16 +116,32 @@ export function createWebhookDeliveryWorker(options) {
114
116
  }
115
117
  }
116
118
  function hydrateAttempt(delivery) {
117
- if (!isEventEnvelope(delivery.requestPayload)) {
118
- return { ok: false, reason: "pending webhook delivery has no complete request payload" };
119
- }
120
119
  if (!isExternalContract(delivery.deliveryContract)) {
121
120
  return { ok: false, reason: "pending webhook delivery has no valid contract snapshot" };
122
121
  }
122
+ if (isAppWebhookDeliveryEnvelope(delivery.requestPayload)) {
123
+ if (delivery.requestPayload.event.type !== delivery.deliveryContract.eventType) {
124
+ return { ok: false, reason: "persisted webhook payload and contract event types differ" };
125
+ }
126
+ return {
127
+ ok: true,
128
+ event: eventFromAppEnvelope(delivery.requestPayload),
129
+ payload: delivery.requestPayload,
130
+ contract: delivery.deliveryContract,
131
+ };
132
+ }
133
+ if (!isEventEnvelope(delivery.requestPayload)) {
134
+ return { ok: false, reason: "pending webhook delivery has no complete request payload" };
135
+ }
123
136
  if (delivery.requestPayload.name !== delivery.deliveryContract.eventType) {
124
137
  return { ok: false, reason: "persisted webhook payload and contract event types differ" };
125
138
  }
126
- return { ok: true, event: delivery.requestPayload, contract: delivery.deliveryContract };
139
+ return {
140
+ ok: true,
141
+ event: delivery.requestPayload,
142
+ payload: delivery.requestPayload,
143
+ contract: delivery.deliveryContract,
144
+ };
127
145
  }
128
146
  function isEventEnvelope(value) {
129
147
  return (isRecord(value) &&
@@ -138,8 +156,16 @@ function isExternalContract(value) {
138
156
  typeof value.eventVersion === "string" &&
139
157
  isExternalWebhookPayloadSchema(value.payloadSchema));
140
158
  }
159
+ function eventFromAppEnvelope(envelope) {
160
+ return {
161
+ name: envelope.event.type,
162
+ data: envelope.payload,
163
+ emittedAt: envelope.event.occurredAt,
164
+ metadata: envelope.metadata,
165
+ };
166
+ }
141
167
  async function dispatch(fetchImpl, url, headers, body, timeoutMs) {
142
- assertWebhookUrl(url);
168
+ assertOutboundWebhookEndpointUrl(url);
143
169
  const controller = new AbortController();
144
170
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
145
171
  try {
@@ -186,9 +212,27 @@ async function dispatch(fetchImpl, url, headers, body, timeoutMs) {
186
212
  clearTimeout(timeout);
187
213
  }
188
214
  }
189
- function retryInput(delivery, event, contract, subscription, body, now, baseDelayMs, maxDelayMs) {
215
+ function retryInput(delivery, payload, contract, subscription, now, baseDelayMs, maxDelayMs) {
190
216
  const nextAttempt = delivery.attemptNumber + 1;
217
+ const deliveryId = newId("webhook_deliveries");
218
+ const retryPayload = isAppWebhookDeliveryEnvelope(payload)
219
+ ? createAppWebhookDeliveryEnvelope({
220
+ deliveryId,
221
+ installationId: payload.installationId,
222
+ appId: payload.appId,
223
+ event: eventFromAppEnvelope(payload),
224
+ contract,
225
+ deliveredAt: now,
226
+ attemptNumber: nextAttempt,
227
+ maxRetries: subscription.maxRetries,
228
+ idempotencyKey: delivery.idempotencyKey ?? delivery.id,
229
+ originalDeliveryId: payload.attempt.originalDeliveryId ?? payload.deliveryId,
230
+ parentDeliveryId: delivery.id,
231
+ })
232
+ : payload;
233
+ const body = JSON.stringify(retryPayload);
191
234
  return {
235
+ id: deliveryId,
192
236
  sourceModule: delivery.sourceModule,
193
237
  sourceEvent: delivery.sourceEvent,
194
238
  sourceEntityModule: delivery.sourceEntityModule,
@@ -199,7 +243,7 @@ function retryInput(delivery, event, contract, subscription, body, now, baseDela
199
243
  requestHeaders: delivery.requestHeaders ?? {},
200
244
  requestBodyHash: hashWebhookPayload(body),
201
245
  requestBodyExcerpt: webhookBodyExcerpt(body),
202
- requestPayload: event,
246
+ requestPayload: retryPayload,
203
247
  deliveryContract: contract,
204
248
  attemptNumber: nextAttempt,
205
249
  parentDeliveryId: delivery.id,
@@ -232,6 +276,13 @@ function signedHeaders(subscription, event, contract, idempotencyKey, timestamp,
232
276
  "x-voyant-event-contract": contract.eventId,
233
277
  "x-voyant-event-version": contract.eventVersion,
234
278
  "x-voyant-timestamp": timestamp,
279
+ ...(subscription.keyId ? { "x-voyant-key-id": subscription.keyId } : {}),
280
+ ...(subscription.app
281
+ ? {
282
+ "x-voyant-app-id": subscription.app.appId,
283
+ "x-voyant-installation-id": subscription.app.installationId,
284
+ }
285
+ : {}),
235
286
  "x-voyant-signature": signWebhookPayload(subscription.secret, timestamp, body),
236
287
  };
237
288
  }
@@ -302,12 +353,6 @@ function positiveInteger(value, fallback) {
302
353
  function boundedMessage(message) {
303
354
  return message.slice(0, MAX_ERROR_MESSAGE_LENGTH);
304
355
  }
305
- function assertWebhookUrl(value) {
306
- const url = new URL(value);
307
- if (url.protocol !== "https:" && url.protocol !== "http:") {
308
- throw new Error(`Webhook URL must use HTTP(S): ${value}`);
309
- }
310
- }
311
356
  function stringMetadata(event, key) {
312
357
  const value = event.metadata?.[key];
313
358
  return typeof value === "string" && value.length > 0 ? value : null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voyant-travel/webhook-delivery",
3
- "version": "0.3.4",
3
+ "version": "0.4.0",
4
4
  "description": "Durable, policy-aware outbound webhook delivery for Voyant Node deployments.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -20,7 +20,7 @@
20
20
  "dependencies": {
21
21
  "drizzle-orm": "^0.45.2",
22
22
  "@voyant-travel/core": "^0.125.0",
23
- "@voyant-travel/db": "^0.114.9"
23
+ "@voyant-travel/db": "^0.114.10"
24
24
  },
25
25
  "devDependencies": {
26
26
  "@types/node": "^25.5.2",