deepline 0.3.113 → 0.3.115

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 (50) hide show
  1. package/dist/bundling-sources/sdk/src/client.ts +3 -1
  2. package/dist/bundling-sources/sdk/src/compat.ts +167 -11
  3. package/dist/bundling-sources/sdk/src/release.ts +1 -1
  4. package/dist/bundling-sources/sdk/src/skills-version.ts +27 -1
  5. package/dist/bundling-sources/shared_libs/monitors/inbound-queue.ts +6 -2
  6. package/dist/bundling-sources/shared_libs/observability/dlq.ts +942 -0
  7. package/dist/bundling-sources/shared_libs/observability/queue-health.ts +361 -6
  8. package/dist/bundling-sources/shared_libs/observability/scheduled-jobs.json +24 -24
  9. package/dist/bundling-sources/shared_libs/observability/scheduled-work-manual.ts +2 -0
  10. package/dist/bundling-sources/shared_libs/observability/scheduled-work.ts +29 -1
  11. package/dist/bundling-sources/shared_libs/play-runtime/admission-transport-policy.ts +91 -0
  12. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +2 -0
  13. package/dist/bundling-sources/shared_libs/play-runtime/direct-admission-deadline.ts +34 -0
  14. package/dist/bundling-sources/shared_libs/play-runtime/fixture-behavior.ts +1 -0
  15. package/dist/bundling-sources/shared_libs/play-runtime/internal-bootstrap-token.ts +325 -0
  16. package/dist/bundling-sources/shared_libs/play-runtime/play-admission-outcome.ts +134 -0
  17. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/admission/contracts.ts +13 -0
  18. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/admission/index.ts +4 -1
  19. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/admission/postgres-adapter.ts +13 -0
  20. package/dist/bundling-sources/shared_libs/play-runtime/play-webhook-admission-probe.ts +32 -0
  21. package/dist/bundling-sources/shared_libs/play-runtime/play-webhook-identity.ts +28 -0
  22. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backend.ts +7 -0
  23. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-cron-fire-task.ts +100 -0
  24. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-runtime-worker.ts +313 -0
  25. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-shared.ts +111 -3
  26. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd.ts +16 -0
  27. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-pool.ts +67 -8
  28. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +1734 -147
  29. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/run-ledger-outbox-recovery.ts +74 -13
  30. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-queue-health.ts +245 -81
  31. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/submit-admission.ts +79 -15
  32. package/dist/bundling-sources/shared_libs/play-runtime/webhook-receipt-retry.ts +21 -21
  33. package/dist/bundling-sources/shared_libs/plays/bundling/index.ts +4 -1
  34. package/dist/bundling-sources/shared_libs/product-notifications/catalog.ts +64 -0
  35. package/dist/bundling-sources/shared_libs/product-notifications/default-policy.ts +1 -1
  36. package/dist/bundling-sources/shared_libs/product-notifications/events.ts +20 -1
  37. package/dist/bundling-sources/shared_libs/security/receipt-admission-token.ts +224 -0
  38. package/dist/cli/index.js +574 -114
  39. package/dist/cli/index.mjs +592 -133
  40. package/dist/index.d.mts +54 -1
  41. package/dist/index.d.ts +54 -1
  42. package/dist/index.js +8 -3
  43. package/dist/index.mjs +8 -3
  44. package/dist/install-integrity.json +8 -0
  45. package/dist/plays/bundle-play-file.mjs +1 -1
  46. package/dist/release.d.mts +1 -1
  47. package/dist/release.d.ts +1 -1
  48. package/dist/release.js +1 -1
  49. package/dist/release.mjs +1 -1
  50. package/package.json +1 -1
@@ -456,6 +456,7 @@ export function isReplaySafeFixtureToolId(toolId: string): boolean {
456
456
  return (
457
457
  shouldRouteFixtureToolId(normalized) &&
458
458
  normalized !== 'ai_inference' &&
459
+ normalized !== 'ai_evaluate' &&
459
460
  normalized !== 'deeplineagent' &&
460
461
  !normalized.startsWith('deeplineagent_')
461
462
  );
@@ -10,6 +10,13 @@ import {
10
10
  type FixtureBehavior,
11
11
  } from './/fixture-behavior';
12
12
  import { normalizeRuntimeSchedulerSchema } from './/runtime-scheduler-topology';
13
+ import {
14
+ mintPlayWebhookReceiptLargeBatchAdmissionToken as mintPlayWebhookReceiptLargeBatchAdmissionTokenShared,
15
+ mintPlayTriggerEventAdmissionToken as mintPlayTriggerEventAdmissionTokenShared,
16
+ mintPlayWebhookReceiptBatchAdmissionToken as mintPlayWebhookReceiptBatchAdmissionTokenShared,
17
+ mintPlayWebhookReceiptAdmissionToken as mintPlayWebhookReceiptAdmissionTokenShared,
18
+ mintSignalRadarInboundReceiptToken as mintSignalRadarInboundReceiptTokenShared,
19
+ } from '../security/receipt-admission-token';
13
20
  import { SignJWT, exportJWK, importJWK, jwtVerify, type JWK } from 'jose';
14
21
  import {
15
22
  directCompletedTerminalContract,
@@ -79,6 +86,24 @@ const PRODUCT_NOTIFICATION_DELIVERY_AUDIENCE =
79
86
  'deepline.product_notification.delivery' as const;
80
87
  const PRODUCT_NOTIFICATION_DELIVERY_PURPOSE =
81
88
  'product_notification_delivery' as const;
89
+ const PLAY_WEBHOOK_RECEIPT_ADMISSION_AUDIENCE =
90
+ 'deepline.play_webhook_receipt.admission' as const;
91
+ const PLAY_WEBHOOK_RECEIPT_ADMISSION_PURPOSE =
92
+ 'play_webhook_receipt_admission' as const;
93
+ const PLAY_WEBHOOK_RECEIPT_BATCH_ADMISSION_AUDIENCE =
94
+ 'deepline.play_webhook_receipt.batch_admission' as const;
95
+ const PLAY_WEBHOOK_RECEIPT_BATCH_ADMISSION_PURPOSE =
96
+ 'play_webhook_receipt_batch_admission' as const;
97
+ const PLAY_WEBHOOK_RECEIPT_LARGE_BATCH_ADMISSION_PURPOSE =
98
+ 'play_webhook_receipt_large_batch_admission' as const;
99
+ const PLAY_TRIGGER_EVENT_ADMISSION_AUDIENCE =
100
+ 'deepline.play_trigger_event.admission' as const;
101
+ const PLAY_TRIGGER_EVENT_ADMISSION_PURPOSE =
102
+ 'play_trigger_event_admission' as const;
103
+ const SIGNAL_RADAR_INBOUND_RECEIPT_AUDIENCE =
104
+ 'deepline.signal_radar_inbound_receipt.process' as const;
105
+ const SIGNAL_RADAR_INBOUND_RECEIPT_PURPOSE =
106
+ 'signal_radar_inbound_receipt_process' as const;
82
107
  const PRODUCT_NOTIFICATION_UNSUBSCRIBE_AUDIENCE =
83
108
  'deepline.product_notification.unsubscribe' as const;
84
109
  const PRODUCT_NOTIFICATION_UNSUBSCRIBE_PURPOSE =
@@ -146,6 +171,67 @@ export type ProductNotificationDeliveryTokenClaims = {
146
171
  v: typeof INTERNAL_BOOTSTRAP_TOKEN_VERSION;
147
172
  };
148
173
 
174
+ /** Capability for exactly one Convex-owned webhook receipt admission. */
175
+ export type PlayWebhookReceiptAdmissionTokenClaims = {
176
+ aud: typeof PLAY_WEBHOOK_RECEIPT_ADMISSION_AUDIENCE;
177
+ exp: number;
178
+ iat: number;
179
+ iss: typeof INTERNAL_BOOTSTRAP_TOKEN_ISSUER;
180
+ org_id: string;
181
+ purpose: typeof PLAY_WEBHOOK_RECEIPT_ADMISSION_PURPOSE;
182
+ receipt_id: string;
183
+ sub: 'play_webhook_receipt_dispatcher';
184
+ v: typeof INTERNAL_BOOTSTRAP_TOKEN_VERSION;
185
+ };
186
+
187
+ /** Capability for a bounded, single-workspace collection of receipt IDs. */
188
+ type PlayWebhookReceiptBatchAdmissionTokenBaseClaims = {
189
+ aud: typeof PLAY_WEBHOOK_RECEIPT_BATCH_ADMISSION_AUDIENCE;
190
+ exp: number;
191
+ iat: number;
192
+ iss: typeof INTERNAL_BOOTSTRAP_TOKEN_ISSUER;
193
+ org_id: string;
194
+ v: typeof INTERNAL_BOOTSTRAP_TOKEN_VERSION;
195
+ };
196
+
197
+ export type PlayWebhookReceiptBatchAdmissionTokenClaims =
198
+ | (PlayWebhookReceiptBatchAdmissionTokenBaseClaims & {
199
+ purpose: typeof PLAY_WEBHOOK_RECEIPT_BATCH_ADMISSION_PURPOSE;
200
+ receipt_ids: string[];
201
+ sub: 'play_webhook_receipt_batch_dispatcher';
202
+ })
203
+ | (PlayWebhookReceiptBatchAdmissionTokenBaseClaims & {
204
+ purpose: typeof PLAY_WEBHOOK_RECEIPT_LARGE_BATCH_ADMISSION_PURPOSE;
205
+ receipt_ids_sha256: string;
206
+ sub: 'play_webhook_receipt_large_batch_dispatcher';
207
+ });
208
+
209
+ /** Capability for exactly one Convex-owned cron trigger-event admission. */
210
+ export type PlayTriggerEventAdmissionTokenClaims = {
211
+ aud: typeof PLAY_TRIGGER_EVENT_ADMISSION_AUDIENCE;
212
+ event_id: string;
213
+ exp: number;
214
+ iat: number;
215
+ iss: typeof INTERNAL_BOOTSTRAP_TOKEN_ISSUER;
216
+ org_id: string;
217
+ purpose: typeof PLAY_TRIGGER_EVENT_ADMISSION_PURPOSE;
218
+ sub: 'play_trigger_event_dispatcher';
219
+ v: typeof INTERNAL_BOOTSTRAP_TOKEN_VERSION;
220
+ };
221
+
222
+ /** Capability for one Convex-owned monitor inbound receipt processor call. */
223
+ export type SignalRadarInboundReceiptTokenClaims = {
224
+ aud: typeof SIGNAL_RADAR_INBOUND_RECEIPT_AUDIENCE;
225
+ exp: number;
226
+ iat: number;
227
+ iss: typeof INTERNAL_BOOTSTRAP_TOKEN_ISSUER;
228
+ org_id: string;
229
+ purpose: typeof SIGNAL_RADAR_INBOUND_RECEIPT_PURPOSE;
230
+ receipt_id: string;
231
+ sub: 'signal_radar_inbound_receipt_dispatcher';
232
+ v: typeof INTERNAL_BOOTSTRAP_TOKEN_VERSION;
233
+ };
234
+
149
235
  export type ProductNotificationUnsubscribeTokenClaims = {
150
236
  aud: typeof PRODUCT_NOTIFICATION_UNSUBSCRIBE_AUDIENCE;
151
237
  group_key: string;
@@ -1045,6 +1131,245 @@ export async function verifyProductNotificationDeliveryToken(
1045
1131
  return claims as ProductNotificationDeliveryTokenClaims;
1046
1132
  }
1047
1133
 
1134
+ export async function mintPlayWebhookReceiptAdmissionToken(input: {
1135
+ orgId: string;
1136
+ receiptId: string;
1137
+ nowMs?: number;
1138
+ }): Promise<string> {
1139
+ return await mintPlayWebhookReceiptAdmissionTokenShared(input);
1140
+ }
1141
+
1142
+ export async function mintPlayWebhookReceiptBatchAdmissionToken(input: {
1143
+ orgId: string;
1144
+ receiptIds: readonly string[];
1145
+ nowMs?: number;
1146
+ }): Promise<string> {
1147
+ return await mintPlayWebhookReceiptBatchAdmissionTokenShared(input);
1148
+ }
1149
+
1150
+ export async function mintPlayWebhookReceiptLargeBatchAdmissionToken(input: {
1151
+ orgId: string;
1152
+ receiptIds: readonly string[];
1153
+ nowMs?: number;
1154
+ }): Promise<string> {
1155
+ return await mintPlayWebhookReceiptLargeBatchAdmissionTokenShared(input);
1156
+ }
1157
+
1158
+ export async function verifyPlayWebhookReceiptBatchAdmissionToken(
1159
+ token: string,
1160
+ nowMs = Date.now(),
1161
+ ): Promise<PlayWebhookReceiptBatchAdmissionTokenClaims> {
1162
+ const segments = token.split('.');
1163
+ if (segments.length !== 3)
1164
+ throw new Error('Invalid webhook receipt batch token.');
1165
+ const [encodedHeader, encodedClaims, signature] = segments;
1166
+ const header = decodeJson<Record<string, unknown>>(encodedHeader);
1167
+ if (header.alg !== 'HS256' || header.typ !== 'JWT') {
1168
+ throw new Error('Invalid webhook receipt batch token.');
1169
+ }
1170
+ const expectedSignature = await signTokenMaterial(
1171
+ `${encodedHeader}.${encodedClaims}`,
1172
+ );
1173
+ if (
1174
+ !timingSafeEqual(fromBase64Url(signature), fromBase64Url(expectedSignature))
1175
+ ) {
1176
+ throw new Error('Invalid webhook receipt batch token.');
1177
+ }
1178
+ const claims = decodeJson<Record<string, unknown>>(encodedClaims);
1179
+ const hasInvalidBaseClaims =
1180
+ claims.v !== INTERNAL_BOOTSTRAP_TOKEN_VERSION ||
1181
+ claims.iss !== INTERNAL_BOOTSTRAP_TOKEN_ISSUER ||
1182
+ claims.aud !== PLAY_WEBHOOK_RECEIPT_BATCH_ADMISSION_AUDIENCE ||
1183
+ typeof claims.org_id !== 'string' ||
1184
+ claims.org_id.length === 0 ||
1185
+ typeof claims.iat !== 'number' ||
1186
+ !Number.isFinite(claims.iat) ||
1187
+ typeof claims.exp !== 'number' ||
1188
+ !Number.isFinite(claims.exp);
1189
+ const hasValidV1Claims =
1190
+ claims.purpose === PLAY_WEBHOOK_RECEIPT_BATCH_ADMISSION_PURPOSE &&
1191
+ claims.sub === 'play_webhook_receipt_batch_dispatcher' &&
1192
+ Array.isArray(claims.receipt_ids) &&
1193
+ claims.receipt_ids.length >= 1 &&
1194
+ claims.receipt_ids.length <= 32 &&
1195
+ !claims.receipt_ids.some(
1196
+ (receiptId) => typeof receiptId !== 'string' || receiptId.length === 0,
1197
+ ) &&
1198
+ new Set(claims.receipt_ids).size === claims.receipt_ids.length;
1199
+ const hasValidV2Claims =
1200
+ claims.purpose === PLAY_WEBHOOK_RECEIPT_LARGE_BATCH_ADMISSION_PURPOSE &&
1201
+ claims.sub === 'play_webhook_receipt_large_batch_dispatcher' &&
1202
+ typeof claims.receipt_ids_sha256 === 'string' &&
1203
+ /^[A-Za-z0-9_-]{43}$/.test(claims.receipt_ids_sha256);
1204
+ if (
1205
+ hasInvalidBaseClaims ||
1206
+ (!hasValidV1Claims && !hasValidV2Claims)
1207
+ ) {
1208
+ throw new Error('Invalid webhook receipt batch token.');
1209
+ }
1210
+ // `hasInvalidBaseClaims` is deliberately a named diagnostic, but TypeScript
1211
+ // cannot narrow `Record<string, unknown>` through that boolean. Reassert the
1212
+ // already-validated temporal claim types before comparing them below.
1213
+ const expiresAt = claims.exp;
1214
+ const issuedAt = claims.iat;
1215
+ if (typeof expiresAt !== 'number' || typeof issuedAt !== 'number') {
1216
+ throw new Error('Invalid webhook receipt batch token.');
1217
+ }
1218
+ const nowSeconds = Math.floor(nowMs / 1000);
1219
+ if (expiresAt <= nowSeconds || issuedAt > nowSeconds + 30) {
1220
+ throw new Error('Webhook receipt batch token expired.');
1221
+ }
1222
+ return claims as PlayWebhookReceiptBatchAdmissionTokenClaims;
1223
+ }
1224
+
1225
+ export async function verifyPlayWebhookReceiptAdmissionToken(
1226
+ token: string,
1227
+ nowMs = Date.now(),
1228
+ ): Promise<PlayWebhookReceiptAdmissionTokenClaims> {
1229
+ const segments = token.split('.');
1230
+ if (segments.length !== 3) throw new Error('Invalid webhook receipt token.');
1231
+ const [encodedHeader, encodedClaims, signature] = segments;
1232
+ const header = decodeJson<Record<string, unknown>>(encodedHeader);
1233
+ if (header.alg !== 'HS256' || header.typ !== 'JWT') {
1234
+ throw new Error('Invalid webhook receipt token.');
1235
+ }
1236
+ const expectedSignature = await signTokenMaterial(
1237
+ `${encodedHeader}.${encodedClaims}`,
1238
+ );
1239
+ if (
1240
+ !timingSafeEqual(fromBase64Url(signature), fromBase64Url(expectedSignature))
1241
+ ) {
1242
+ throw new Error('Invalid webhook receipt token.');
1243
+ }
1244
+ const claims = decodeJson<Record<string, unknown>>(encodedClaims);
1245
+ if (
1246
+ claims.v !== INTERNAL_BOOTSTRAP_TOKEN_VERSION ||
1247
+ claims.iss !== INTERNAL_BOOTSTRAP_TOKEN_ISSUER ||
1248
+ claims.aud !== PLAY_WEBHOOK_RECEIPT_ADMISSION_AUDIENCE ||
1249
+ claims.purpose !== PLAY_WEBHOOK_RECEIPT_ADMISSION_PURPOSE ||
1250
+ claims.sub !== 'play_webhook_receipt_dispatcher' ||
1251
+ typeof claims.org_id !== 'string' ||
1252
+ claims.org_id.length === 0 ||
1253
+ typeof claims.receipt_id !== 'string' ||
1254
+ claims.receipt_id.length === 0 ||
1255
+ typeof claims.iat !== 'number' ||
1256
+ !Number.isFinite(claims.iat) ||
1257
+ typeof claims.exp !== 'number' ||
1258
+ !Number.isFinite(claims.exp)
1259
+ ) {
1260
+ throw new Error('Invalid webhook receipt token.');
1261
+ }
1262
+ const nowSeconds = Math.floor(nowMs / 1000);
1263
+ if (claims.exp <= nowSeconds || claims.iat > nowSeconds + 30) {
1264
+ throw new Error('Webhook receipt token expired.');
1265
+ }
1266
+ return claims as PlayWebhookReceiptAdmissionTokenClaims;
1267
+ }
1268
+
1269
+ export async function mintPlayTriggerEventAdmissionToken(input: {
1270
+ orgId: string;
1271
+ eventId: string;
1272
+ nowMs?: number;
1273
+ }): Promise<string> {
1274
+ return await mintPlayTriggerEventAdmissionTokenShared(input);
1275
+ }
1276
+
1277
+ export async function verifyPlayTriggerEventAdmissionToken(
1278
+ token: string,
1279
+ nowMs = Date.now(),
1280
+ ): Promise<PlayTriggerEventAdmissionTokenClaims> {
1281
+ const segments = token.split('.');
1282
+ if (segments.length !== 3) throw new Error('Invalid trigger event token.');
1283
+ const [encodedHeader, encodedClaims, signature] = segments;
1284
+ const header = decodeJson<Record<string, unknown>>(encodedHeader);
1285
+ if (header.alg !== 'HS256' || header.typ !== 'JWT') {
1286
+ throw new Error('Invalid trigger event token.');
1287
+ }
1288
+ const expectedSignature = await signTokenMaterial(
1289
+ `${encodedHeader}.${encodedClaims}`,
1290
+ );
1291
+ if (
1292
+ !timingSafeEqual(fromBase64Url(signature), fromBase64Url(expectedSignature))
1293
+ ) {
1294
+ throw new Error('Invalid trigger event token.');
1295
+ }
1296
+ const claims = decodeJson<Record<string, unknown>>(encodedClaims);
1297
+ if (
1298
+ claims.v !== INTERNAL_BOOTSTRAP_TOKEN_VERSION ||
1299
+ claims.iss !== INTERNAL_BOOTSTRAP_TOKEN_ISSUER ||
1300
+ claims.aud !== PLAY_TRIGGER_EVENT_ADMISSION_AUDIENCE ||
1301
+ claims.purpose !== PLAY_TRIGGER_EVENT_ADMISSION_PURPOSE ||
1302
+ claims.sub !== 'play_trigger_event_dispatcher' ||
1303
+ typeof claims.org_id !== 'string' ||
1304
+ claims.org_id.length === 0 ||
1305
+ typeof claims.event_id !== 'string' ||
1306
+ claims.event_id.length === 0 ||
1307
+ typeof claims.iat !== 'number' ||
1308
+ !Number.isFinite(claims.iat) ||
1309
+ typeof claims.exp !== 'number' ||
1310
+ !Number.isFinite(claims.exp)
1311
+ ) {
1312
+ throw new Error('Invalid trigger event token.');
1313
+ }
1314
+ const nowSeconds = Math.floor(nowMs / 1000);
1315
+ if (claims.exp <= nowSeconds || claims.iat > nowSeconds + 30) {
1316
+ throw new Error('Trigger event token expired.');
1317
+ }
1318
+ return claims as PlayTriggerEventAdmissionTokenClaims;
1319
+ }
1320
+
1321
+ export async function mintSignalRadarInboundReceiptToken(input: {
1322
+ orgId: string;
1323
+ receiptId: string;
1324
+ nowMs?: number;
1325
+ }): Promise<string> {
1326
+ return await mintSignalRadarInboundReceiptTokenShared(input);
1327
+ }
1328
+
1329
+ export async function verifySignalRadarInboundReceiptToken(
1330
+ token: string,
1331
+ nowMs = Date.now(),
1332
+ ): Promise<SignalRadarInboundReceiptTokenClaims> {
1333
+ const segments = token.split('.');
1334
+ if (segments.length !== 3) throw new Error('Invalid inbound receipt token.');
1335
+ const [encodedHeader, encodedClaims, signature] = segments;
1336
+ const header = decodeJson<Record<string, unknown>>(encodedHeader);
1337
+ if (header.alg !== 'HS256' || header.typ !== 'JWT') {
1338
+ throw new Error('Invalid inbound receipt token.');
1339
+ }
1340
+ const expectedSignature = await signTokenMaterial(
1341
+ `${encodedHeader}.${encodedClaims}`,
1342
+ );
1343
+ if (
1344
+ !timingSafeEqual(fromBase64Url(signature), fromBase64Url(expectedSignature))
1345
+ ) {
1346
+ throw new Error('Invalid inbound receipt token.');
1347
+ }
1348
+ const claims = decodeJson<Record<string, unknown>>(encodedClaims);
1349
+ if (
1350
+ claims.v !== INTERNAL_BOOTSTRAP_TOKEN_VERSION ||
1351
+ claims.iss !== INTERNAL_BOOTSTRAP_TOKEN_ISSUER ||
1352
+ claims.aud !== SIGNAL_RADAR_INBOUND_RECEIPT_AUDIENCE ||
1353
+ claims.purpose !== SIGNAL_RADAR_INBOUND_RECEIPT_PURPOSE ||
1354
+ claims.sub !== 'signal_radar_inbound_receipt_dispatcher' ||
1355
+ typeof claims.org_id !== 'string' ||
1356
+ claims.org_id.length === 0 ||
1357
+ typeof claims.receipt_id !== 'string' ||
1358
+ claims.receipt_id.length === 0 ||
1359
+ typeof claims.iat !== 'number' ||
1360
+ !Number.isFinite(claims.iat) ||
1361
+ typeof claims.exp !== 'number' ||
1362
+ !Number.isFinite(claims.exp)
1363
+ ) {
1364
+ throw new Error('Invalid inbound receipt token.');
1365
+ }
1366
+ const nowSeconds = Math.floor(nowMs / 1000);
1367
+ if (claims.exp <= nowSeconds || claims.iat > nowSeconds + 30) {
1368
+ throw new Error('Inbound receipt token expired.');
1369
+ }
1370
+ return claims as SignalRadarInboundReceiptTokenClaims;
1371
+ }
1372
+
1048
1373
  /**
1049
1374
  * Mints a durable, narrowly scoped opt-out command. It never expires because
1050
1375
  * an old notification must remain usable to stop future email; rotating the
@@ -0,0 +1,134 @@
1
+ import {
2
+ PLAY_SECRET_UNAVAILABLE,
3
+ playSecretUnavailableErrorFrom,
4
+ } from '../plays/secrets/errors';
5
+
6
+ /**
7
+ * Stable failure codes written onto a durable webhook receipt or cron
8
+ * occurrence. These are an admission ABI: a source can outlive the Vercel
9
+ * build that classified it, so route-local error classes are not a contract.
10
+ */
11
+ export const PLAY_ADMISSION_INTERNAL_FAILURE =
12
+ 'INTERNAL_ADMISSION_FAILURE' as const;
13
+ export const PLAY_ADMISSION_PAYLOAD_UNREADABLE = 'PAYLOAD_UNREADABLE' as const;
14
+ /**
15
+ * An invalid JSON string, so it cannot be a payload produced by JSON.stringify.
16
+ * This preserves the established Convex action return-object contract while a
17
+ * new adapter can still distinguish verified envelope corruption from an
18
+ * arbitrary transport or Runtime parsing error.
19
+ */
20
+ export const PLAY_ADMISSION_PAYLOAD_UNREADABLE_SENTINEL =
21
+ '\u0000deepline:payload-unreadable';
22
+ export const PLAY_RUN_PRE_RUN_VALIDATION_FAILED =
23
+ 'PLAY_RUN_PRE_RUN_VALIDATION_FAILED' as const;
24
+ export const RUNTIME_DB_SESSION_PRELOAD_INCOMPLETE =
25
+ 'RUNTIME_DB_SESSION_PRELOAD_INCOMPLETE' as const;
26
+
27
+ export type PlayAdmissionBlockedCode =
28
+ | typeof PLAY_ADMISSION_PAYLOAD_UNREADABLE
29
+ | typeof PLAY_SECRET_UNAVAILABLE
30
+ | typeof PLAY_RUN_PRE_RUN_VALIDATION_FAILED
31
+ | typeof RUNTIME_DB_SESSION_PRELOAD_INCOMPLETE;
32
+
33
+ export type PlayAdmissionRetryableCode = typeof PLAY_ADMISSION_INTERNAL_FAILURE;
34
+
35
+ export type PlayAdmissionFailure =
36
+ | {
37
+ /** Preserve the source and let its bounded transport retry. */
38
+ disposition: 'retry';
39
+ code: PlayAdmissionRetryableCode;
40
+ message: string;
41
+ }
42
+ | {
43
+ /** Retain the source as the pre-admission DLQ and release transport capacity. */
44
+ disposition: 'blocked';
45
+ code: PlayAdmissionBlockedCode;
46
+ message: string;
47
+ };
48
+
49
+ const TERMINAL_PRE_ADMISSION_CODES = new Set<PlayAdmissionBlockedCode>([
50
+ PLAY_RUN_PRE_RUN_VALIDATION_FAILED,
51
+ RUNTIME_DB_SESSION_PRELOAD_INCOMPLETE,
52
+ ]);
53
+
54
+ function stableErrorCode(error: unknown): string | null {
55
+ if (error === null || typeof error !== 'object') return null;
56
+ const code = (error as { code?: unknown }).code;
57
+ return typeof code === 'string' ? code : null;
58
+ }
59
+
60
+ function blocked(code: PlayAdmissionBlockedCode): PlayAdmissionFailure {
61
+ return {
62
+ disposition: 'blocked',
63
+ code,
64
+ message:
65
+ code === PLAY_SECRET_UNAVAILABLE
66
+ ? 'The Play configuration is unavailable. Repair it, then retry this delivery.'
67
+ : 'This delivery needs attention before it can be retried.',
68
+ };
69
+ }
70
+
71
+ /**
72
+ * Only the receipt adapter may raise this while decoding its durable JSON
73
+ * envelope, before it has called Runtime admission. A generic `SyntaxError`
74
+ * can come from a driver, generated code, or an uncertain submit path, so it
75
+ * must retain the safe retry disposition.
76
+ */
77
+ export class PlayAdmissionPayloadDecodeError extends Error {
78
+ constructor(options?: { cause?: unknown }) {
79
+ super('Stored Play admission payload could not be decoded.');
80
+ this.name = 'PlayAdmissionPayloadDecodeError';
81
+ // This module is also compiled by Convex with ES2021 library types.
82
+ // Attach the diagnostic cause without Error's ES2022 two-argument overload.
83
+ if (options?.cause !== undefined) {
84
+ (this as { cause?: unknown }).cause = options.cause;
85
+ }
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Classifies only failures that occur before the Runtime Run + Absurd task
91
+ * transaction. Unknown errors remain retryable because a lost response cannot
92
+ * prove that the atomic submission did not commit. Known invalid pinned input
93
+ * is terminal for this source and must not occupy every Workpool attempt.
94
+ */
95
+ export function classifyPlayAdmissionFailure(
96
+ error: unknown,
97
+ ): PlayAdmissionFailure {
98
+ if (error instanceof PlayAdmissionPayloadDecodeError) {
99
+ return blocked(PLAY_ADMISSION_PAYLOAD_UNREADABLE);
100
+ }
101
+ if (playSecretUnavailableErrorFrom(error)) {
102
+ return blocked(PLAY_SECRET_UNAVAILABLE);
103
+ }
104
+ const code = stableErrorCode(error);
105
+ if (
106
+ code !== null &&
107
+ TERMINAL_PRE_ADMISSION_CODES.has(code as PlayAdmissionBlockedCode)
108
+ ) {
109
+ return blocked(code as PlayAdmissionBlockedCode);
110
+ }
111
+ return {
112
+ disposition: 'retry',
113
+ code: PLAY_ADMISSION_INTERNAL_FAILURE,
114
+ message:
115
+ 'Deepline could not start this delivery yet. It will retry automatically.',
116
+ };
117
+ }
118
+
119
+ /**
120
+ * Signals a retryable private-admission response to Workpool without making
121
+ * the route or a source-specific adapter re-interpret arbitrary thrown text.
122
+ */
123
+ export class PlayAdmissionRetryableError extends Error {
124
+ constructor(
125
+ readonly code: PlayAdmissionRetryableCode,
126
+ options?: { cause?: unknown },
127
+ ) {
128
+ super('Play admission is retryable.');
129
+ this.name = 'PlayAdmissionRetryableError';
130
+ if (options?.cause !== undefined) {
131
+ (this as { cause?: unknown }).cause = options.cause;
132
+ }
133
+ }
134
+ }
@@ -54,3 +54,16 @@ export class PlayRunLaunchConflictError extends Error {
54
54
  this.name = 'PlayRunLaunchConflictError';
55
55
  }
56
56
  }
57
+
58
+ /** A fenced replay named a Run that was never durably admitted. */
59
+ export class PlayRunAdmissionReplayMissingError extends Error {
60
+ readonly code = 'PLAY_RUN_ADMISSION_REPLAY_MISSING';
61
+
62
+ constructor(runId: string, options?: { cause?: unknown }) {
63
+ super(
64
+ `Play run "${runId}" does not exist for an existing-only admission.`,
65
+ options,
66
+ );
67
+ this.name = 'PlayRunAdmissionReplayMissingError';
68
+ }
69
+ }
@@ -1,6 +1,9 @@
1
1
  export { createPlayRunAdmissionJob } from '../../play-runs/admission/job';
2
2
  export { createPostgresPlayRunAdmissionPorts } from '../../play-runs/admission/postgres-adapter';
3
- export { PlayRunLaunchConflictError } from '../../play-runs/admission/contracts';
3
+ export {
4
+ PlayRunAdmissionReplayMissingError,
5
+ PlayRunLaunchConflictError,
6
+ } from '../../play-runs/admission/contracts';
4
7
  export type {
5
8
  PlayRunAdmissionInput,
6
9
  PlayRunAdmissionJob,
@@ -3,6 +3,7 @@ import {
3
3
  type PostgresSchedulerQueryClient,
4
4
  } from '../../scheduler-backends/postgres';
5
5
  import {
6
+ PlayRunAdmissionReplayMissingError,
6
7
  PlayRunLaunchConflictError,
7
8
  type PlayRunAdmissionInput,
8
9
  type PlayRunAdmissionReceipt,
@@ -39,6 +40,11 @@ export function createPostgresPlayRunAdmissionPorts(input: {
39
40
  cause: error,
40
41
  });
41
42
  }
43
+ if (isExistingOnlyReplayMissing(error)) {
44
+ throw new PlayRunAdmissionReplayMissingError(admission.launch.runId, {
45
+ cause: error,
46
+ });
47
+ }
42
48
  throw error;
43
49
  }
44
50
  },
@@ -46,6 +52,13 @@ export function createPostgresPlayRunAdmissionPorts(input: {
46
52
  };
47
53
  }
48
54
 
55
+ function isExistingOnlyReplayMissing(error: unknown): boolean {
56
+ return (
57
+ error instanceof Error &&
58
+ /existing-only admission/i.test(error.message)
59
+ );
60
+ }
61
+
49
62
  function isLaunchConflict(error: unknown): boolean {
50
63
  return (
51
64
  error instanceof Error &&
@@ -0,0 +1,32 @@
1
+ /**
2
+ * The private Workpool dispatcher stops waiting for Vercel at this bound. A
3
+ * client abort does not prove the Vercel handler stopped, so it is not by
4
+ * itself safe for cleanup to recover an in-flight submit.
5
+ */
6
+ export const PLAY_WEBHOOK_ADMISSION_PROBE_DISPATCH_REQUEST_MAX_MS = 60_000;
7
+
8
+ /**
9
+ * The final Vercel-side Runtime admission can make one pooled read and one
10
+ * atomic scheduler submit. Each is bounded by a 5s acquire plus 30s query
11
+ * timeout; retain five seconds for in-process handoff/settlement.
12
+ */
13
+ export const PLAY_WEBHOOK_ADMISSION_PROBE_SCHEDULER_COMMIT_TAIL_MS = 75_000;
14
+
15
+ /** Vercel must survive the dispatch client timeout plus its commit tail. */
16
+ export const PLAY_WEBHOOK_ADMISSION_PROBE_VERCEL_MAX_DURATION_SECONDS = 135;
17
+
18
+ /** A confirmed submission is valid until the bounded handler can finish. */
19
+ export const PLAY_WEBHOOK_ADMISSION_PROBE_SUBMISSION_MAX_MS =
20
+ PLAY_WEBHOOK_ADMISSION_PROBE_VERCEL_MAX_DURATION_SECONDS * 1_000;
21
+
22
+ /**
23
+ * Keep a source fence live after Vercel's bounded submission window. This
24
+ * avoids reclaiming on the exact platform deadline while a final scheduler
25
+ * connection teardown or commit acknowledgement is still resolving.
26
+ */
27
+ export const PLAY_WEBHOOK_ADMISSION_PROBE_CLEANUP_GRACE_MS = 5_000;
28
+
29
+ /** Cleanup may reclaim a killed probe only after submission plus grace. */
30
+ export const PLAY_WEBHOOK_ADMISSION_PROBE_SUBMISSION_LEASE_MS =
31
+ PLAY_WEBHOOK_ADMISSION_PROBE_SUBMISSION_MAX_MS +
32
+ PLAY_WEBHOOK_ADMISSION_PROBE_CLEANUP_GRACE_MS;
@@ -0,0 +1,28 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { buildPlayWorkflowFamilyKey } from './run-identity';
3
+
4
+ /**
5
+ * The deterministic identity shared by a durable webhook source, its Runtime
6
+ * Run, and its Absurd task. This is intentionally dependency-free: provider
7
+ * and CLI tooling must be able to reconstruct it without importing Vercel
8
+ * admission code.
9
+ */
10
+ export function buildPlayWebhookIdentity(input: {
11
+ bindingId: string;
12
+ dedupeKey: string;
13
+ playName: string;
14
+ }) {
15
+ const deliveryHash = createHash('sha256')
16
+ .update(input.bindingId)
17
+ .update('\0')
18
+ .update(input.dedupeKey)
19
+ .digest('hex');
20
+ const workflowFamilyKey = buildPlayWorkflowFamilyKey(input.playName);
21
+ const runId = `play/${workflowFamilyKey}/webhook/${deliveryHash.slice(0, 32)}`;
22
+ return {
23
+ eventId: `pwh_${deliveryHash}`,
24
+ workflowId: runId,
25
+ runId,
26
+ workflowFamilyKey,
27
+ };
28
+ }
@@ -41,6 +41,13 @@ export type { PlayRunInputPayload };
41
41
 
42
42
  export type PlaySchedulerSubmitInput = {
43
43
  runId: string;
44
+ /**
45
+ * Admit an exact replay only. This is an ephemeral admission instruction:
46
+ * it is deliberately excluded from the persisted launch identity so a
47
+ * delivery retried after its binding is disabled can observe its original
48
+ * Run without changing that Run's immutable launch contract.
49
+ */
50
+ existingOnly?: boolean;
44
51
  /** First-class async-child lineage. Absent for root launches. */
45
52
  parentRunId?: string | null;
46
53
  rootRunId?: string | null;