deepline 0.3.85 → 0.3.87

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 (65) hide show
  1. package/dist/bundling-sources/sdk/src/client.ts +2 -2
  2. package/dist/bundling-sources/sdk/src/monitor-fleet-contract.ts +1 -1
  3. package/dist/bundling-sources/sdk/src/plays/bundle-play-file.ts +1 -1
  4. package/dist/bundling-sources/sdk/src/release.ts +1 -1
  5. package/dist/bundling-sources/shared_libs/integration-contracts/AGENTS.md +9 -0
  6. package/dist/bundling-sources/shared_libs/integration-contracts/README.md +3 -0
  7. package/dist/bundling-sources/shared_libs/integration-contracts/index.ts +2 -0
  8. package/dist/bundling-sources/shared_libs/monitors/analytics-install-guides.ts +77 -0
  9. package/dist/bundling-sources/shared_libs/monitors/billing/charges.ts +125 -0
  10. package/dist/bundling-sources/shared_libs/monitors/billing/policy-builders.ts +686 -0
  11. package/dist/bundling-sources/shared_libs/monitors/billing/pricing.ts +54 -0
  12. package/dist/bundling-sources/shared_libs/monitors/billing/public-pricing.ts +72 -0
  13. package/dist/bundling-sources/shared_libs/monitors/billing/session.ts +45 -0
  14. package/dist/bundling-sources/shared_libs/monitors/billing/state.ts +454 -0
  15. package/dist/bundling-sources/shared_libs/monitors/billing/tamradar.ts +260 -0
  16. package/dist/bundling-sources/shared_libs/monitors/billing/types.ts +215 -0
  17. package/dist/bundling-sources/shared_libs/monitors/callback-security.ts +333 -0
  18. package/dist/bundling-sources/shared_libs/monitors/cursor-page.ts +42 -0
  19. package/dist/bundling-sources/shared_libs/monitors/framework-managed-payload-fields.ts +71 -0
  20. package/dist/bundling-sources/shared_libs/monitors/generated/provider-monitor-callback-admission-targets.ts +16 -0
  21. package/dist/bundling-sources/shared_libs/monitors/http-error-response.ts +267 -0
  22. package/dist/bundling-sources/shared_libs/monitors/inbound-publish-key.ts +39 -0
  23. package/dist/bundling-sources/shared_libs/monitors/inbound-queue.ts +82 -0
  24. package/dist/bundling-sources/shared_libs/monitors/lifecycle-notifications.ts +473 -0
  25. package/dist/bundling-sources/shared_libs/monitors/org-monitor-limits.ts +1 -1
  26. package/dist/bundling-sources/shared_libs/monitors/package.json +29 -0
  27. package/dist/bundling-sources/shared_libs/monitors/provider-callback-verification.ts +1125 -0
  28. package/dist/bundling-sources/shared_libs/monitors/provider-monitor-capability.ts +964 -0
  29. package/dist/bundling-sources/shared_libs/monitors/provider-monitor-consumer-health.ts +56 -0
  30. package/dist/bundling-sources/shared_libs/monitors/provider-monitor-errors.ts +131 -0
  31. package/dist/bundling-sources/shared_libs/monitors/provider-monitor-identity-claims.ts +527 -0
  32. package/dist/bundling-sources/shared_libs/monitors/provider-monitor-legacy-aliases.ts +16 -0
  33. package/dist/bundling-sources/shared_libs/monitors/provider-monitor-priority-execution.ts +56 -0
  34. package/dist/bundling-sources/shared_libs/monitors/provider-monitor-public-aliases.ts +28 -0
  35. package/dist/bundling-sources/shared_libs/monitors/provider-monitor-runtime-read-eligibility.ts +38 -0
  36. package/dist/bundling-sources/shared_libs/monitors/provider-monitor-status.ts +10 -0
  37. package/dist/bundling-sources/shared_libs/monitors/provider-monitor-upstream-identity.ts +112 -0
  38. package/dist/bundling-sources/shared_libs/monitors/provider-webhook-monitor-runtime.ts +300 -0
  39. package/dist/bundling-sources/shared_libs/monitors/quickstart-prompts.ts +22 -0
  40. package/dist/bundling-sources/shared_libs/monitors/snitcher-install-validation.ts +93 -0
  41. package/dist/bundling-sources/shared_libs/monitors/snitcher-ip2company.ts +332 -0
  42. package/dist/bundling-sources/shared_libs/monitors/snitcher-radar.ts +221 -0
  43. package/dist/bundling-sources/shared_libs/monitors/starter-templates.ts +105 -0
  44. package/dist/bundling-sources/shared_libs/monitors/tracking-provider-guidance.ts +73 -0
  45. package/dist/bundling-sources/shared_libs/monitors/tracking-provider-integrations.ts +81 -0
  46. package/dist/bundling-sources/shared_libs/monitors/tsconfig.json +10 -0
  47. package/dist/bundling-sources/shared_libs/play-runtime/runtime-env.ts +6 -0
  48. package/dist/bundling-sources/shared_libs/play-runtime/tool-http-errors.ts +1 -1
  49. package/dist/bundling-sources/shared_libs/play-runtime/tool-result.ts +1 -1
  50. package/dist/bundling-sources/shared_libs/plays/authoring-contract.ts +302 -0
  51. package/dist/bundling-sources/shared_libs/plays/email-status.ts +1 -1
  52. package/dist/bundling-sources/shared_libs/plays/tool-category-descriptions.ts +2 -2
  53. package/dist/bundling-sources/shared_libs/runtime-env.ts +6 -0
  54. package/dist/bundling-sources/shared_libs/security/header-safe-env.ts +28 -0
  55. package/dist/bundling-sources/shared_libs/security/secret-redaction.ts +267 -0
  56. package/dist/cli/index.js +25 -14
  57. package/dist/cli/index.mjs +25 -14
  58. package/dist/index.d.mts +1 -1
  59. package/dist/index.d.ts +1 -1
  60. package/dist/index.js +3 -3
  61. package/dist/index.mjs +3 -3
  62. package/dist/install-integrity.json +45 -2
  63. package/package.json +3 -1
  64. /package/dist/bundling-sources/shared_libs/{integrations/bettercontact-execution-policy.ts → integration-contracts/bettercontact/execution-policy.ts} +0 -0
  65. /package/dist/bundling-sources/shared_libs/{integrations/theirstack-execution-policy.ts → integration-contracts/theirstack/execution-policy.ts} +0 -0
@@ -0,0 +1,56 @@
1
+ import { internal } from '@convex/_generated/api';
2
+ import { getRequiredInternalConvexClient } from '@/lib/convex/internal-client';
3
+
4
+ type MonitorStreamResource = {
5
+ key: string;
6
+ tool: string;
7
+ outputs?: unknown;
8
+ };
9
+
10
+ function record(value: unknown): Record<string, unknown> {
11
+ return value && typeof value === 'object' && !Array.isArray(value)
12
+ ? (value as Record<string, unknown>)
13
+ : {};
14
+ }
15
+
16
+ /**
17
+ * Resolve current SQL-listener consumers for one monitor. Binding metadata is
18
+ * cheap and preserves the established `bound_plays` list contract. Delivery/run
19
+ * health is an explicit, bounded opt-in; monitor source/webhook health remains
20
+ * on the monitor resource itself.
21
+ */
22
+ export async function boundPlaysForProviderMonitor(input: {
23
+ orgId: string;
24
+ monitor: MonitorStreamResource;
25
+ includeHealth?: boolean;
26
+ }): Promise<{ plays: unknown[]; truncated: boolean }> {
27
+ const outputs = record(input.monitor.outputs);
28
+ const tables = Object.values(outputs).flatMap((output) => {
29
+ const table = record(output).table;
30
+ return typeof table === 'string' ? [table] : [];
31
+ });
32
+ const [provider, capability] = input.monitor.tool.split('.', 2);
33
+ if (!provider || !capability) return { plays: [], truncated: false };
34
+
35
+ const convex = getRequiredInternalConvexClient();
36
+ const bound = (await convex.query(
37
+ internal.monitorObservability.dependentPlaysForMonitor,
38
+ {
39
+ orgId: input.orgId,
40
+ monitorKey: input.monitor.key,
41
+ provider,
42
+ capability,
43
+ outputKeys: Object.keys(outputs),
44
+ tables,
45
+ includeHealth: input.includeHealth === true,
46
+ },
47
+ )) as { plays?: unknown[]; truncated?: boolean } | null;
48
+
49
+ // Consumer-health is supplementary observability. An absent result during a
50
+ // rolling control-plane deploy must never make the established monitor
51
+ // list/get surface fail; it simply means there are no observable consumers.
52
+ return {
53
+ plays: Array.isArray(bound?.plays) ? bound.plays : [],
54
+ truncated: bound?.truncated === true,
55
+ };
56
+ }
@@ -0,0 +1,131 @@
1
+ export class ProviderMonitorNotFoundError extends Error {
2
+ constructor(readonly monitorKey: string) {
3
+ super(`Provider monitor "${monitorKey}" was not found.`);
4
+ this.name = 'ProviderMonitorNotFoundError';
5
+ }
6
+ }
7
+
8
+ /**
9
+ * An old Convex org is being indexed for exact provider-identity admission.
10
+ * This is intentionally a temporary, retryable refusal instead of making one
11
+ * customer-facing deploy synchronously scan every legacy monitor definition.
12
+ */
13
+ export class ProviderMonitorIdentityBackfillInProgressError extends Error {
14
+ constructor() {
15
+ super(
16
+ 'Monitor identity records are still being prepared for this workspace. Retry in a few seconds; no upstream monitor was created.',
17
+ );
18
+ this.name = 'ProviderMonitorIdentityBackfillInProgressError';
19
+ }
20
+ }
21
+
22
+ /**
23
+ * A caller tried to publish an older monitor definition after newer control
24
+ * state had committed. Re-read before deciding whether to retry.
25
+ */
26
+ export class ProviderMonitorControlStateConflictError extends Error {
27
+ constructor(readonly monitorKey: string) {
28
+ super(
29
+ `Monitor "${monitorKey}" changed while this update was in progress. Re-read it and retry the update if it is still needed.`,
30
+ );
31
+ this.name = 'ProviderMonitorControlStateConflictError';
32
+ }
33
+ }
34
+
35
+ export class ProviderMonitorCapacityBackfillInProgressError extends Error {
36
+ constructor() {
37
+ super(
38
+ 'Monitor capacity records are still being prepared for this workspace. Retry in a few seconds; no upstream monitor was created.',
39
+ );
40
+ this.name = 'ProviderMonitorCapacityBackfillInProgressError';
41
+ }
42
+ }
43
+
44
+ export type ProviderMonitorCapacitySnapshot = {
45
+ used: number;
46
+ reserved: number;
47
+ limit: number;
48
+ remaining: number;
49
+ over_by: number;
50
+ can_create: boolean;
51
+ };
52
+
53
+ export class ProviderMonitorOrgLimitExceededError extends Error {
54
+ readonly code = 'ORG_MONITOR_LIMIT_EXCEEDED';
55
+
56
+ constructor(readonly capacity: ProviderMonitorCapacitySnapshot) {
57
+ super(
58
+ `This organization is watching ${capacity.used + capacity.reserved} of ${capacity.limit} accounts.`,
59
+ );
60
+ this.name = 'ProviderMonitorOrgLimitExceededError';
61
+ }
62
+ }
63
+
64
+ /**
65
+ * The organization may watch this account, but not with another signal.
66
+ *
67
+ * Distinct from the organization limit on purpose: the customer's next action
68
+ * is different. A full organization means stop watching some account; a full
69
+ * account means drop one of the signals on this one.
70
+ */
71
+ export class ProviderMonitorEntityLimitExceededError extends Error {
72
+ readonly code = 'MONITOR_ENTITY_LIMIT_EXCEEDED';
73
+
74
+ constructor(
75
+ readonly entityKey: string,
76
+ readonly monitorsPerEntity: number,
77
+ ) {
78
+ super(
79
+ `This account already has ${monitorsPerEntity} of ${monitorsPerEntity} monitors.`,
80
+ );
81
+ this.name = 'ProviderMonitorEntityLimitExceededError';
82
+ }
83
+ }
84
+
85
+ export class ProviderMonitorCapacityCleanupRequiredError extends Error {
86
+ readonly code = 'MONITOR_CAPACITY_CLEANUP_REQUIRED';
87
+
88
+ constructor(readonly monitorKey: string) {
89
+ super(
90
+ `Monitor "${monitorKey}" still owns capacity because upstream cleanup is incomplete.`,
91
+ );
92
+ this.name = 'ProviderMonitorCapacityCleanupRequiredError';
93
+ }
94
+ }
95
+
96
+ /**
97
+ * An update already created its new upstream resource and is waiting to remove
98
+ * the recorded previous one. A different desired definition is unsafe until
99
+ * the original update finishes.
100
+ */
101
+ export class ProviderMonitorUpdateOperationInProgressError extends Error {
102
+ constructor(readonly monitorKey: string) {
103
+ super(
104
+ `Monitor "${monitorKey}" is still finishing its previous change. Rerun that same deploy or update before making a different change.`,
105
+ );
106
+ this.name = 'ProviderMonitorUpdateOperationInProgressError';
107
+ }
108
+ }
109
+
110
+ /**
111
+ * A prior delete reached an upstream provider and did not get a confirmed
112
+ * result. Only a retry of that delete may proceed; a deploy or update would
113
+ * risk creating a second resource while the first is still live.
114
+ */
115
+ export class ProviderMonitorDeleteOperationInProgressError extends Error {
116
+ constructor(readonly monitorKey: string) {
117
+ super(
118
+ `Monitor "${monitorKey}" still has an unfinished delete. Rerun the same delete before deploying, updating, or reactivating it.`,
119
+ );
120
+ this.name = 'ProviderMonitorDeleteOperationInProgressError';
121
+ }
122
+ }
123
+
124
+ export class PriorityMonitorExecutionLimitError extends Error {
125
+ constructor(readonly max: number) {
126
+ super(
127
+ `This workspace already has ${max} priority radars. Remove priority from a radar that is no longer urgent before creating another one.`,
128
+ );
129
+ this.name = 'PriorityMonitorExecutionLimitError';
130
+ }
131
+ }
@@ -0,0 +1,527 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import {
3
+ PriorityMonitorExecutionLimitError,
4
+ ProviderMonitorCapacityBackfillInProgressError,
5
+ ProviderMonitorCapacityCleanupRequiredError,
6
+ ProviderMonitorIdentityBackfillInProgressError,
7
+ ProviderMonitorEntityLimitExceededError,
8
+ ProviderMonitorOrgLimitExceededError,
9
+ } from '@deepline/monitors/provider-monitor-errors';
10
+ import { MAX_PRIORITY_MONITORS_PER_ORG } from '@deepline/monitors/provider-monitor-priority-execution';
11
+ import { internal } from '@convex/_generated/api';
12
+ import { getRequiredInternalConvexClient } from '@/lib/convex/internal-client';
13
+ import {
14
+ providerMonitorUpstreamIdentitySignature,
15
+ type ProviderMonitorUpstreamIdentitySignature,
16
+ } from '@deepline/monitors/provider-monitor-upstream-identity';
17
+
18
+ export type ProviderMonitorIdentityClaim = {
19
+ tool: string;
20
+ identityKey: string;
21
+ identity: Record<string, string | null>;
22
+ };
23
+
24
+ type OrgAuthContext = { orgId: string };
25
+
26
+ /**
27
+ * The durable control-plane release operation needs only the opaque claim
28
+ * locator. Keep the display identity out of this boundary: Convex validates
29
+ * release arguments strictly so a rich conflict claim cannot turn a completed
30
+ * upstream delete into a failed local transition.
31
+ */
32
+ export type ProviderMonitorIdentityClaimReference = Pick<
33
+ ProviderMonitorIdentityClaim,
34
+ 'tool' | 'identityKey'
35
+ >;
36
+
37
+ export function providerMonitorIdentityClaimReference(
38
+ claim: ProviderMonitorIdentityClaim | null | undefined,
39
+ ): ProviderMonitorIdentityClaimReference | null {
40
+ return claim ? { tool: claim.tool, identityKey: claim.identityKey } : null;
41
+ }
42
+
43
+ export type ProviderMonitorIdentityReservation =
44
+ | {
45
+ kind: 'acquired';
46
+ claim: ProviderMonitorIdentityClaim;
47
+ restoredState: 'release' | 'active';
48
+ operationId: string;
49
+ }
50
+ | {
51
+ kind: 'occupied';
52
+ claim: ProviderMonitorIdentityClaim;
53
+ monitorKey: string;
54
+ state: 'provisioning' | 'active' | 'migration_conflict';
55
+ conflictingMonitorKeys?: string[];
56
+ }
57
+ | {
58
+ kind: 'in_progress';
59
+ claim: ProviderMonitorIdentityClaim;
60
+ monitorKey: string;
61
+ retryAt: number;
62
+ };
63
+
64
+ /**
65
+ * An exact, per-monitor-key fence around an upstream lifecycle request.
66
+ * Unlike an identity claim, this applies even when a provider does not expose
67
+ * an account-unique identity.
68
+ */
69
+ export type ProviderMonitorLifecycleReservation =
70
+ | {
71
+ kind: 'acquired';
72
+ state: 'provisioning' | 'deleting';
73
+ operationId: string;
74
+ }
75
+ | {
76
+ kind: 'in_progress';
77
+ state: 'provisioning' | 'deleting';
78
+ retryAt: number;
79
+ }
80
+ | {
81
+ kind: 'deleting';
82
+ state: 'deleting';
83
+ retryAt: number;
84
+ }
85
+ | {
86
+ kind: 'not_eligible';
87
+ };
88
+
89
+ function record(value: unknown): Record<string, unknown> | null {
90
+ return value && typeof value === 'object' && !Array.isArray(value)
91
+ ? (value as Record<string, unknown>)
92
+ : null;
93
+ }
94
+
95
+ function finiteNumber(value: unknown): number | null {
96
+ return typeof value === 'number' && Number.isFinite(value) ? value : null;
97
+ }
98
+
99
+ /** Convex owns lifecycle admission and validates every durable result. */
100
+ export function normalizeProviderMonitorLifecycleReservation(input: {
101
+ result: unknown;
102
+ state: 'provisioning' | 'deleting';
103
+ operationId: string;
104
+ }): ProviderMonitorLifecycleReservation {
105
+ const result = record(input.result);
106
+ if (!result || typeof result.kind !== 'string') {
107
+ throw new Error(
108
+ 'Provider monitor lifecycle admission returned an invalid control-plane result.',
109
+ );
110
+ }
111
+ if (result.kind === 'acquired') {
112
+ return {
113
+ kind: 'acquired',
114
+ state: input.state,
115
+ operationId: input.operationId,
116
+ };
117
+ }
118
+ if (result.kind === 'not_eligible') {
119
+ return { kind: 'not_eligible' };
120
+ }
121
+ if (
122
+ (result.kind !== 'in_progress' && result.kind !== 'deleting') ||
123
+ (result.state !== 'provisioning' && result.state !== 'deleting') ||
124
+ finiteNumber(result.retryAt) === null
125
+ ) {
126
+ throw new Error(
127
+ 'Provider monitor lifecycle admission returned an unknown control-plane result.',
128
+ );
129
+ }
130
+ if (result.kind === 'deleting' && result.state !== 'deleting') {
131
+ throw new Error(
132
+ 'Provider monitor lifecycle deletion admission returned an invalid operation state.',
133
+ );
134
+ }
135
+ if (result.kind === 'in_progress') {
136
+ return {
137
+ kind: 'in_progress',
138
+ state: result.state as 'provisioning' | 'deleting',
139
+ retryAt: result.retryAt as number,
140
+ };
141
+ }
142
+ return {
143
+ kind: 'deleting',
144
+ state: 'deleting',
145
+ retryAt: result.retryAt as number,
146
+ };
147
+ }
148
+
149
+ function claimFromSignature(
150
+ signature: ProviderMonitorUpstreamIdentitySignature,
151
+ ): ProviderMonitorIdentityClaim {
152
+ const identity = Object.fromEntries(
153
+ Object.entries(signature.key).sort(([a], [b]) => a.localeCompare(b)),
154
+ );
155
+ return {
156
+ tool: signature.tool,
157
+ identityKey: JSON.stringify(identity),
158
+ identity,
159
+ };
160
+ }
161
+
162
+ export function providerMonitorIdentityClaim(input: {
163
+ tool: string;
164
+ payload: Record<string, unknown> | null | undefined;
165
+ }) {
166
+ // Claims protect provider resources, including legacy rows whose public
167
+ // capability id was later retired. The Convex-safe identity contract owns
168
+ // those aliases; the live capability registry intentionally does not.
169
+ const signature = providerMonitorUpstreamIdentitySignature(
170
+ input.tool,
171
+ input.payload,
172
+ );
173
+ return signature ? claimFromSignature(signature) : null;
174
+ }
175
+
176
+ /**
177
+ * Makes a legacy Convex org safe for indexed identity admission. A new org
178
+ * gets one bounded page inline so normal small workspaces stay transparent;
179
+ * a larger org continues in a durable, no-provider-side-effect Convex job and
180
+ * receives a retryable response rather than scanning tens of thousands of
181
+ * legacy definitions synchronously. Later calls are a single complete-marker
182
+ * read.
183
+ */
184
+ export async function ensureProviderMonitorIdentityBackfill(input: {
185
+ auth: OrgAuthContext;
186
+ }) {
187
+ const client = getRequiredInternalConvexClient();
188
+ const globallyComplete = await client.query(
189
+ internal.providerMonitorIdentityClaims.globalBackfillComplete,
190
+ {},
191
+ );
192
+ if (globallyComplete) return;
193
+ const state = await client.query(
194
+ internal.providerMonitorIdentityClaims.backfillStatus,
195
+ { orgId: input.auth.orgId },
196
+ );
197
+ if (state === 'complete') return;
198
+
199
+ if (state === 'not_started') {
200
+ // The marker/fence is committed before the first page reads the legacy
201
+ // index. Therefore every caller, including a rolling pre-claim app
202
+ // process, reaches the current Convex upsert and cannot slip an unclaimed
203
+ // identity behind this cursor.
204
+ await client.mutation(
205
+ internal.providerMonitorIdentityClaims.startBackfill,
206
+ { orgId: input.auth.orgId, schedule: false },
207
+ );
208
+ const initialPage = await client.mutation(
209
+ internal.providerMonitorIdentityClaims.backfillNextPage,
210
+ { orgId: input.auth.orgId },
211
+ );
212
+ if (initialPage.complete) return;
213
+ }
214
+
215
+ const started = await client.mutation(
216
+ internal.providerMonitorIdentityClaims.startBackfill,
217
+ { orgId: input.auth.orgId },
218
+ );
219
+ if (started.state === 'complete') return;
220
+ throw new ProviderMonitorIdentityBackfillInProgressError();
221
+ }
222
+
223
+ export async function getProviderMonitorIdentityOwner(input: {
224
+ auth: OrgAuthContext;
225
+ claim: ProviderMonitorIdentityClaim;
226
+ }) {
227
+ return getRequiredInternalConvexClient().query(
228
+ internal.providerMonitorIdentityClaims.get,
229
+ {
230
+ orgId: input.auth.orgId,
231
+ tool: input.claim.tool,
232
+ identityKey: input.claim.identityKey,
233
+ },
234
+ );
235
+ }
236
+
237
+ export function providerMonitorIdentityProvisioningForMonitor(input: {
238
+ auth: OrgAuthContext;
239
+ monitorKey: string;
240
+ }) {
241
+ return getRequiredInternalConvexClient().query(
242
+ internal.providerMonitorIdentityClaims.provisioningForMonitor,
243
+ { orgId: input.auth.orgId, monitorKey: input.monitorKey },
244
+ );
245
+ }
246
+
247
+ export function priorityMonitorExecutionLimitFromLifecycleError(
248
+ error: unknown,
249
+ ) {
250
+ if (
251
+ error instanceof Error &&
252
+ error.message.includes('PRIORITY_MONITOR_LIMIT_EXCEEDED')
253
+ ) {
254
+ return new PriorityMonitorExecutionLimitError(
255
+ MAX_PRIORITY_MONITORS_PER_ORG,
256
+ );
257
+ }
258
+ return null;
259
+ }
260
+
261
+ // Convex retries mutations itself. This is the bounded outer retry for the
262
+ // rare case in which that budget is exhausted while the short capacity-fold
263
+ // mutation is updating the same organization fence. Lifecycle admission is
264
+ // still pre-provider and reuses one operation id, so replaying it cannot make
265
+ // an upstream monitor, charge, or fence duplicate.
266
+ const LIFECYCLE_ADMISSION_WRITE_CONFLICT =
267
+ /write conflict|optimisticconcurrencycontrolfailure|documents read from or written to .* changed while this mutation was being run/i;
268
+ const MAX_LIFECYCLE_ADMISSION_RETRIES = 3;
269
+
270
+ function lifecycleAdmissionWriteConflict(error: unknown) {
271
+ const message =
272
+ error instanceof Error
273
+ ? error.message
274
+ : typeof error === 'string'
275
+ ? error
276
+ : '';
277
+ return LIFECYCLE_ADMISSION_WRITE_CONFLICT.test(message);
278
+ }
279
+
280
+ async function sleep(ms: number) {
281
+ await new Promise((resolve) => setTimeout(resolve, ms));
282
+ }
283
+
284
+ export async function retryProviderMonitorLifecycleAdmission<T>(
285
+ operation: () => Promise<T>,
286
+ ): Promise<T> {
287
+ for (
288
+ let attempt = 0;
289
+ attempt <= MAX_LIFECYCLE_ADMISSION_RETRIES;
290
+ attempt += 1
291
+ ) {
292
+ try {
293
+ return await operation();
294
+ } catch (error) {
295
+ if (
296
+ !lifecycleAdmissionWriteConflict(error) ||
297
+ attempt === MAX_LIFECYCLE_ADMISSION_RETRIES
298
+ ) {
299
+ throw error;
300
+ }
301
+ await sleep(25 * 2 ** attempt);
302
+ }
303
+ }
304
+ throw new Error('Provider monitor lifecycle admission retry exhausted.');
305
+ }
306
+
307
+ export async function beginProviderMonitorLifecycleReservation(input: {
308
+ auth: OrgAuthContext;
309
+ monitorKey: string;
310
+ /** Tool and payload of the monitor being provisioned, so capacity can be
311
+ * counted against the account it watches rather than against the row. */
312
+ entityKey?: string | null;
313
+ state: 'provisioning' | 'deleting';
314
+ priorityExecutionSlots?: number;
315
+ expectedControlStateRevision?: number;
316
+ requiredMonitorStatus?: 'active';
317
+ }): Promise<ProviderMonitorLifecycleReservation> {
318
+ const operationId = randomUUID();
319
+ const action =
320
+ input.state === 'provisioning'
321
+ ? internal.providerMonitorIdentityClaims.beginLifecycleProvisioning
322
+ : internal.providerMonitorIdentityClaims.beginLifecycleDeletion;
323
+ const request = () =>
324
+ getRequiredInternalConvexClient().mutation(action, {
325
+ orgId: input.auth.orgId,
326
+ monitorKey: input.monitorKey,
327
+ operationId,
328
+ ...(input.entityKey ? { entityKey: input.entityKey } : {}),
329
+ ...(input.priorityExecutionSlots === 1
330
+ ? { priorityExecutionSlots: 1 }
331
+ : {}),
332
+ ...(typeof input.expectedControlStateRevision === 'number'
333
+ ? { expectedControlStateRevision: input.expectedControlStateRevision }
334
+ : {}),
335
+ ...(input.requiredMonitorStatus
336
+ ? { requiredMonitorStatus: input.requiredMonitorStatus }
337
+ : {}),
338
+ });
339
+ // A priority-slot refusal arrives as a thrown lifecycle error, while the
340
+ // capacity outcomes below arrive as a returned kind. Both paths have to work
341
+ // on the same call, and the backfill retry has to re-run the same request.
342
+ const attempt = async () => {
343
+ try {
344
+ return await request();
345
+ } catch (error) {
346
+ const priorityLimit =
347
+ priorityMonitorExecutionLimitFromLifecycleError(error);
348
+ if (priorityLimit) throw priorityLimit;
349
+ throw error;
350
+ }
351
+ };
352
+ let result = await retryProviderMonitorLifecycleAdmission(attempt);
353
+ if (result.kind === 'backfill_required') {
354
+ await ensureProviderMonitorCapacityBackfill({ auth: input.auth });
355
+ result = await retryProviderMonitorLifecycleAdmission(attempt);
356
+ }
357
+ if (result.kind === 'limit_exceeded') {
358
+ throw new ProviderMonitorOrgLimitExceededError(result.capacity);
359
+ }
360
+ if (result.kind === 'entity_limit_exceeded') {
361
+ throw new ProviderMonitorEntityLimitExceededError(
362
+ result.entityKey,
363
+ result.capacity.monitors_per_entity,
364
+ );
365
+ }
366
+ if (result.kind === 'cleanup_blocked') {
367
+ throw new ProviderMonitorCapacityCleanupRequiredError(input.monitorKey);
368
+ }
369
+ if (result.kind === 'backfill_required') {
370
+ throw new ProviderMonitorCapacityBackfillInProgressError();
371
+ }
372
+ return normalizeProviderMonitorLifecycleReservation({
373
+ result,
374
+ state: input.state,
375
+ operationId,
376
+ });
377
+ }
378
+
379
+ async function ensureProviderMonitorCapacityBackfill(input: {
380
+ auth: OrgAuthContext;
381
+ }) {
382
+ const client = getRequiredInternalConvexClient();
383
+ const state = await client.query(
384
+ internal.providerMonitorCapacity.backfillStatus,
385
+ { orgId: input.auth.orgId },
386
+ );
387
+ if (state === 'complete') return;
388
+
389
+ if (state === 'not_started') {
390
+ await client.mutation(internal.providerMonitorCapacity.startBackfill, {
391
+ orgId: input.auth.orgId,
392
+ schedule: false,
393
+ });
394
+ const initialPage = await client.mutation(
395
+ internal.providerMonitorCapacity.backfillNextPage,
396
+ { orgId: input.auth.orgId },
397
+ );
398
+ if (initialPage.complete) return;
399
+ }
400
+
401
+ const started = await client.mutation(
402
+ internal.providerMonitorCapacity.startBackfill,
403
+ { orgId: input.auth.orgId },
404
+ );
405
+ if (started.state === 'complete') return;
406
+ throw new ProviderMonitorCapacityBackfillInProgressError();
407
+ }
408
+
409
+ export function failProviderMonitorLifecycleReservation(input: {
410
+ auth: OrgAuthContext;
411
+ monitorKey: string;
412
+ reservation: Extract<
413
+ ProviderMonitorLifecycleReservation,
414
+ { kind: 'acquired' }
415
+ >;
416
+ }) {
417
+ return getRequiredInternalConvexClient().mutation(
418
+ internal.providerMonitorIdentityClaims.failLifecycleOperation,
419
+ {
420
+ orgId: input.auth.orgId,
421
+ monitorKey: input.monitorKey,
422
+ state: input.reservation.state,
423
+ operationId: input.reservation.operationId,
424
+ },
425
+ );
426
+ }
427
+
428
+ export async function beginProviderMonitorIdentityReservation(input: {
429
+ auth: OrgAuthContext;
430
+ monitorKey: string;
431
+ tool: string;
432
+ payload: Record<string, unknown> | null | undefined;
433
+ /**
434
+ * Resume rather than wait when THIS monitor key already holds a provisioning
435
+ * claim. Only for callers that serialise their own passes for a member -
436
+ * see `reclaimSelfProvisioning` on the mutation.
437
+ */
438
+ reclaimSelfProvisioning?: boolean;
439
+ }): Promise<ProviderMonitorIdentityReservation | null> {
440
+ const claim = providerMonitorIdentityClaim(input);
441
+ if (!claim) return null;
442
+ await ensureProviderMonitorIdentityBackfill({ auth: input.auth });
443
+ const operationId = randomUUID();
444
+ const result = await getRequiredInternalConvexClient().mutation(
445
+ internal.providerMonitorIdentityClaims.begin,
446
+ {
447
+ orgId: input.auth.orgId,
448
+ tool: claim.tool,
449
+ identityKey: claim.identityKey,
450
+ monitorKey: input.monitorKey,
451
+ operationId,
452
+ ...(input.reclaimSelfProvisioning
453
+ ? { reclaimSelfProvisioning: true }
454
+ : {}),
455
+ },
456
+ );
457
+ if (result.kind === 'acquired') {
458
+ return {
459
+ kind: 'acquired',
460
+ claim,
461
+ restoredState: result.restoredState,
462
+ operationId,
463
+ };
464
+ }
465
+ if (result.kind === 'occupied') {
466
+ return { ...result, claim };
467
+ }
468
+ return { ...result, claim };
469
+ }
470
+
471
+ async function mutateClaim(
472
+ action: 'activate' | 'release',
473
+ input: {
474
+ auth: OrgAuthContext;
475
+ monitorKey: string;
476
+ claim: ProviderMonitorIdentityClaim;
477
+ operationId?: string;
478
+ },
479
+ ) {
480
+ return getRequiredInternalConvexClient().mutation(
481
+ internal.providerMonitorIdentityClaims[action],
482
+ {
483
+ orgId: input.auth.orgId,
484
+ tool: input.claim.tool,
485
+ identityKey: input.claim.identityKey,
486
+ monitorKey: input.monitorKey,
487
+ ...(input.operationId ? { operationId: input.operationId } : {}),
488
+ },
489
+ );
490
+ }
491
+
492
+ export function activateProviderMonitorIdentityReservation(input: {
493
+ auth: OrgAuthContext;
494
+ monitorKey: string;
495
+ claim: ProviderMonitorIdentityClaim;
496
+ operationId: string;
497
+ }) {
498
+ return mutateClaim('activate', input);
499
+ }
500
+
501
+ export function releaseProviderMonitorIdentityClaim(input: {
502
+ auth: OrgAuthContext;
503
+ monitorKey: string;
504
+ claim: ProviderMonitorIdentityClaim;
505
+ }) {
506
+ return mutateClaim('release', input);
507
+ }
508
+
509
+ export function failProviderMonitorIdentityReservation(input: {
510
+ auth: OrgAuthContext;
511
+ monitorKey: string;
512
+ reservation: Extract<
513
+ ProviderMonitorIdentityReservation,
514
+ { kind: 'acquired' }
515
+ >;
516
+ }) {
517
+ return getRequiredInternalConvexClient().mutation(
518
+ internal.providerMonitorIdentityClaims.fail,
519
+ {
520
+ orgId: input.auth.orgId,
521
+ tool: input.reservation.claim.tool,
522
+ identityKey: input.reservation.claim.identityKey,
523
+ monitorKey: input.monitorKey,
524
+ operationId: input.reservation.operationId,
525
+ },
526
+ );
527
+ }