deepline 0.3.113 → 0.3.114
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.
- package/dist/bundling-sources/sdk/src/client.ts +3 -1
- package/dist/bundling-sources/sdk/src/compat.ts +167 -11
- package/dist/bundling-sources/sdk/src/release.ts +1 -1
- package/dist/bundling-sources/sdk/src/skills-version.ts +27 -1
- package/dist/bundling-sources/shared_libs/monitors/inbound-queue.ts +6 -2
- package/dist/bundling-sources/shared_libs/observability/queue-health.ts +291 -6
- package/dist/bundling-sources/shared_libs/observability/scheduled-jobs.json +24 -24
- package/dist/bundling-sources/shared_libs/observability/scheduled-work-manual.ts +2 -0
- package/dist/bundling-sources/shared_libs/observability/scheduled-work.ts +29 -1
- package/dist/bundling-sources/shared_libs/play-runtime/admission-transport-policy.ts +91 -0
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +2 -0
- package/dist/bundling-sources/shared_libs/play-runtime/direct-admission-deadline.ts +34 -0
- package/dist/bundling-sources/shared_libs/play-runtime/fixture-behavior.ts +1 -0
- package/dist/bundling-sources/shared_libs/play-runtime/internal-bootstrap-token.ts +325 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-admission-outcome.ts +134 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/admission/contracts.ts +13 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/admission/index.ts +4 -1
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/admission/postgres-adapter.ts +13 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-webhook-admission-probe.ts +32 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-webhook-identity.ts +28 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backend.ts +7 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-cron-fire-task.ts +100 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-runtime-worker.ts +313 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-shared.ts +111 -3
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd.ts +16 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-pool.ts +67 -8
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +1394 -22
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/run-ledger-outbox-recovery.ts +74 -13
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/submit-admission.ts +79 -15
- package/dist/bundling-sources/shared_libs/play-runtime/webhook-receipt-retry.ts +21 -21
- package/dist/bundling-sources/shared_libs/plays/bundling/index.ts +4 -1
- package/dist/bundling-sources/shared_libs/product-notifications/catalog.ts +64 -0
- package/dist/bundling-sources/shared_libs/product-notifications/default-policy.ts +1 -1
- package/dist/bundling-sources/shared_libs/product-notifications/events.ts +20 -1
- package/dist/bundling-sources/shared_libs/security/receipt-admission-token.ts +224 -0
- package/dist/cli/index.js +574 -114
- package/dist/cli/index.mjs +592 -133
- package/dist/index.d.mts +54 -1
- package/dist/index.d.ts +54 -1
- package/dist/index.js +8 -3
- package/dist/index.mjs +8 -3
- package/dist/install-integrity.json +7 -0
- package/dist/plays/bundle-play-file.mjs +1 -1
- package/dist/release.d.mts +1 -1
- package/dist/release.d.ts +1 -1
- package/dist/release.js +1 -1
- package/dist/release.mjs +1 -1
- package/package.json +1 -1
|
@@ -976,13 +976,13 @@
|
|
|
976
976
|
"domainReportRequired": true
|
|
977
977
|
},
|
|
978
978
|
{
|
|
979
|
-
"id": "
|
|
979
|
+
"id": "convex.recover-play-webhook-receipts",
|
|
980
980
|
"name": "Recover Play webhook receipts",
|
|
981
|
-
"scheduler": "
|
|
982
|
-
"cadence": "
|
|
981
|
+
"scheduler": "convex_cron",
|
|
982
|
+
"cadence": "every 60s",
|
|
983
983
|
"timezone": "UTC",
|
|
984
|
-
"source": "
|
|
985
|
-
"locator": "
|
|
984
|
+
"source": "packages/convex/crons.ts",
|
|
985
|
+
"locator": "recover Play webhook receipts",
|
|
986
986
|
"owner": "runtime",
|
|
987
987
|
"criticality": "critical",
|
|
988
988
|
"purpose": "Recover Play webhook receipts whose admission/dispatch continuation is delayed or stranded.",
|
|
@@ -990,7 +990,25 @@
|
|
|
990
990
|
"failureDefinition": "A receipt cannot be safely recovered, remains stranded, or evidence cannot persist.",
|
|
991
991
|
"startGraceMs": 300000,
|
|
992
992
|
"runTimeoutMs": 1800000,
|
|
993
|
-
"failureAlert": "
|
|
993
|
+
"failureAlert": "convex_action_wrapper",
|
|
994
|
+
"domainReportRequired": true
|
|
995
|
+
},
|
|
996
|
+
{
|
|
997
|
+
"id": "convex.recover-signal-radar-inbound-receipts",
|
|
998
|
+
"name": "Recover Signal Radar inbound receipts",
|
|
999
|
+
"scheduler": "convex_cron",
|
|
1000
|
+
"cadence": "every 60s",
|
|
1001
|
+
"timezone": "UTC",
|
|
1002
|
+
"source": "packages/convex/crons.ts",
|
|
1003
|
+
"locator": "recover Signal Radar inbound receipts",
|
|
1004
|
+
"owner": "monitors",
|
|
1005
|
+
"criticality": "critical",
|
|
1006
|
+
"purpose": "Recover Signal Radar inbound receipts whose processing is delayed or stranded.",
|
|
1007
|
+
"successDefinition": "Every eligible receipt is retried or terminally classified with evidence; no eligible receipts is a valid no-op.",
|
|
1008
|
+
"failureDefinition": "A receipt cannot be safely recovered, remains stranded, or evidence cannot persist.",
|
|
1009
|
+
"startGraceMs": 300000,
|
|
1010
|
+
"runTimeoutMs": 1800000,
|
|
1011
|
+
"failureAlert": "convex_action_wrapper",
|
|
994
1012
|
"domainReportRequired": true
|
|
995
1013
|
},
|
|
996
1014
|
{
|
|
@@ -1011,24 +1029,6 @@
|
|
|
1011
1029
|
"failureAlert": "convex_action_wrapper",
|
|
1012
1030
|
"domainReportRequired": true
|
|
1013
1031
|
},
|
|
1014
|
-
{
|
|
1015
|
-
"id": "github.modal-ci-runner-weekly-proof",
|
|
1016
|
-
"name": "Modal CI runner",
|
|
1017
|
-
"scheduler": "github_actions",
|
|
1018
|
-
"cadence": "23 6 * * 1",
|
|
1019
|
-
"timezone": "UTC",
|
|
1020
|
-
"source": ".github/workflows/modal-ci-runner.yml",
|
|
1021
|
-
"locator": "schedule:",
|
|
1022
|
-
"owner": "platform",
|
|
1023
|
-
"criticality": "maintenance",
|
|
1024
|
-
"purpose": "Lint, test, deploy, and prove the pinned Modal CI runner image and isolated smoke path.",
|
|
1025
|
-
"successDefinition": "Pinned package lint/tests, control-plane/image deploy, and exact-image credential-isolation smoke all pass.",
|
|
1026
|
-
"failureDefinition": "Any lint/test/deploy/health/smoke/cleanup proof fails or image identity is wrong.",
|
|
1027
|
-
"startGraceMs": 600000,
|
|
1028
|
-
"runTimeoutMs": 3600000,
|
|
1029
|
-
"failureAlert": "github_workflow_run",
|
|
1030
|
-
"domainReportRequired": true
|
|
1031
|
-
},
|
|
1032
1032
|
{
|
|
1033
1033
|
"id": "convex.async-usage-recovery-continuation",
|
|
1034
1034
|
"name": "Async usage recovery continuation",
|
|
@@ -18,6 +18,8 @@ export const MANUALLY_RUNNABLE_CONVEX_CRON_JOB_IDS = [
|
|
|
18
18
|
'convex.monitor-deployment-job-tick',
|
|
19
19
|
'convex.provider-monitor-global-audit-daily',
|
|
20
20
|
'convex.recover-notification-delivery-work',
|
|
21
|
+
'convex.recover-play-webhook-receipts',
|
|
22
|
+
'convex.recover-signal-radar-inbound-receipts',
|
|
21
23
|
'convex.process-notification-usage-refreshes',
|
|
22
24
|
'convex.observe-operational-queue-health',
|
|
23
25
|
'convex.cleanup-expired-notification-audit-records',
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
/** The only scheduled-work telemetry envelope emitted after the hard cut. */
|
|
2
|
+
export const SCHEDULED_WORK_OBSERVABILITY_VERSION = 2 as const;
|
|
3
|
+
export const SCHEDULED_WORK_CONTRACT_VERSION = 2 as const;
|
|
2
4
|
|
|
3
5
|
export type ScheduledWorkScheduler =
|
|
4
6
|
| 'convex_cron'
|
|
@@ -11,6 +13,17 @@ export type ScheduledWorkScheduler =
|
|
|
11
13
|
|
|
12
14
|
export type ScheduledWorkOutcome = 'running' | 'succeeded' | 'failed';
|
|
13
15
|
export type ScheduledWorkTrigger = 'scheduled' | 'manual';
|
|
16
|
+
export type ScheduledWorkDomainOutcome = 'success' | 'warning' | 'failure';
|
|
17
|
+
export type ScheduledWorkFailureClass = 'execution' | 'domain';
|
|
18
|
+
|
|
19
|
+
export type ScheduledWorkTelemetryDisposition = {
|
|
20
|
+
/** The occurrence slot this event belongs to, when one exists. */
|
|
21
|
+
occurrenceId?: string;
|
|
22
|
+
expectedAt?: number;
|
|
23
|
+
/** Explicit domain result; absence is a migration gap, not a green run. */
|
|
24
|
+
domainOutcome?: ScheduledWorkDomainOutcome;
|
|
25
|
+
failureClass?: ScheduledWorkFailureClass;
|
|
26
|
+
};
|
|
14
27
|
|
|
15
28
|
export type ScheduledWorkVolume = {
|
|
16
29
|
unit: string;
|
|
@@ -44,9 +57,12 @@ export type ScheduledWorkObservation = {
|
|
|
44
57
|
|
|
45
58
|
export function scheduledWorkTelemetryFields(
|
|
46
59
|
observation: ScheduledWorkObservation,
|
|
60
|
+
disposition: ScheduledWorkTelemetryDisposition = {},
|
|
47
61
|
): Record<string, string | number> {
|
|
48
62
|
return {
|
|
49
63
|
scheduledWorkSchemaVersion: SCHEDULED_WORK_OBSERVABILITY_VERSION,
|
|
64
|
+
// Keep one explicit marker for the hard-cut occurrence/domain contract.
|
|
65
|
+
scheduledWorkContractVersion: SCHEDULED_WORK_CONTRACT_VERSION,
|
|
50
66
|
scheduledWorkJobId: observation.jobId,
|
|
51
67
|
scheduledWorkJobName: observation.jobName,
|
|
52
68
|
scheduledWorkScheduler: observation.scheduler,
|
|
@@ -55,6 +71,18 @@ export function scheduledWorkTelemetryFields(
|
|
|
55
71
|
scheduledWorkOutcome: observation.outcome,
|
|
56
72
|
scheduledWorkInvocationId: observation.invocationId,
|
|
57
73
|
scheduledWorkExpectedWithinMs: observation.expectedWithinMs,
|
|
74
|
+
...(disposition.occurrenceId === undefined
|
|
75
|
+
? {}
|
|
76
|
+
: { scheduledWorkOccurrenceId: disposition.occurrenceId }),
|
|
77
|
+
...(disposition.expectedAt === undefined
|
|
78
|
+
? {}
|
|
79
|
+
: { scheduledWorkExpectedAt: disposition.expectedAt }),
|
|
80
|
+
...(disposition.domainOutcome === undefined
|
|
81
|
+
? {}
|
|
82
|
+
: { scheduledWorkDomainOutcome: disposition.domainOutcome }),
|
|
83
|
+
...(disposition.failureClass === undefined
|
|
84
|
+
? {}
|
|
85
|
+
: { scheduledWorkFailureClass: disposition.failureClass }),
|
|
58
86
|
...(observation.trigger === undefined
|
|
59
87
|
? {}
|
|
60
88
|
: { scheduledWorkTrigger: observation.trigger }),
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { PLAY_WEBHOOK_RECEIPT_LARGE_BATCH_MAX_RECEIPTS } from '../security/receipt-admission-token';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The per-request concurrency budget for moving a durable Play source into
|
|
5
|
+
* the Runtime Postgres + Absurd admission transaction. This is not an
|
|
6
|
+
* execution limit: Absurd owns execution capacity independently.
|
|
7
|
+
*
|
|
8
|
+
* Thirty-two is the private batch's entire scheduler-client budget for a
|
|
9
|
+
* normal 1,000-delivery burst. One client remains reserved below, rather than
|
|
10
|
+
* letting source admission consume every control-plane connection.
|
|
11
|
+
*/
|
|
12
|
+
export const PLAY_ADMISSION_MAX_PARALLELISM = 32;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The private batch owns all 32 serverless scheduler clients, but one remains
|
|
16
|
+
* available for executor-token and other critical runtime control-plane work.
|
|
17
|
+
* Only atomic Runtime Postgres + Absurd submission uses this value.
|
|
18
|
+
* Source-lifecycle preflight has its own bounded budget because it does not
|
|
19
|
+
* consume a scheduler-pool client.
|
|
20
|
+
*/
|
|
21
|
+
export const PLAY_ADMISSION_MAX_ACTIVE_SUBMITS =
|
|
22
|
+
PLAY_ADMISSION_MAX_PARALLELISM - 1;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A batch is allowed to prepare more independent source lifecycles than it
|
|
26
|
+
* may submit to Runtime Postgres at once. Preparation is mostly Convex/CPU
|
|
27
|
+
* work (read the immutable receipt, validate its pinned launch, mint its
|
|
28
|
+
* authority); it must not sit behind a scarce PgBouncer connection. The
|
|
29
|
+
* scheduler submitter remains capped by `PLAY_ADMISSION_MAX_ACTIVE_SUBMITS`.
|
|
30
|
+
*
|
|
31
|
+
* This is deliberately finite. It lets a 1K burst fill the 31 durable-submit
|
|
32
|
+
* slots while prior sources are still doing their non-Postgres preparation,
|
|
33
|
+
* without turning the private route into an unbounded fan-out.
|
|
34
|
+
*/
|
|
35
|
+
export const PLAY_ADMISSION_MAX_ACTIVE_LIFECYCLES = 256;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A private 1K batch may queue behind the bounded scheduler submit cohort for
|
|
39
|
+
* longer than an ordinary single-run request, but never beyond this deadline.
|
|
40
|
+
*/
|
|
41
|
+
export const PLAY_ADMISSION_MAX_ACQUIRE_QUEUE_WAIT_MS = 120_000;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* One managed transport request may carry this many independently durable
|
|
45
|
+
* sources. The Vercel adapter processes one legacy wave at bounded
|
|
46
|
+
* source-lifecycle concurrency; its scheduler submit gate separately reserves
|
|
47
|
+
* critical control-plane capacity.
|
|
48
|
+
*/
|
|
49
|
+
export const PLAY_ADMISSION_BATCH_SIZE = PLAY_ADMISSION_MAX_PARALLELISM;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* `batch_v2` carries one full, bounded ingress burst into a single Vercel
|
|
53
|
+
* invocation. The invocation still submits at most
|
|
54
|
+
* `PLAY_ADMISSION_MAX_ACTIVE_SUBMITS` Runs at a time; this only removes the
|
|
55
|
+
* serial Convex-to-Vercel round trip between 32-receipt waves. The current
|
|
56
|
+
* dispatcher sends one request and lets that request drain its owned tail.
|
|
57
|
+
*/
|
|
58
|
+
export const PLAY_ADMISSION_LARGE_BATCH_SIZE =
|
|
59
|
+
PLAY_WEBHOOK_RECEIPT_LARGE_BATCH_MAX_RECEIPTS;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* `batch_v1` remains during the Convex-before-Vercel rollout window. It
|
|
63
|
+
* drains this many 32-source waves from one fenced Workpool action. New
|
|
64
|
+
* Retained custom-runner v2 actions may take a second large wave while their
|
|
65
|
+
* finite rollout cohort drains. Current scheduled dispatches own one
|
|
66
|
+
* exact wave and do not use this compatibility limit.
|
|
67
|
+
*/
|
|
68
|
+
export const PLAY_ADMISSION_BATCHES_PER_DISPATCH = 32;
|
|
69
|
+
|
|
70
|
+
export const PLAY_ADMISSION_LARGE_BATCHES_PER_DISPATCH = 2;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Upper bound for one direct drain under continuous traffic, not an admission
|
|
74
|
+
* SLO or a debounce. A request stops as soon as its queue is empty. Keep the
|
|
75
|
+
* exported name and persisted deadline compatible with retained dispatches.
|
|
76
|
+
*/
|
|
77
|
+
export const PLAY_ADMISSION_DIRECT_BATCH_COLLECTION_MAX_MS = 60_000;
|
|
78
|
+
|
|
79
|
+
/** Poll/debounce interval for retained legacy dispatch implementations only. */
|
|
80
|
+
export const PLAY_ADMISSION_DIRECT_BATCH_QUIET_MS = 250;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Workpool is recovery, not the normal batch transport. It becomes eligible
|
|
84
|
+
* quickly, but the ownerless direct-dispatch fence keeps it from claiming the
|
|
85
|
+
* same source while the private request drains. That gives a lost action
|
|
86
|
+
* prompt recovery without adding another normal-path batch request.
|
|
87
|
+
*/
|
|
88
|
+
export const PLAY_ADMISSION_DIRECT_FALLBACK_DELAY_MS = 15_000;
|
|
89
|
+
|
|
90
|
+
/** A pending source older than this is actionable queue delay. */
|
|
91
|
+
export const PLAY_ADMISSION_DEGRADED_QUEUE_AGE_MS = 60_000;
|
|
@@ -1123,6 +1123,8 @@ export function resolveToolRuntimeTimeoutMs(
|
|
|
1123
1123
|
normalized === 'deeplineagent_deeplineagent' ||
|
|
1124
1124
|
normalized === 'ai_inference' ||
|
|
1125
1125
|
normalized === 'deeplineagent_ai_inference' ||
|
|
1126
|
+
normalized === 'ai_evaluate' ||
|
|
1127
|
+
normalized === 'deeplineagent_ai_evaluate' ||
|
|
1126
1128
|
normalized === 'aiinference'
|
|
1127
1129
|
? DEEPLINEAGENT_TOOL_RUNTIME_TIMEOUT_MS
|
|
1128
1130
|
: DEFAULT_TOOL_RUNTIME_TIMEOUT_MS;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounds a synchronous direct-admission attempt. The source and its delayed
|
|
3
|
+
* Workpool recovery item already committed before this helper is used, so a
|
|
4
|
+
* timeout is deliberately an ambiguous outcome rather than a failed delivery.
|
|
5
|
+
* The race does not cancel already-started work; a replay must use the same
|
|
6
|
+
* idempotency identity and converge on the existing Runtime Run/Absurd task.
|
|
7
|
+
*/
|
|
8
|
+
export const PLAY_DIRECT_ADMISSION_DEADLINE_MS = 1_500;
|
|
9
|
+
|
|
10
|
+
export class PlayDirectAdmissionDeadlineExceededError extends Error {
|
|
11
|
+
constructor(timeoutMs: number) {
|
|
12
|
+
super(`Direct Play admission exceeded its ${timeoutMs}ms deadline.`);
|
|
13
|
+
this.name = 'PlayDirectAdmissionDeadlineExceededError';
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function withPlayDirectAdmissionDeadline<T>(
|
|
18
|
+
fn: () => Promise<T> | T,
|
|
19
|
+
timeoutMs = PLAY_DIRECT_ADMISSION_DEADLINE_MS,
|
|
20
|
+
): Promise<T> {
|
|
21
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
22
|
+
const work = Promise.resolve().then(fn);
|
|
23
|
+
const deadline = new Promise<never>((_resolve, reject) => {
|
|
24
|
+
timer = setTimeout(
|
|
25
|
+
() => reject(new PlayDirectAdmissionDeadlineExceededError(timeoutMs)),
|
|
26
|
+
timeoutMs,
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
try {
|
|
30
|
+
return await Promise.race([work, deadline]);
|
|
31
|
+
} finally {
|
|
32
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -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
|