@adcp/sdk 14.0.0-beta.14 → 14.0.0-beta.15

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 (127) hide show
  1. package/dist/lib/auth/oauth/authorization-required.d.mts +25 -11
  2. package/dist/lib/auth/oauth/authorization-required.d.ts +25 -11
  3. package/dist/lib/auth/oauth/authorization-required.js +18 -4
  4. package/dist/lib/auth/oauth/authorization-required.mjs +17 -4
  5. package/dist/lib/auth/oauth/index.d.mts +1 -1
  6. package/dist/lib/auth/oauth/index.d.ts +1 -1
  7. package/dist/lib/auth/oauth/index.js +2 -0
  8. package/dist/lib/auth/oauth/index.mjs +2 -0
  9. package/dist/lib/auth/oauth/resource-url.d.mts +8 -0
  10. package/dist/lib/auth/oauth/resource-url.d.ts +8 -0
  11. package/dist/lib/auth/oauth/resource-url.js +42 -0
  12. package/dist/lib/auth/oauth/resource-url.mjs +40 -0
  13. package/dist/lib/core/ConversationTypes.d.mts +10 -0
  14. package/dist/lib/core/ConversationTypes.d.ts +10 -0
  15. package/dist/lib/core/SingleAgentClient.d.mts +5 -2
  16. package/dist/lib/core/SingleAgentClient.d.ts +5 -2
  17. package/dist/lib/core/SingleAgentClient.js +133 -28
  18. package/dist/lib/core/SingleAgentClient.mjs +138 -29
  19. package/dist/lib/core/TaskExecutor.d.mts +1 -0
  20. package/dist/lib/core/TaskExecutor.d.ts +1 -0
  21. package/dist/lib/core/TaskExecutor.js +7 -1
  22. package/dist/lib/core/TaskExecutor.mjs +7 -1
  23. package/dist/lib/core/webhook-registration.d.mts +8 -1
  24. package/dist/lib/core/webhook-registration.d.ts +8 -1
  25. package/dist/lib/core/webhook-registration.js +65 -5
  26. package/dist/lib/core/webhook-registration.mjs +61 -4
  27. package/dist/lib/errors/index.d.mts +9 -0
  28. package/dist/lib/errors/index.d.ts +9 -0
  29. package/dist/lib/errors/index.js +33 -0
  30. package/dist/lib/errors/index.mjs +32 -0
  31. package/dist/lib/index.d.mts +3 -2
  32. package/dist/lib/index.d.ts +3 -2
  33. package/dist/lib/index.js +4 -0
  34. package/dist/lib/index.mjs +4 -0
  35. package/dist/lib/media-buy/compatibility.js +14 -2
  36. package/dist/lib/media-buy/compatibility.mjs +14 -2
  37. package/dist/lib/protocols/index.js +23 -7
  38. package/dist/lib/protocols/index.mjs +24 -8
  39. package/dist/lib/protocols/mcp.js +8 -6
  40. package/dist/lib/protocols/mcp.mjs +9 -7
  41. package/dist/lib/schemas-data/v2.5/_provenance.json +1 -1
  42. package/dist/lib/server/decisioning/async-outcome.d.mts +11 -0
  43. package/dist/lib/server/decisioning/async-outcome.d.ts +11 -0
  44. package/dist/lib/server/decisioning/context.d.mts +11 -1
  45. package/dist/lib/server/decisioning/context.d.ts +11 -1
  46. package/dist/lib/server/decisioning/index.d.mts +3 -2
  47. package/dist/lib/server/decisioning/index.d.ts +3 -2
  48. package/dist/lib/server/decisioning/index.js +9 -0
  49. package/dist/lib/server/decisioning/index.mjs +10 -0
  50. package/dist/lib/server/decisioning/runtime/from-platform.d.mts +5 -2
  51. package/dist/lib/server/decisioning/runtime/from-platform.d.ts +5 -2
  52. package/dist/lib/server/decisioning/runtime/from-platform.js +40 -8
  53. package/dist/lib/server/decisioning/runtime/from-platform.mjs +40 -8
  54. package/dist/lib/server/decisioning/runtime/postgres-task-registry.d.mts +8 -1
  55. package/dist/lib/server/decisioning/runtime/postgres-task-registry.d.ts +8 -1
  56. package/dist/lib/server/decisioning/runtime/postgres-task-registry.js +10 -1
  57. package/dist/lib/server/decisioning/runtime/postgres-task-registry.mjs +9 -1
  58. package/dist/lib/server/decisioning/runtime/postgres-task-settlement.d.mts +79 -0
  59. package/dist/lib/server/decisioning/runtime/postgres-task-settlement.d.ts +79 -0
  60. package/dist/lib/server/decisioning/runtime/postgres-task-settlement.js +391 -0
  61. package/dist/lib/server/decisioning/runtime/postgres-task-settlement.mjs +369 -0
  62. package/dist/lib/server/decisioning/runtime/task-registry.d.mts +2 -0
  63. package/dist/lib/server/decisioning/runtime/task-registry.d.ts +2 -0
  64. package/dist/lib/server/decisioning/runtime/task-registry.js +2 -1
  65. package/dist/lib/server/decisioning/runtime/task-registry.mjs +2 -1
  66. package/dist/lib/server/decisioning/runtime/to-context.js +15 -11
  67. package/dist/lib/server/decisioning/runtime/to-context.mjs +15 -11
  68. package/dist/lib/server/index.d.mts +2 -2
  69. package/dist/lib/server/index.d.ts +2 -2
  70. package/dist/lib/server/index.js +4 -0
  71. package/dist/lib/server/index.mjs +4 -0
  72. package/dist/lib/server/webhook-delivery/index.d.mts +2 -2
  73. package/dist/lib/server/webhook-delivery/index.d.ts +2 -2
  74. package/dist/lib/server/webhook-delivery/index.js +4 -0
  75. package/dist/lib/server/webhook-delivery/index.mjs +5 -1
  76. package/dist/lib/server/webhook-delivery/pg.d.mts +3 -1
  77. package/dist/lib/server/webhook-delivery/pg.d.ts +3 -1
  78. package/dist/lib/server/webhook-delivery/pg.js +25 -2
  79. package/dist/lib/server/webhook-delivery/pg.mjs +25 -2
  80. package/dist/lib/server/webhook-delivery/recovery.d.mts +44 -0
  81. package/dist/lib/server/webhook-delivery/recovery.d.ts +44 -0
  82. package/dist/lib/server/webhook-delivery/recovery.js +146 -43
  83. package/dist/lib/server/webhook-delivery/recovery.mjs +144 -43
  84. package/dist/lib/signing/agent-resolver/index.d.mts +1 -1
  85. package/dist/lib/signing/agent-resolver/index.d.ts +1 -1
  86. package/dist/lib/signing/agent-resolver/resolve-agent.d.mts +9 -0
  87. package/dist/lib/signing/agent-resolver/resolve-agent.d.ts +9 -0
  88. package/dist/lib/signing/agent-resolver/resolved-agent-jwks.d.mts +4 -1
  89. package/dist/lib/signing/agent-resolver/resolved-agent-jwks.d.ts +4 -1
  90. package/dist/lib/signing/agent-resolver/resolved-agent-jwks.js +17 -4
  91. package/dist/lib/signing/agent-resolver/resolved-agent-jwks.mjs +17 -4
  92. package/dist/lib/signing/jwks.d.mts +7 -0
  93. package/dist/lib/signing/jwks.d.ts +7 -0
  94. package/dist/lib/signing/server.d.mts +2 -2
  95. package/dist/lib/signing/server.d.ts +2 -2
  96. package/dist/lib/signing/webhook-verifier.d.mts +6 -0
  97. package/dist/lib/signing/webhook-verifier.d.ts +6 -0
  98. package/dist/lib/signing/webhook-verifier.js +38 -4
  99. package/dist/lib/signing/webhook-verifier.mjs +38 -4
  100. package/dist/lib/testing/client.d.mts +1 -0
  101. package/dist/lib/testing/client.d.ts +1 -0
  102. package/dist/lib/testing/client.js +6 -2
  103. package/dist/lib/testing/client.mjs +6 -2
  104. package/dist/lib/testing/compliance/comply.d.mts +2 -2
  105. package/dist/lib/testing/compliance/comply.d.ts +2 -2
  106. package/dist/lib/testing/compliance/comply.js +41 -31
  107. package/dist/lib/testing/compliance/comply.mjs +44 -21
  108. package/dist/lib/testing/storyboard/oauth-metadata-graph/grader.d.mts +1 -7
  109. package/dist/lib/testing/storyboard/oauth-metadata-graph/grader.d.ts +1 -7
  110. package/dist/lib/testing/storyboard/oauth-metadata-graph/grader.js +6 -42
  111. package/dist/lib/testing/storyboard/oauth-metadata-graph/grader.mjs +10 -40
  112. package/dist/lib/testing/storyboard/validations.d.mts +1 -1
  113. package/dist/lib/testing/storyboard/validations.d.ts +1 -1
  114. package/dist/lib/utils/adcp-version-config.d.mts +2 -0
  115. package/dist/lib/utils/adcp-version-config.d.ts +2 -0
  116. package/dist/lib/utils/adcp-version-config.js +5 -0
  117. package/dist/lib/utils/adcp-version-config.mjs +4 -0
  118. package/dist/lib/version.d.mts +3 -3
  119. package/dist/lib/version.d.ts +3 -3
  120. package/dist/lib/version.js +3 -3
  121. package/dist/lib/version.mjs +3 -3
  122. package/docs/llms.txt +3 -1
  123. package/docs/migration-12-to-14.md +6 -5
  124. package/docs/migration-13-to-14.md +48 -7
  125. package/docs/migration-task-registry-scoping.md +164 -11
  126. package/package.json +1 -1
  127. package/skills/build-decisioning-platform/advanced/REFERENCE.md +1 -1
@@ -1,5 +1,5 @@
1
1
  import type { PgQueryable } from '../postgres-task-store';
2
- import type { WebhookDeliveryStore } from '../webhook-emitter';
2
+ import type { WebhookDeliveryKey, WebhookDeliveryStore } from '../webhook-emitter';
3
3
  import type { WebhookDeliveryRecoveryBackend } from './recovery';
4
4
  export interface PgWebhookDeliveryStoreOptions {
5
5
  /** Defaults to `adcp_webhook_deliveries`. */
@@ -12,6 +12,8 @@ export interface PgWebhookDeliveryRecoveryOptions {
12
12
  tableName?: string;
13
13
  /** Assert that the database/schema is dedicated to this deployment. */
14
14
  acknowledgeIsolatedDatabase?: boolean;
15
+ /** Restrict recovery leases to this trusted publisher/tenant namespace. */
16
+ claimScope?: Pick<WebhookDeliveryKey, 'publisherScope' | 'tenantScope'>;
15
17
  }
16
18
  export declare function getWebhookDeliveryMigration(options?: PgWebhookDeliveryStoreOptions): string;
17
19
  export declare const WEBHOOK_DELIVERY_MIGRATION: string;
@@ -89,6 +89,9 @@ CREATE TABLE IF NOT EXISTS ${table} (
89
89
  CONSTRAINT ${raw}_valid_disposition CHECK (disposition IS NULL OR disposition IN ('delivered', 'terminal'))
90
90
  );
91
91
 
92
+ ALTER TABLE ${table}
93
+ ADD COLUMN IF NOT EXISTS intent_fingerprint TEXT;
94
+
92
95
  CREATE INDEX IF NOT EXISTS idx_${raw}_pending
93
96
  ON ${table}(next_attempt_at, lease_expires_at) WHERE state = 'pending';
94
97
  `.trim();
@@ -169,6 +172,14 @@ function pgWebhookDeliveryRecoveryBackend(db, options = {}) {
169
172
  const tableName = options.tableName ?? DEFAULT_OUTBOX_TABLE;
170
173
  const table = (0, import_common.quoteWebhookTable)(tableName);
171
174
  assertPgDeploymentNamespace("pgWebhookDeliveryRecoveryBackend", tableName, DEFAULT_OUTBOX_TABLE, options);
175
+ if (options.claimScope) {
176
+ (0, import_common.assertDeliveryKey)({ ...options.claimScope, deliveryId: "claim-scope-validation" });
177
+ }
178
+ const assertConfiguredScope = (key) => {
179
+ if (options.claimScope && (key.publisherScope !== options.claimScope.publisherScope || key.tenantScope !== options.claimScope.tenantScope)) {
180
+ throw new Error("Webhook recovery key is outside the configured claim scope");
181
+ }
182
+ };
172
183
  async function query(operation, text, values) {
173
184
  try {
174
185
  return await db.query(text, values);
@@ -182,7 +193,7 @@ function pgWebhookDeliveryRecoveryBackend(db, options = {}) {
182
193
  try {
183
194
  await db.query(
184
195
  `SELECT publisher_scope, tenant_scope, delivery_id, snapshot, snapshot_fingerprint,
185
- storage_fingerprint, state, disposition, attempt_count, next_attempt_at, lease_owner,
196
+ storage_fingerprint, intent_fingerprint, state, disposition, attempt_count, next_attempt_at, lease_owner,
186
197
  lease_claim_id, lease_version, lease_expires_at FROM ${table} LIMIT 0`
187
198
  );
188
199
  } catch (cause) {
@@ -194,6 +205,7 @@ function pgWebhookDeliveryRecoveryBackend(db, options = {}) {
194
205
  },
195
206
  async checkpoint(key, snapshot, snapshotFingerprint, storageFingerprint, initialLease) {
196
207
  (0, import_common.assertDeliveryKey)(key);
208
+ assertConfiguredScope(key);
197
209
  (0, import_common.assertLeaseControls)(initialLease.ownerToken, initialLease.leaseMs);
198
210
  const leaseClaimId = (0, import_node_crypto.randomUUID)();
199
211
  const result = await query(
@@ -259,6 +271,7 @@ function pgWebhookDeliveryRecoveryBackend(db, options = {}) {
259
271
  },
260
272
  async settle(key, disposition) {
261
273
  (0, import_common.assertDeliveryKey)(key);
274
+ assertConfiguredScope(key);
262
275
  (0, import_common.assertWebhookDisposition)(disposition);
263
276
  await query(
264
277
  "settle",
@@ -271,12 +284,14 @@ function pgWebhookDeliveryRecoveryBackend(db, options = {}) {
271
284
  },
272
285
  async claimPending({ ownerToken, leaseMs, limit }) {
273
286
  (0, import_common.assertLeaseControls)(ownerToken, leaseMs, limit);
287
+ const scopePredicate = options.claimScope ? "AND publisher_scope = $4 AND tenant_scope = $5" : "";
274
288
  const result = await query(
275
289
  "claimPending",
276
290
  `WITH candidates AS (
277
291
  SELECT publisher_scope, tenant_scope, delivery_id
278
292
  FROM ${table}
279
293
  WHERE state = 'pending'
294
+ ${scopePredicate}
280
295
  AND next_attempt_at <= clock_timestamp()
281
296
  AND (lease_expires_at IS NULL OR lease_expires_at < clock_timestamp())
282
297
  ORDER BY next_attempt_at, created_at
@@ -299,12 +314,18 @@ function pgWebhookDeliveryRecoveryBackend(db, options = {}) {
299
314
  FLOOR(EXTRACT(EPOCH FROM outbox.next_attempt_at) * 1000)::bigint AS next_attempt_at_ms,
300
315
  outbox.lease_owner, outbox.lease_version,
301
316
  FLOOR(EXTRACT(EPOCH FROM outbox.lease_expires_at) * 1000)::bigint AS lease_expires_at_ms`,
302
- [ownerToken, leaseMs, limit]
317
+ [
318
+ ownerToken,
319
+ leaseMs,
320
+ limit,
321
+ ...options.claimScope ? [options.claimScope.publisherScope, options.claimScope.tenantScope] : []
322
+ ]
303
323
  );
304
324
  return result.rows.map(rowToRecoveryRecord);
305
325
  },
306
326
  async renew(lease, leaseMs) {
307
327
  (0, import_common.assertDeliveryKey)(lease.key);
328
+ assertConfiguredScope(lease.key);
308
329
  (0, import_common.assertLeaseControls)(lease.leaseOwner, leaseMs);
309
330
  const result = await query(
310
331
  "renew",
@@ -326,6 +347,7 @@ function pgWebhookDeliveryRecoveryBackend(db, options = {}) {
326
347
  },
327
348
  async release(lease, retryAfterMs) {
328
349
  (0, import_common.assertDeliveryKey)(lease.key);
350
+ assertConfiguredScope(lease.key);
329
351
  (0, import_common.assertLeaseControls)(lease.leaseOwner, 1);
330
352
  (0, import_common.assertRetryAfterMs)(retryAfterMs);
331
353
  const result = await query(
@@ -349,6 +371,7 @@ function pgWebhookDeliveryRecoveryBackend(db, options = {}) {
349
371
  },
350
372
  async settleLease(lease, disposition) {
351
373
  (0, import_common.assertDeliveryKey)(lease.key);
374
+ assertConfiguredScope(lease.key);
352
375
  (0, import_common.assertLeaseControls)(lease.leaseOwner, 1);
353
376
  (0, import_common.assertWebhookDisposition)(disposition);
354
377
  const result = await query(
@@ -70,6 +70,9 @@ CREATE TABLE IF NOT EXISTS ${table} (
70
70
  CONSTRAINT ${raw}_valid_disposition CHECK (disposition IS NULL OR disposition IN ('delivered', 'terminal'))
71
71
  );
72
72
 
73
+ ALTER TABLE ${table}
74
+ ADD COLUMN IF NOT EXISTS intent_fingerprint TEXT;
75
+
73
76
  CREATE INDEX IF NOT EXISTS idx_${raw}_pending
74
77
  ON ${table}(next_attempt_at, lease_expires_at) WHERE state = 'pending';
75
78
  `.trim();
@@ -150,6 +153,14 @@ function pgWebhookDeliveryRecoveryBackend(db, options = {}) {
150
153
  const tableName = options.tableName ?? DEFAULT_OUTBOX_TABLE;
151
154
  const table = quoteWebhookTable(tableName);
152
155
  assertPgDeploymentNamespace("pgWebhookDeliveryRecoveryBackend", tableName, DEFAULT_OUTBOX_TABLE, options);
156
+ if (options.claimScope) {
157
+ assertDeliveryKey({ ...options.claimScope, deliveryId: "claim-scope-validation" });
158
+ }
159
+ const assertConfiguredScope = (key) => {
160
+ if (options.claimScope && (key.publisherScope !== options.claimScope.publisherScope || key.tenantScope !== options.claimScope.tenantScope)) {
161
+ throw new Error("Webhook recovery key is outside the configured claim scope");
162
+ }
163
+ };
153
164
  async function query(operation, text, values) {
154
165
  try {
155
166
  return await db.query(text, values);
@@ -163,7 +174,7 @@ function pgWebhookDeliveryRecoveryBackend(db, options = {}) {
163
174
  try {
164
175
  await db.query(
165
176
  `SELECT publisher_scope, tenant_scope, delivery_id, snapshot, snapshot_fingerprint,
166
- storage_fingerprint, state, disposition, attempt_count, next_attempt_at, lease_owner,
177
+ storage_fingerprint, intent_fingerprint, state, disposition, attempt_count, next_attempt_at, lease_owner,
167
178
  lease_claim_id, lease_version, lease_expires_at FROM ${table} LIMIT 0`
168
179
  );
169
180
  } catch (cause) {
@@ -175,6 +186,7 @@ function pgWebhookDeliveryRecoveryBackend(db, options = {}) {
175
186
  },
176
187
  async checkpoint(key, snapshot, snapshotFingerprint, storageFingerprint, initialLease) {
177
188
  assertDeliveryKey(key);
189
+ assertConfiguredScope(key);
178
190
  assertLeaseControls(initialLease.ownerToken, initialLease.leaseMs);
179
191
  const leaseClaimId = randomUUID();
180
192
  const result = await query(
@@ -240,6 +252,7 @@ function pgWebhookDeliveryRecoveryBackend(db, options = {}) {
240
252
  },
241
253
  async settle(key, disposition) {
242
254
  assertDeliveryKey(key);
255
+ assertConfiguredScope(key);
243
256
  assertWebhookDisposition(disposition);
244
257
  await query(
245
258
  "settle",
@@ -252,12 +265,14 @@ function pgWebhookDeliveryRecoveryBackend(db, options = {}) {
252
265
  },
253
266
  async claimPending({ ownerToken, leaseMs, limit }) {
254
267
  assertLeaseControls(ownerToken, leaseMs, limit);
268
+ const scopePredicate = options.claimScope ? "AND publisher_scope = $4 AND tenant_scope = $5" : "";
255
269
  const result = await query(
256
270
  "claimPending",
257
271
  `WITH candidates AS (
258
272
  SELECT publisher_scope, tenant_scope, delivery_id
259
273
  FROM ${table}
260
274
  WHERE state = 'pending'
275
+ ${scopePredicate}
261
276
  AND next_attempt_at <= clock_timestamp()
262
277
  AND (lease_expires_at IS NULL OR lease_expires_at < clock_timestamp())
263
278
  ORDER BY next_attempt_at, created_at
@@ -280,12 +295,18 @@ function pgWebhookDeliveryRecoveryBackend(db, options = {}) {
280
295
  FLOOR(EXTRACT(EPOCH FROM outbox.next_attempt_at) * 1000)::bigint AS next_attempt_at_ms,
281
296
  outbox.lease_owner, outbox.lease_version,
282
297
  FLOOR(EXTRACT(EPOCH FROM outbox.lease_expires_at) * 1000)::bigint AS lease_expires_at_ms`,
283
- [ownerToken, leaseMs, limit]
298
+ [
299
+ ownerToken,
300
+ leaseMs,
301
+ limit,
302
+ ...options.claimScope ? [options.claimScope.publisherScope, options.claimScope.tenantScope] : []
303
+ ]
284
304
  );
285
305
  return result.rows.map(rowToRecoveryRecord);
286
306
  },
287
307
  async renew(lease, leaseMs) {
288
308
  assertDeliveryKey(lease.key);
309
+ assertConfiguredScope(lease.key);
289
310
  assertLeaseControls(lease.leaseOwner, leaseMs);
290
311
  const result = await query(
291
312
  "renew",
@@ -307,6 +328,7 @@ function pgWebhookDeliveryRecoveryBackend(db, options = {}) {
307
328
  },
308
329
  async release(lease, retryAfterMs) {
309
330
  assertDeliveryKey(lease.key);
331
+ assertConfiguredScope(lease.key);
310
332
  assertLeaseControls(lease.leaseOwner, 1);
311
333
  assertRetryAfterMs(retryAfterMs);
312
334
  const result = await query(
@@ -330,6 +352,7 @@ function pgWebhookDeliveryRecoveryBackend(db, options = {}) {
330
352
  },
331
353
  async settleLease(lease, disposition) {
332
354
  assertDeliveryKey(lease.key);
355
+ assertConfiguredScope(lease.key);
333
356
  assertLeaseControls(lease.leaseOwner, 1);
334
357
  assertWebhookDisposition(disposition);
335
358
  const result = await query(
@@ -10,9 +10,23 @@ export interface WebhookAuthenticationAdapter {
10
10
  protect(authentication: Exclude<WebhookAuthentication, null>, context: Readonly<WebhookAuthenticationContext>): Promise<ProtectedWebhookAuthentication> | ProtectedWebhookAuthentication;
11
11
  resolve(protectedValue: unknown, context: Readonly<WebhookAuthenticationContext>): Promise<WebhookAuthentication> | WebhookAuthentication;
12
12
  }
13
+ /** Retryable failure while calling an application's KMS/secret adapter. */
14
+ export declare class WebhookAuthenticationProtectionError extends Error {
15
+ readonly name = "WebhookAuthenticationProtectionError";
16
+ }
17
+ /** Retryable failure while resolving protected authentication for delivery. */
18
+ export declare class WebhookAuthenticationResolutionError extends Error {
19
+ readonly name = "WebhookAuthenticationResolutionError";
20
+ }
13
21
  export interface WebhookAuthenticationContext {
14
22
  key: WebhookDeliveryKey;
15
23
  url: string;
24
+ /**
25
+ * Domain-separates task payload validation tokens. Undefined is the
26
+ * original transport-authentication context and is intentionally preserved
27
+ * so pending encrypted snapshots remain decryptable across SDK upgrades.
28
+ */
29
+ purpose?: 'payload_token';
16
30
  snapshotContextFingerprint: string;
17
31
  }
18
32
  export interface StoredWebhookDeliverySnapshot {
@@ -25,8 +39,32 @@ export interface StoredWebhookDeliverySnapshot {
25
39
  protectedValue: unknown;
26
40
  fingerprint: string;
27
41
  };
42
+ /**
43
+ * A top-level task-webhook `token`, protected independently from transport
44
+ * authentication. The cleartext is restored only after a recovery lease is
45
+ * claimed and is never written to the outbox JSON payload.
46
+ */
47
+ payloadToken?: {
48
+ kind: 'protected';
49
+ protectedValue: unknown;
50
+ fingerprint: string;
51
+ };
28
52
  retries: WebhookDeliverySnapshot['retries'];
29
53
  }
54
+ /** Storage-ready snapshot used by transactional outbox coordinators. */
55
+ export interface PreparedWebhookDeliverySnapshot {
56
+ snapshot: StoredWebhookDeliverySnapshot;
57
+ snapshotFingerprint: string;
58
+ storageFingerprint: string;
59
+ }
60
+ export interface PrepareWebhookDeliveryOptions {
61
+ /**
62
+ * Protect and remove the top-level `payload.token` before persistence.
63
+ * This is opt-in because generic webhook payloads may legitimately use a
64
+ * non-secret field with that name.
65
+ */
66
+ protectPayloadToken?: boolean;
67
+ }
30
68
  export interface WebhookRecoveryRecord {
31
69
  key: WebhookDeliveryKey;
32
70
  snapshot: StoredWebhookDeliverySnapshot;
@@ -71,6 +109,12 @@ export interface WebhookDeliveryRecoveryBackend {
71
109
  }
72
110
  export interface DurableWebhookDeliveryRecovery extends WebhookDeliveryRecovery {
73
111
  probe(): Promise<void>;
112
+ /**
113
+ * Protect and fingerprint a snapshot without writing it. This is reserved
114
+ * for coordinators that insert the returned value in the same database
115
+ * transaction as their domain mutation.
116
+ */
117
+ prepare(key: Readonly<WebhookDeliveryKey>, snapshot: Readonly<WebhookDeliverySnapshot>, options?: Readonly<PrepareWebhookDeliveryOptions>): Promise<PreparedWebhookDeliverySnapshot>;
74
118
  claimPending(options: {
75
119
  ownerToken?: string;
76
120
  leaseMs?: number;
@@ -10,9 +10,23 @@ export interface WebhookAuthenticationAdapter {
10
10
  protect(authentication: Exclude<WebhookAuthentication, null>, context: Readonly<WebhookAuthenticationContext>): Promise<ProtectedWebhookAuthentication> | ProtectedWebhookAuthentication;
11
11
  resolve(protectedValue: unknown, context: Readonly<WebhookAuthenticationContext>): Promise<WebhookAuthentication> | WebhookAuthentication;
12
12
  }
13
+ /** Retryable failure while calling an application's KMS/secret adapter. */
14
+ export declare class WebhookAuthenticationProtectionError extends Error {
15
+ readonly name = "WebhookAuthenticationProtectionError";
16
+ }
17
+ /** Retryable failure while resolving protected authentication for delivery. */
18
+ export declare class WebhookAuthenticationResolutionError extends Error {
19
+ readonly name = "WebhookAuthenticationResolutionError";
20
+ }
13
21
  export interface WebhookAuthenticationContext {
14
22
  key: WebhookDeliveryKey;
15
23
  url: string;
24
+ /**
25
+ * Domain-separates task payload validation tokens. Undefined is the
26
+ * original transport-authentication context and is intentionally preserved
27
+ * so pending encrypted snapshots remain decryptable across SDK upgrades.
28
+ */
29
+ purpose?: 'payload_token';
16
30
  snapshotContextFingerprint: string;
17
31
  }
18
32
  export interface StoredWebhookDeliverySnapshot {
@@ -25,8 +39,32 @@ export interface StoredWebhookDeliverySnapshot {
25
39
  protectedValue: unknown;
26
40
  fingerprint: string;
27
41
  };
42
+ /**
43
+ * A top-level task-webhook `token`, protected independently from transport
44
+ * authentication. The cleartext is restored only after a recovery lease is
45
+ * claimed and is never written to the outbox JSON payload.
46
+ */
47
+ payloadToken?: {
48
+ kind: 'protected';
49
+ protectedValue: unknown;
50
+ fingerprint: string;
51
+ };
28
52
  retries: WebhookDeliverySnapshot['retries'];
29
53
  }
54
+ /** Storage-ready snapshot used by transactional outbox coordinators. */
55
+ export interface PreparedWebhookDeliverySnapshot {
56
+ snapshot: StoredWebhookDeliverySnapshot;
57
+ snapshotFingerprint: string;
58
+ storageFingerprint: string;
59
+ }
60
+ export interface PrepareWebhookDeliveryOptions {
61
+ /**
62
+ * Protect and remove the top-level `payload.token` before persistence.
63
+ * This is opt-in because generic webhook payloads may legitimately use a
64
+ * non-secret field with that name.
65
+ */
66
+ protectPayloadToken?: boolean;
67
+ }
30
68
  export interface WebhookRecoveryRecord {
31
69
  key: WebhookDeliveryKey;
32
70
  snapshot: StoredWebhookDeliverySnapshot;
@@ -71,6 +109,12 @@ export interface WebhookDeliveryRecoveryBackend {
71
109
  }
72
110
  export interface DurableWebhookDeliveryRecovery extends WebhookDeliveryRecovery {
73
111
  probe(): Promise<void>;
112
+ /**
113
+ * Protect and fingerprint a snapshot without writing it. This is reserved
114
+ * for coordinators that insert the returned value in the same database
115
+ * transaction as their domain mutation.
116
+ */
117
+ prepare(key: Readonly<WebhookDeliveryKey>, snapshot: Readonly<WebhookDeliverySnapshot>, options?: Readonly<PrepareWebhookDeliveryOptions>): Promise<PreparedWebhookDeliverySnapshot>;
74
118
  claimPending(options: {
75
119
  ownerToken?: string;
76
120
  leaseMs?: number;
@@ -18,6 +18,8 @@ var __copyProps = (to, from, except, desc) => {
18
18
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
19
  var recovery_exports = {};
20
20
  __export(recovery_exports, {
21
+ WebhookAuthenticationProtectionError: () => WebhookAuthenticationProtectionError,
22
+ WebhookAuthenticationResolutionError: () => WebhookAuthenticationResolutionError,
21
23
  createWebhookDeliveryRecovery: () => createWebhookDeliveryRecovery,
22
24
  memoryWebhookDeliveryRecoveryBackend: () => memoryWebhookDeliveryRecoveryBackend,
23
25
  pollWebhookDeliveryRecovery: () => pollWebhookDeliveryRecovery
@@ -26,6 +28,12 @@ module.exports = __toCommonJS(recovery_exports);
26
28
  var import_node_crypto = require("node:crypto");
27
29
  var import_jcs = require('../../utils/jcs.js');
28
30
  var import_common = require('./common.js');
31
+ class WebhookAuthenticationProtectionError extends Error {
32
+ name = "WebhookAuthenticationProtectionError";
33
+ }
34
+ class WebhookAuthenticationResolutionError extends Error {
35
+ name = "WebhookAuthenticationResolutionError";
36
+ }
29
37
  function memoryWebhookDeliveryRecoveryBackend(options = {}) {
30
38
  const records = /* @__PURE__ */ new Map();
31
39
  const now = options.now ?? Date.now;
@@ -135,13 +143,25 @@ function createWebhookDeliveryRecovery(options) {
135
143
  return {
136
144
  durability: "durable",
137
145
  probe: () => options.backend.probe(),
146
+ async prepare(key, snapshot, prepareOptions) {
147
+ (0, import_common.assertDeliveryKey)(key);
148
+ const stored = await protectSnapshot(
149
+ snapshot,
150
+ key,
151
+ options.authenticationAdapter,
152
+ prepareOptions?.protectPayloadToken === true
153
+ );
154
+ assertStoredSnapshotSize(stored, maxSnapshotBytes);
155
+ return {
156
+ snapshot: stored,
157
+ snapshotFingerprint: snapshotFingerprint(stored),
158
+ storageFingerprint: (0, import_jcs.canonicalJsonSha256)(stored)
159
+ };
160
+ },
138
161
  async checkpoint(key, snapshot) {
139
162
  (0, import_common.assertDeliveryKey)(key);
140
- const stored = await protectSnapshot(snapshot, key, options.authenticationAdapter);
141
- const storedBytes = Buffer.byteLength(JSON.stringify(stored));
142
- if (storedBytes > maxSnapshotBytes) {
143
- throw new TypeError(`Webhook recovery snapshot exceeds maxSnapshotBytes (${maxSnapshotBytes})`);
144
- }
163
+ const stored = await protectSnapshot(snapshot, key, options.authenticationAdapter, false);
164
+ assertStoredSnapshotSize(stored, maxSnapshotBytes);
145
165
  const fingerprint = snapshotFingerprint(stored);
146
166
  const storageFingerprint = (0, import_jcs.canonicalJsonSha256)(stored);
147
167
  const ownerToken = (0, import_node_crypto.randomUUID)();
@@ -313,59 +333,101 @@ function assertPollOutcome(outcome) {
313
333
  throw new TypeError("Webhook recovery deliver() returned an invalid disposition");
314
334
  }
315
335
  }
316
- async function protectSnapshot(snapshot, key, adapter) {
336
+ async function protectSnapshot(snapshot, key, adapter, protectPayloadToken) {
337
+ const payload = structuredClone(snapshot.payload);
338
+ const cleartextPayloadToken = protectPayloadToken && typeof payload.token === "string" ? payload.token : void 0;
339
+ if (cleartextPayloadToken !== void 0) delete payload.token;
317
340
  const base = {
318
341
  url: snapshot.url,
319
- payload: structuredClone(snapshot.payload),
342
+ payload,
320
343
  retries: { ...snapshot.retries }
321
344
  };
322
345
  (0, import_jcs.canonicalJsonSha256)(base);
323
- if (snapshot.authentication === null) return { ...base, authentication: { kind: "none" } };
324
- if (!adapter) {
346
+ if ((snapshot.authentication !== null || cleartextPayloadToken !== void 0) && !adapter) {
325
347
  throw new TypeError(
326
- "Durable webhook recovery requires authenticationAdapter when bearer or HMAC authentication is present"
348
+ "Durable webhook recovery requires authenticationAdapter when bearer/HMAC authentication or a payload token is present"
327
349
  );
328
350
  }
329
- const context = authenticationContext(key, base);
330
- const protectedAuth = await adapter.protect(structuredClone(snapshot.authentication), context);
331
- if (!protectedAuth || typeof protectedAuth.fingerprint !== "string" || protectedAuth.fingerprint.length < 16) {
332
- throw new TypeError(
333
- "WebhookAuthenticationAdapter.protect() must return a stable fingerprint of at least 16 characters"
334
- );
335
- }
336
- (0, import_jcs.canonicalJsonSha256)(protectedAuth.protectedValue);
337
- return {
351
+ const stored = {
338
352
  ...base,
339
- authentication: {
340
- kind: "protected",
341
- protectedValue: structuredClone(protectedAuth.protectedValue),
342
- fingerprint: protectedAuth.fingerprint
343
- }
353
+ authentication: { kind: "none" }
344
354
  };
355
+ if (snapshot.authentication !== null) {
356
+ const protectedAuth = await protectAuthentication(
357
+ adapter,
358
+ structuredClone(snapshot.authentication),
359
+ authenticationContext(key, base)
360
+ );
361
+ stored.authentication = { kind: "protected", ...protectedAuth };
362
+ }
363
+ if (cleartextPayloadToken !== void 0) {
364
+ const protectedToken = await protectAuthentication(
365
+ adapter,
366
+ { type: "bearer", token: cleartextPayloadToken },
367
+ authenticationContext(key, base, "payload_token")
368
+ );
369
+ stored.payloadToken = { kind: "protected", ...protectedToken };
370
+ }
371
+ return stored;
345
372
  }
346
373
  async function resolveSnapshot(snapshot, key, adapter) {
347
- let authentication = null;
348
- if (snapshot.authentication.kind === "protected") {
349
- if (!adapter) throw new Error("Cannot recover protected webhook authentication without authenticationAdapter");
350
- authentication = await adapter.resolve(
351
- structuredClone(snapshot.authentication.protectedValue),
352
- authenticationContext(key, snapshot)
353
- );
354
- if (authentication === null)
355
- throw new Error("WebhookAuthenticationAdapter.resolve() returned null for protected state");
374
+ try {
375
+ let authentication = null;
376
+ if (snapshot.authentication.kind === "protected") {
377
+ if (!adapter) throw new Error("Cannot recover protected webhook authentication without authenticationAdapter");
378
+ authentication = await adapter.resolve(
379
+ structuredClone(snapshot.authentication.protectedValue),
380
+ authenticationContext(key, snapshot)
381
+ );
382
+ if (authentication === null)
383
+ throw new Error("WebhookAuthenticationAdapter.resolve() returned null for protected state");
384
+ }
385
+ const payload = structuredClone(snapshot.payload);
386
+ if (snapshot.payloadToken) {
387
+ if (!adapter) throw new Error("Cannot recover protected webhook payload token without authenticationAdapter");
388
+ const resolved = await adapter.resolve(
389
+ structuredClone(snapshot.payloadToken.protectedValue),
390
+ authenticationContext(key, snapshot, "payload_token")
391
+ );
392
+ if (resolved?.type !== "bearer" || typeof resolved.token !== "string") {
393
+ throw new Error("WebhookAuthenticationAdapter.resolve() returned an invalid protected payload token");
394
+ }
395
+ payload.token = resolved.token;
396
+ }
397
+ return {
398
+ url: snapshot.url,
399
+ payload,
400
+ authentication,
401
+ retries: { ...snapshot.retries }
402
+ };
403
+ } catch (cause) {
404
+ if (cause instanceof WebhookAuthenticationResolutionError) throw cause;
405
+ throw new WebhookAuthenticationResolutionError("Webhook authentication resolution failed", { cause });
356
406
  }
357
- return {
358
- url: snapshot.url,
359
- payload: structuredClone(snapshot.payload),
360
- authentication,
361
- retries: { ...snapshot.retries }
362
- };
363
407
  }
364
- function authenticationContext(key, snapshot) {
365
- return {
408
+ function authenticationContext(key, snapshot, purpose) {
409
+ const base = {
366
410
  key: { ...key },
367
- url: snapshot.url,
411
+ url: snapshot.url
412
+ };
413
+ if (purpose === void 0) {
414
+ return {
415
+ ...base,
416
+ // This is the exact pre-payload-token context hash. Do not change it:
417
+ // applications may bind KMS AAD to this value for pending snapshots.
418
+ snapshotContextFingerprint: (0, import_jcs.canonicalJsonSha256)({
419
+ key,
420
+ url: snapshot.url,
421
+ payload: snapshot.payload,
422
+ retries: snapshot.retries
423
+ })
424
+ };
425
+ }
426
+ return {
427
+ ...base,
428
+ purpose,
368
429
  snapshotContextFingerprint: (0, import_jcs.canonicalJsonSha256)({
430
+ purpose,
369
431
  key,
370
432
  url: snapshot.url,
371
433
  payload: snapshot.payload,
@@ -421,9 +483,48 @@ function snapshotFingerprint(snapshot) {
421
483
  url: snapshot.url,
422
484
  payload: snapshot.payload,
423
485
  retries: snapshot.retries,
424
- authentication: snapshot.authentication.kind === "none" ? { kind: "none" } : { kind: "protected", fingerprint: snapshot.authentication.fingerprint }
486
+ authentication: snapshot.authentication.kind === "none" ? { kind: "none" } : { kind: "protected", fingerprint: snapshot.authentication.fingerprint },
487
+ ...snapshot.payloadToken && { payloadToken: { fingerprint: snapshot.payloadToken.fingerprint } }
425
488
  });
426
489
  }
490
+ async function protectAuthentication(adapter, authentication, context) {
491
+ let protectedAuth;
492
+ try {
493
+ protectedAuth = await adapter.protect(authentication, context);
494
+ } catch (cause) {
495
+ throw new WebhookAuthenticationProtectionError("Webhook authentication protection failed", { cause });
496
+ }
497
+ if (!protectedAuth || typeof protectedAuth.fingerprint !== "string" || protectedAuth.fingerprint.length < 16) {
498
+ throw new TypeError(
499
+ "WebhookAuthenticationAdapter.protect() must return a stable fingerprint of at least 16 characters"
500
+ );
501
+ }
502
+ (0, import_jcs.canonicalJsonSha256)(protectedAuth.protectedValue);
503
+ const cleartext = authentication.type === "bearer" ? authentication.token : authentication.secret;
504
+ if (protectedValueContainsSecret(protectedAuth.protectedValue, cleartext) || protectedAuth.fingerprint.includes(cleartext)) {
505
+ throw new TypeError("WebhookAuthenticationAdapter.protect() returned durable state containing cleartext");
506
+ }
507
+ return {
508
+ protectedValue: structuredClone(protectedAuth.protectedValue),
509
+ fingerprint: protectedAuth.fingerprint
510
+ };
511
+ }
512
+ function protectedValueContainsSecret(value, cleartext) {
513
+ if (typeof value === "string") return value.includes(cleartext);
514
+ if (Array.isArray(value)) return value.some((item) => protectedValueContainsSecret(item, cleartext));
515
+ if (value && typeof value === "object") {
516
+ return Object.entries(value).some(
517
+ ([key, item]) => key.includes(cleartext) || protectedValueContainsSecret(item, cleartext)
518
+ );
519
+ }
520
+ return false;
521
+ }
522
+ function assertStoredSnapshotSize(snapshot, maxSnapshotBytes) {
523
+ const storedBytes = Buffer.byteLength(JSON.stringify(snapshot));
524
+ if (storedBytes > maxSnapshotBytes) {
525
+ throw new TypeError(`Webhook recovery snapshot exceeds maxSnapshotBytes (${maxSnapshotBytes})`);
526
+ }
527
+ }
427
528
  function assertLeaseMs(value) {
428
529
  if (!Number.isSafeInteger(value) || value < 1e3 || value > 36e5) {
429
530
  throw new TypeError("leaseMs must be an integer from 1000 through 3600000");
@@ -431,6 +532,8 @@ function assertLeaseMs(value) {
431
532
  }
432
533
  // Annotate the CommonJS export names for ESM import in node:
433
534
  0 && (module.exports = {
535
+ WebhookAuthenticationProtectionError,
536
+ WebhookAuthenticationResolutionError,
434
537
  createWebhookDeliveryRecovery,
435
538
  memoryWebhookDeliveryRecoveryBackend,
436
539
  pollWebhookDeliveryRecovery