deepline 0.3.88 → 0.3.89

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 (121) hide show
  1. package/dist/bundling-sources/sdk/src/client.ts +534 -4
  2. package/dist/bundling-sources/sdk/src/config.ts +77 -0
  3. package/dist/bundling-sources/sdk/src/http.ts +76 -30
  4. package/dist/bundling-sources/sdk/src/index.ts +6 -0
  5. package/dist/bundling-sources/sdk/src/monitor-fleet-contract.ts +1 -1
  6. package/dist/bundling-sources/sdk/src/play.ts +15 -1
  7. package/dist/bundling-sources/sdk/src/plays/bundle-play-file.ts +2 -2
  8. package/dist/bundling-sources/sdk/src/release.ts +1 -1
  9. package/dist/bundling-sources/sdk/src/types.ts +83 -4
  10. package/dist/bundling-sources/shared_libs/integration-contracts/AGENTS.md +9 -0
  11. package/dist/bundling-sources/shared_libs/integration-contracts/README.md +3 -0
  12. package/dist/bundling-sources/shared_libs/integration-contracts/index.ts +2 -0
  13. package/dist/bundling-sources/shared_libs/monitors/analytics-install-guides.ts +77 -0
  14. package/dist/bundling-sources/shared_libs/monitors/billing/charges.ts +125 -0
  15. package/dist/bundling-sources/shared_libs/monitors/billing/policy-builders.ts +686 -0
  16. package/dist/bundling-sources/shared_libs/monitors/billing/pricing.ts +54 -0
  17. package/dist/bundling-sources/shared_libs/monitors/billing/public-pricing.ts +72 -0
  18. package/dist/bundling-sources/shared_libs/monitors/billing/session.ts +45 -0
  19. package/dist/bundling-sources/shared_libs/monitors/billing/state.ts +454 -0
  20. package/dist/bundling-sources/shared_libs/monitors/billing/tamradar.ts +260 -0
  21. package/dist/bundling-sources/shared_libs/monitors/billing/types.ts +215 -0
  22. package/dist/bundling-sources/shared_libs/monitors/callback-security.ts +333 -0
  23. package/dist/bundling-sources/shared_libs/monitors/cursor-page.ts +42 -0
  24. package/dist/bundling-sources/shared_libs/monitors/deactivation-reasons.ts +16 -0
  25. package/dist/bundling-sources/shared_libs/monitors/framework-managed-payload-fields.ts +71 -0
  26. package/dist/bundling-sources/shared_libs/monitors/generated/provider-monitor-callback-admission-targets.ts +16 -0
  27. package/dist/bundling-sources/shared_libs/monitors/http-error-response.ts +267 -0
  28. package/dist/bundling-sources/shared_libs/monitors/inbound-publish-key.ts +39 -0
  29. package/dist/bundling-sources/shared_libs/monitors/inbound-queue.ts +82 -0
  30. package/dist/bundling-sources/shared_libs/monitors/lifecycle-notifications.ts +473 -0
  31. package/dist/bundling-sources/shared_libs/monitors/org-monitor-limits.ts +1 -1
  32. package/dist/bundling-sources/shared_libs/monitors/package.json +29 -0
  33. package/dist/bundling-sources/shared_libs/monitors/provider-callback-verification.ts +1125 -0
  34. package/dist/bundling-sources/shared_libs/monitors/provider-monitor-batch-operation.ts +263 -0
  35. package/dist/bundling-sources/shared_libs/monitors/provider-monitor-capability.ts +964 -0
  36. package/dist/bundling-sources/shared_libs/monitors/provider-monitor-consumer-health.ts +56 -0
  37. package/dist/bundling-sources/shared_libs/monitors/provider-monitor-errors.ts +131 -0
  38. package/dist/bundling-sources/shared_libs/monitors/provider-monitor-identity-claims.ts +535 -0
  39. package/dist/bundling-sources/shared_libs/monitors/provider-monitor-legacy-aliases.ts +16 -0
  40. package/dist/bundling-sources/shared_libs/monitors/provider-monitor-priority-execution.ts +56 -0
  41. package/dist/bundling-sources/shared_libs/monitors/provider-monitor-public-aliases.ts +28 -0
  42. package/dist/bundling-sources/shared_libs/monitors/provider-monitor-runtime-read-eligibility.ts +38 -0
  43. package/dist/bundling-sources/shared_libs/monitors/provider-monitor-state.ts +122 -0
  44. package/dist/bundling-sources/shared_libs/monitors/provider-monitor-status.ts +10 -0
  45. package/dist/bundling-sources/shared_libs/monitors/provider-monitor-upstream-identity.ts +112 -0
  46. package/dist/bundling-sources/shared_libs/monitors/provider-webhook-monitor-runtime.ts +300 -0
  47. package/dist/bundling-sources/shared_libs/monitors/quickstart-prompts.ts +22 -0
  48. package/dist/bundling-sources/shared_libs/monitors/snitcher-install-validation.ts +93 -0
  49. package/dist/bundling-sources/shared_libs/monitors/snitcher-ip2company.ts +332 -0
  50. package/dist/bundling-sources/shared_libs/monitors/snitcher-radar.ts +221 -0
  51. package/dist/bundling-sources/shared_libs/monitors/starter-templates.ts +105 -0
  52. package/dist/bundling-sources/shared_libs/monitors/tracking-provider-guidance.ts +73 -0
  53. package/dist/bundling-sources/shared_libs/monitors/tracking-provider-integrations.ts +81 -0
  54. package/dist/bundling-sources/shared_libs/monitors/tsconfig.json +10 -0
  55. package/dist/bundling-sources/shared_libs/play-runtime/activity-observation.ts +6 -11
  56. package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +90 -12
  57. package/dist/bundling-sources/shared_libs/play-runtime/async-play-invocation.ts +78 -0
  58. package/dist/bundling-sources/shared_libs/play-runtime/checkpoint-boundaries.ts +70 -0
  59. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +407 -53
  60. package/dist/bundling-sources/shared_libs/play-runtime/coordinator-headers.ts +1 -32
  61. package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +57 -0
  62. package/dist/bundling-sources/shared_libs/play-runtime/default-batch-strategies.ts +20 -1
  63. package/dist/bundling-sources/shared_libs/play-runtime/execution-capabilities.ts +6 -0
  64. package/dist/bundling-sources/shared_libs/play-runtime/execution-protocol.ts +7 -1
  65. package/dist/bundling-sources/shared_libs/play-runtime/lane-retirement-gate.ts +189 -0
  66. package/dist/bundling-sources/shared_libs/play-runtime/play-runtime-semantics.ts +28 -0
  67. package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +2 -0
  68. package/dist/bundling-sources/shared_libs/play-runtime/run-failure.ts +23 -0
  69. package/dist/bundling-sources/shared_libs/play-runtime/run-snapshot-stream.ts +1 -1
  70. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-lifecycle.ts +18 -4
  71. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-start-admission.ts +68 -0
  72. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona.ts +2 -0
  73. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/runtime-sandbox-reconciliation.ts +1 -0
  74. package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +10 -8
  75. package/dist/bundling-sources/shared_libs/play-runtime/runtime-capacity-policy.ts +20 -0
  76. package/dist/bundling-sources/shared_libs/play-runtime/runtime-contract.ts +6 -0
  77. package/dist/bundling-sources/shared_libs/play-runtime/runtime-env.ts +6 -0
  78. package/dist/bundling-sources/shared_libs/play-runtime/runtime-scheduler-topology.ts +24 -0
  79. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backend.ts +30 -1
  80. package/dist/bundling-sources/shared_libs/play-runtime/tool-http-errors.ts +1 -1
  81. package/dist/bundling-sources/shared_libs/play-runtime/tool-result.ts +1 -1
  82. package/dist/bundling-sources/shared_libs/plays/artifact-types.ts +6 -3
  83. package/dist/bundling-sources/shared_libs/plays/authoring-contract.ts +454 -17
  84. package/dist/bundling-sources/shared_libs/plays/bundling/index.ts +19 -4
  85. package/dist/bundling-sources/shared_libs/plays/contracts.ts +6 -14
  86. package/dist/bundling-sources/shared_libs/plays/core.ts +11 -6
  87. package/dist/bundling-sources/shared_libs/plays/docflow-binding-owner.ts +16 -11
  88. package/dist/bundling-sources/shared_libs/plays/email-status.ts +1 -1
  89. package/dist/bundling-sources/shared_libs/plays/resolve-static-pipeline.ts +1 -1
  90. package/dist/bundling-sources/shared_libs/plays/slug.ts +115 -0
  91. package/dist/bundling-sources/shared_libs/plays/static-pipeline.ts +44 -14
  92. package/dist/bundling-sources/shared_libs/plays/tool-category-descriptions.ts +2 -2
  93. package/dist/bundling-sources/shared_libs/product-notifications/catalog.ts +877 -0
  94. package/dist/bundling-sources/shared_libs/product-notifications/configuration-draft.ts +456 -0
  95. package/dist/bundling-sources/shared_libs/product-notifications/configuration.ts +560 -0
  96. package/dist/bundling-sources/shared_libs/product-notifications/content.ts +28 -0
  97. package/dist/bundling-sources/shared_libs/product-notifications/default-policy.ts +59 -0
  98. package/dist/bundling-sources/shared_libs/product-notifications/email.ts +246 -0
  99. package/dist/bundling-sources/shared_libs/product-notifications/evaluation.ts +280 -0
  100. package/dist/bundling-sources/shared_libs/product-notifications/events.ts +433 -0
  101. package/dist/bundling-sources/shared_libs/product-notifications/legacy-slack.ts +80 -0
  102. package/dist/bundling-sources/shared_libs/product-notifications/workspace.ts +38 -0
  103. package/dist/bundling-sources/shared_libs/runtime-env.ts +6 -0
  104. package/dist/bundling-sources/shared_libs/security/header-safe-env.ts +28 -0
  105. package/dist/bundling-sources/shared_libs/security/internal-actor.ts +50 -0
  106. package/dist/bundling-sources/shared_libs/security/secret-redaction.ts +267 -0
  107. package/dist/cli/index.js +1070 -193
  108. package/dist/cli/index.mjs +1057 -180
  109. package/dist/{compiler-manifest-Dbov16F7.d.mts → compiler-manifest-DsLbQaew.d.mts} +132 -8
  110. package/dist/{compiler-manifest-Dbov16F7.d.ts → compiler-manifest-DsLbQaew.d.ts} +132 -8
  111. package/dist/index.d.mts +834 -12
  112. package/dist/index.d.ts +834 -12
  113. package/dist/index.js +469 -40
  114. package/dist/index.mjs +469 -40
  115. package/dist/install-integrity.json +65 -4
  116. package/dist/plays/bundle-play-file.d.mts +8 -3
  117. package/dist/plays/bundle-play-file.d.ts +8 -3
  118. package/dist/plays/bundle-play-file.mjs +100 -20
  119. package/package.json +3 -1
  120. /package/dist/bundling-sources/shared_libs/{integrations/bettercontact-execution-policy.ts → integration-contracts/bettercontact/execution-policy.ts} +0 -0
  121. /package/dist/bundling-sources/shared_libs/{integrations/theirstack-execution-policy.ts → integration-contracts/theirstack/execution-policy.ts} +0 -0
@@ -0,0 +1,433 @@
1
+ import type {
2
+ ProductNotificationFacts,
3
+ ProductNotificationTrigger,
4
+ } from './configuration';
5
+ import type {
6
+ ProductNotificationObservation,
7
+ ProductNotificationOutcomeFact,
8
+ } from './evaluation';
9
+
10
+ export const PRODUCT_NOTIFICATION_EVENT_SCHEMA_VERSION = 1 as const;
11
+ export const PRODUCT_NOTIFICATION_MAX_EVENT_BYTES = 16_000;
12
+ export const PRODUCT_NOTIFICATION_MAX_EVENT_FACTS = 20;
13
+ export const PRODUCT_NOTIFICATION_MAX_EVENT_AGE_MS = 30 * 24 * 60 * 60 * 1_000;
14
+ export const PRODUCT_NOTIFICATION_MAX_SOURCE_CLOCK_SKEW_MS = 5 * 60 * 1_000;
15
+
16
+ export const ProductNotificationEvent = {
17
+ CreditsBalanceChanged: 'credits.balance.changed',
18
+ CreditsUsageRecorded: 'credits.usage.recorded',
19
+ MonitorLifecycleChanged: 'monitor.lifecycle.changed',
20
+ PlayLifecycleChanged: 'play.lifecycle.changed',
21
+ PlayRunCompleted: 'play.run.completed',
22
+ WebhookDeliveryCompleted: 'webhook.delivery.completed',
23
+ PlayCronSucceeded: 'play.cron.succeeded',
24
+ PlayCronFailed: 'play.cron.failed',
25
+ PlayWebhookSucceeded: 'play.webhook.succeeded',
26
+ PlayWebhookFailed: 'play.webhook.failed',
27
+ } as const;
28
+
29
+ export type ProductNotificationEvent =
30
+ (typeof ProductNotificationEvent)[keyof typeof ProductNotificationEvent];
31
+
32
+ export const ProductNotificationProducerKey = {
33
+ BillingCredits: 'billing.credits',
34
+ BillingUsage: 'billing.usage',
35
+ MonitorsLifecycle: 'monitors.lifecycle',
36
+ PlaysLifecycle: 'plays.lifecycle',
37
+ PlaysRuns: 'plays.runs',
38
+ WebhooksDelivery: 'webhooks.delivery',
39
+ LegacyProductNotifications: 'legacy.product_notifications',
40
+ } as const;
41
+
42
+ export type ProductNotificationProducerKey =
43
+ (typeof ProductNotificationProducerKey)[keyof typeof ProductNotificationProducerKey];
44
+
45
+ export const ProductNotificationTargetPath = {
46
+ Billing: '/dashboard/billing',
47
+ } as const;
48
+
49
+ export const PRODUCT_NOTIFICATION_EVENT_NAMES = [
50
+ ProductNotificationEvent.CreditsBalanceChanged,
51
+ ProductNotificationEvent.MonitorLifecycleChanged,
52
+ ProductNotificationEvent.PlayLifecycleChanged,
53
+ ProductNotificationEvent.PlayRunCompleted,
54
+ ProductNotificationEvent.WebhookDeliveryCompleted,
55
+ ProductNotificationEvent.CreditsUsageRecorded,
56
+ ] as const;
57
+
58
+ export const PRODUCT_NOTIFICATION_COMPATIBILITY_EVENT_NAMES = [
59
+ ProductNotificationEvent.PlayCronSucceeded,
60
+ ProductNotificationEvent.PlayCronFailed,
61
+ ProductNotificationEvent.PlayWebhookSucceeded,
62
+ ProductNotificationEvent.PlayWebhookFailed,
63
+ ] as const;
64
+
65
+ export type ProductNotificationEventName =
66
+ | (typeof PRODUCT_NOTIFICATION_EVENT_NAMES)[number]
67
+ | (typeof PRODUCT_NOTIFICATION_COMPATIBILITY_EVENT_NAMES)[number];
68
+
69
+ export type ProductNotificationCanonicalEvent = {
70
+ orgId: string;
71
+ producerKey: ProductNotificationProducerKey;
72
+ sourceEventId: string;
73
+ eventName: ProductNotificationEventName;
74
+ eventSchemaVersion: typeof PRODUCT_NOTIFICATION_EVENT_SCHEMA_VERSION;
75
+ occurredAt: number;
76
+ admittedAt: number;
77
+ payload: ProductNotificationFacts;
78
+ };
79
+
80
+ type FactType = 'string' | 'number' | 'boolean';
81
+ export type ProductNotificationEventDefinition = {
82
+ eventName: ProductNotificationEventName;
83
+ producerKey: ProductNotificationProducerKey;
84
+ requiredFacts: Record<string, FactType>;
85
+ optionalFacts?: Record<string, FactType>;
86
+ orderingSubjectFact: string;
87
+ allowedTriggers: ProductNotificationTrigger['kind'][];
88
+ allowedThresholdUnits?: string[];
89
+ };
90
+
91
+ export const PRODUCT_NOTIFICATION_EVENT_DEFINITIONS: readonly ProductNotificationEventDefinition[] =
92
+ [
93
+ {
94
+ eventName: ProductNotificationEvent.CreditsBalanceChanged,
95
+ producerKey: ProductNotificationProducerKey.BillingCredits,
96
+ requiredFacts: {
97
+ accountId: 'string',
98
+ previousCredits: 'number',
99
+ currentCredits: 'number',
100
+ targetUrl: 'string',
101
+ },
102
+ orderingSubjectFact: 'accountId',
103
+ allowedTriggers: ['threshold_crossing'],
104
+ allowedThresholdUnits: ['credits'],
105
+ },
106
+ {
107
+ eventName: ProductNotificationEvent.MonitorLifecycleChanged,
108
+ producerKey: ProductNotificationProducerKey.MonitorsLifecycle,
109
+ requiredFacts: {
110
+ monitorId: 'string',
111
+ monitorName: 'string',
112
+ previousState: 'string',
113
+ currentState: 'string',
114
+ reasonCode: 'string',
115
+ targetUrl: 'string',
116
+ },
117
+ orderingSubjectFact: 'monitorId',
118
+ optionalFacts: { previousReasonCode: 'string' },
119
+ allowedTriggers: ['state_transition'],
120
+ },
121
+ {
122
+ eventName: ProductNotificationEvent.PlayLifecycleChanged,
123
+ producerKey: ProductNotificationProducerKey.PlaysLifecycle,
124
+ requiredFacts: {
125
+ playId: 'string',
126
+ playName: 'string',
127
+ previousState: 'string',
128
+ currentState: 'string',
129
+ reasonCode: 'string',
130
+ targetUrl: 'string',
131
+ },
132
+ orderingSubjectFact: 'playId',
133
+ optionalFacts: { previousReasonCode: 'string' },
134
+ allowedTriggers: ['state_transition'],
135
+ },
136
+ {
137
+ eventName: ProductNotificationEvent.PlayRunCompleted,
138
+ producerKey: ProductNotificationProducerKey.PlaysRuns,
139
+ requiredFacts: {
140
+ playId: 'string',
141
+ playName: 'string',
142
+ runId: 'string',
143
+ outcome: 'string',
144
+ targetUrl: 'string',
145
+ },
146
+ optionalFacts: {
147
+ triggerKind: 'string',
148
+ },
149
+ orderingSubjectFact: 'playId',
150
+ allowedTriggers: ['each_match', 'consecutive_outcomes'],
151
+ },
152
+ {
153
+ eventName: ProductNotificationEvent.WebhookDeliveryCompleted,
154
+ producerKey: ProductNotificationProducerKey.WebhooksDelivery,
155
+ requiredFacts: {
156
+ endpointId: 'string',
157
+ endpointName: 'string',
158
+ deliveryId: 'string',
159
+ outcome: 'string',
160
+ targetUrl: 'string',
161
+ },
162
+ orderingSubjectFact: 'endpointId',
163
+ allowedTriggers: ['each_match', 'consecutive_outcomes'],
164
+ },
165
+ {
166
+ eventName: ProductNotificationEvent.CreditsUsageRecorded,
167
+ producerKey: ProductNotificationProducerKey.BillingUsage,
168
+ requiredFacts: {
169
+ accountId: 'string',
170
+ creditsUsed: 'number',
171
+ limitDenied: 'boolean',
172
+ targetUrl: 'string',
173
+ },
174
+ orderingSubjectFact: 'accountId',
175
+ allowedTriggers: ['threshold_crossing'],
176
+ allowedThresholdUnits: ['percent', 'usage'],
177
+ },
178
+ ...PRODUCT_NOTIFICATION_COMPATIBILITY_EVENT_NAMES.map((eventName) => ({
179
+ eventName,
180
+ producerKey: ProductNotificationProducerKey.LegacyProductNotifications,
181
+ requiredFacts: {
182
+ playId: 'string' as const,
183
+ playName: 'string' as const,
184
+ targetUrl: 'string' as const,
185
+ },
186
+ optionalFacts: {
187
+ runId: 'string' as const,
188
+ reasonCode: 'string' as const,
189
+ reason: 'string' as const,
190
+ },
191
+ orderingSubjectFact: 'playId',
192
+ allowedTriggers: ['each_match' as const],
193
+ })),
194
+ ];
195
+
196
+ export function getProductNotificationEventDefinition(
197
+ eventName: string,
198
+ ): ProductNotificationEventDefinition {
199
+ const definition = PRODUCT_NOTIFICATION_EVENT_DEFINITIONS.find(
200
+ (candidate) => candidate.eventName === eventName,
201
+ );
202
+ if (!definition) throw new Error(`Unknown notification event ${eventName}.`);
203
+ return definition;
204
+ }
205
+
206
+ function requiredString(value: unknown, field: string) {
207
+ if (typeof value !== 'string' || !value.trim()) {
208
+ throw new Error(`${field} must be a non-empty string.`);
209
+ }
210
+ if (value.length > 512) throw new Error(`${field} is too long.`);
211
+ }
212
+
213
+ function requireInternalTargetPath(value: unknown) {
214
+ requiredString(value, 'targetUrl');
215
+ if (
216
+ typeof value !== 'string' ||
217
+ !value.startsWith('/') ||
218
+ value.startsWith('//') ||
219
+ value.includes('\\') ||
220
+ /[\u0000-\u001f\u007f]/.test(value)
221
+ ) {
222
+ throw new Error('targetUrl must be a safe internal path.');
223
+ }
224
+ }
225
+
226
+ /** Validate the semantic event contract after the producer transaction commits. */
227
+ export function validateProductNotificationEvent(
228
+ event: ProductNotificationCanonicalEvent,
229
+ now: number,
230
+ ) {
231
+ const definition = getProductNotificationEventDefinition(event.eventName);
232
+ requiredString(event.orgId, 'orgId');
233
+ requiredString(event.producerKey, 'producerKey');
234
+ requiredString(event.sourceEventId, 'sourceEventId');
235
+ if (event.producerKey !== definition.producerKey) {
236
+ throw new Error(`${event.producerKey} cannot produce ${event.eventName}.`);
237
+ }
238
+ if (event.eventSchemaVersion !== PRODUCT_NOTIFICATION_EVENT_SCHEMA_VERSION) {
239
+ throw new Error(
240
+ `Unsupported ${event.eventName} schema version ${event.eventSchemaVersion}.`,
241
+ );
242
+ }
243
+ if (
244
+ !Number.isFinite(event.occurredAt) ||
245
+ !Number.isFinite(event.admittedAt) ||
246
+ !Number.isFinite(now)
247
+ ) {
248
+ throw new Error('Event timestamps must be finite.');
249
+ }
250
+ if (event.occurredAt < now - PRODUCT_NOTIFICATION_MAX_EVENT_AGE_MS) {
251
+ throw new Error('Notification event is older than 30 days.');
252
+ }
253
+ if (event.admittedAt > now) {
254
+ throw new Error('Notification admission time is in the future.');
255
+ }
256
+ if (
257
+ event.occurredAt >
258
+ event.admittedAt + PRODUCT_NOTIFICATION_MAX_SOURCE_CLOCK_SKEW_MS
259
+ ) {
260
+ throw new Error(
261
+ 'Notification event exceeds the allowed source clock skew.',
262
+ );
263
+ }
264
+ const allowedFacts = {
265
+ ...definition.requiredFacts,
266
+ ...definition.optionalFacts,
267
+ };
268
+ const entries = Object.entries(event.payload);
269
+ if (entries.length > PRODUCT_NOTIFICATION_MAX_EVENT_FACTS) {
270
+ throw new Error('Notification event contains too many facts.');
271
+ }
272
+ for (const [fact, expectedType] of Object.entries(definition.requiredFacts)) {
273
+ const value = event.payload[fact];
274
+ if (typeof value !== expectedType) {
275
+ throw new Error(`${event.eventName}.${fact} must be ${expectedType}.`);
276
+ }
277
+ if (expectedType === 'string') requiredString(value, fact);
278
+ if (expectedType === 'number' && !Number.isFinite(value)) {
279
+ throw new Error(`${event.eventName}.${fact} must be finite.`);
280
+ }
281
+ }
282
+ for (const [fact, value] of entries) {
283
+ const expectedType = allowedFacts[fact];
284
+ if (!expectedType) {
285
+ throw new Error(`${event.eventName} does not accept fact ${fact}.`);
286
+ }
287
+ if (typeof value !== expectedType) {
288
+ throw new Error(`${event.eventName}.${fact} must be ${expectedType}.`);
289
+ }
290
+ if (expectedType === 'string') requiredString(value, fact);
291
+ if (expectedType === 'number' && !Number.isFinite(value)) {
292
+ throw new Error(`${event.eventName}.${fact} must be finite.`);
293
+ }
294
+ }
295
+ requireInternalTargetPath(event.payload.targetUrl);
296
+ if (
297
+ new TextEncoder().encode(JSON.stringify(event)).byteLength >
298
+ PRODUCT_NOTIFICATION_MAX_EVENT_BYTES
299
+ ) {
300
+ throw new Error('Notification event exceeds its byte bound.');
301
+ }
302
+ return definition;
303
+ }
304
+
305
+ export function productNotificationOrderingKey(
306
+ event: ProductNotificationCanonicalEvent,
307
+ ) {
308
+ const definition = getProductNotificationEventDefinition(event.eventName);
309
+ return `${event.eventName}/${String(
310
+ event.payload[definition.orderingSubjectFact],
311
+ )}`;
312
+ }
313
+
314
+ export function compareProductNotificationOrder(
315
+ left: { occurredAt: number; sourceEventId: string },
316
+ right: { occurredAt: number; sourceEventId: string },
317
+ ) {
318
+ return (
319
+ left.occurredAt - right.occurredAt ||
320
+ left.sourceEventId.localeCompare(right.sourceEventId)
321
+ );
322
+ }
323
+
324
+ export function productNotificationCanonicalOrder(
325
+ event: Pick<
326
+ ProductNotificationCanonicalEvent,
327
+ 'occurredAt' | 'sourceEventId'
328
+ >,
329
+ ) {
330
+ return [event.occurredAt, event.sourceEventId] as const;
331
+ }
332
+
333
+ export function productNotificationTriggerIsAllowed(
334
+ eventName: ProductNotificationEventName,
335
+ trigger: ProductNotificationTrigger,
336
+ ) {
337
+ const definition = getProductNotificationEventDefinition(eventName);
338
+ if (!definition.allowedTriggers.includes(trigger.kind)) return false;
339
+ return (
340
+ trigger.kind !== 'threshold_crossing' ||
341
+ trigger.unit === undefined ||
342
+ definition.allowedThresholdUnits?.includes(trigger.unit) === true
343
+ );
344
+ }
345
+
346
+ /** Convert a registered event into the generic observation a trigger consumes. */
347
+ export function adaptProductNotificationObservation(input: {
348
+ event: ProductNotificationCanonicalEvent;
349
+ trigger: ProductNotificationTrigger;
350
+ outcomeHistory?: ProductNotificationOutcomeFact[];
351
+ }): ProductNotificationObservation {
352
+ const { event, trigger } = input;
353
+ if (!productNotificationTriggerIsAllowed(event.eventName, trigger)) {
354
+ throw new Error(`${trigger.kind} is not allowed for ${event.eventName}.`);
355
+ }
356
+ if (trigger.kind === 'each_match') return { kind: 'match' };
357
+ if (trigger.kind === 'state_transition') {
358
+ return {
359
+ kind: 'state_transition',
360
+ previousState: String(event.payload.previousState),
361
+ currentState: String(event.payload.currentState),
362
+ previousReasonCode:
363
+ typeof event.payload.previousReasonCode === 'string'
364
+ ? event.payload.previousReasonCode
365
+ : undefined,
366
+ currentReasonCode: String(event.payload.reasonCode),
367
+ reasonCode: String(event.payload.reasonCode),
368
+ };
369
+ }
370
+ if (trigger.kind === 'consecutive_outcomes') {
371
+ if (!input.outcomeHistory) {
372
+ throw new Error(`${event.eventName} requires bounded outcome history.`);
373
+ }
374
+ const current = input.outcomeHistory[0];
375
+ const orderingKey = productNotificationOrderingKey(event);
376
+ if (
377
+ !current ||
378
+ input.outcomeHistory.some(
379
+ (outcome) => outcome.orderingKey !== orderingKey,
380
+ ) ||
381
+ current.sourceEventId !== event.sourceEventId ||
382
+ current.occurredAt !== event.occurredAt ||
383
+ current.outcome !== event.payload.outcome
384
+ ) {
385
+ throw new Error(
386
+ `${event.eventName} outcome history must begin with the current event.`,
387
+ );
388
+ }
389
+ return { kind: 'outcome_history', outcomes: input.outcomeHistory };
390
+ }
391
+ if (event.eventName === ProductNotificationEvent.CreditsBalanceChanged) {
392
+ return {
393
+ kind: 'threshold_crossing',
394
+ previousValue: Number(event.payload.previousCredits),
395
+ currentValue: Number(event.payload.currentCredits),
396
+ unit: 'credits',
397
+ };
398
+ }
399
+ const limit = Number(event.payload.limit);
400
+ if (event.eventName === ProductNotificationEvent.CreditsUsageRecorded) {
401
+ if (!(limit > 0)) throw new Error('Monthly usage limit must be positive.');
402
+ return {
403
+ kind: 'threshold_state',
404
+ currentValue: (Number(event.payload.currentUsage) / limit) * 100,
405
+ unit: 'percent',
406
+ };
407
+ }
408
+ throw new Error(`${event.eventName} has no threshold observation adapter.`);
409
+ }
410
+
411
+ /** Fingerprint identity excludes admission time so forwarding retries are equal. */
412
+ export async function productNotificationEventFingerprint(
413
+ event: ProductNotificationCanonicalEvent,
414
+ ) {
415
+ const canonical = JSON.stringify({
416
+ orgId: event.orgId,
417
+ producerKey: event.producerKey,
418
+ sourceEventId: event.sourceEventId,
419
+ eventName: event.eventName,
420
+ eventSchemaVersion: event.eventSchemaVersion,
421
+ occurredAt: event.occurredAt,
422
+ payload: Object.fromEntries(
423
+ Object.entries(event.payload).sort(([left], [right]) =>
424
+ left.localeCompare(right),
425
+ ),
426
+ ),
427
+ });
428
+ const bytes = new TextEncoder().encode(canonical);
429
+ const digest = await crypto.subtle.digest('SHA-256', bytes);
430
+ return [...new Uint8Array(digest)]
431
+ .map((value) => value.toString(16).padStart(2, '0'))
432
+ .join('');
433
+ }
@@ -0,0 +1,80 @@
1
+ import type {
2
+ ProductNotificationDefaultGroup,
3
+ ProductNotificationDefaultRule,
4
+ } from './configuration';
5
+ import {
6
+ PRODUCT_NOTIFICATION_EVENT_CATALOG,
7
+ type ProductNotificationEventType,
8
+ } from './contract';
9
+
10
+ const TYPE_BY_LEGACY_EVENT = {
11
+ 'play.cron.succeeded': 'compat.play.cron.succeeded',
12
+ 'play.cron.failed': 'compat.play.cron.failed',
13
+ 'play.webhook.succeeded': 'compat.play.webhook.succeeded',
14
+ 'play.webhook.failed': 'compat.play.webhook.failed',
15
+ } as const;
16
+
17
+ export function legacySlackNotificationType(
18
+ eventType: ProductNotificationEventType,
19
+ ) {
20
+ return TYPE_BY_LEGACY_EVENT[eventType];
21
+ }
22
+
23
+ export function legacySlackNotificationTypeMetadata(notificationType: string) {
24
+ const event = PRODUCT_NOTIFICATION_EVENT_CATALOG.find(
25
+ ({ id }) => TYPE_BY_LEGACY_EVENT[id] === notificationType,
26
+ );
27
+ return event
28
+ ? { name: event.label, description: event.description }
29
+ : undefined;
30
+ }
31
+
32
+ function legacyPrefix(destinationId: string) {
33
+ if (!destinationId)
34
+ throw new Error('Legacy Slack destination ID is required.');
35
+ return `legacy.slack.${destinationId}`;
36
+ }
37
+
38
+ export function isLegacySlackCompatibilityKey(value: string) {
39
+ return value.startsWith('legacy.slack.');
40
+ }
41
+
42
+ export function legacySlackGroupKey(destinationId: string) {
43
+ return `${legacyPrefix(destinationId)}.group`;
44
+ }
45
+
46
+ export function legacySlackTargetConfiguration(input: {
47
+ destinationId: string;
48
+ destinationName: string;
49
+ subscriptions: readonly {
50
+ eventType: ProductNotificationEventType;
51
+ enabled: boolean;
52
+ }[];
53
+ }): {
54
+ group: ProductNotificationDefaultGroup;
55
+ rules: ProductNotificationDefaultRule[];
56
+ } {
57
+ const prefix = legacyPrefix(input.destinationId);
58
+ return {
59
+ group: {
60
+ groupKey: legacySlackGroupKey(input.destinationId),
61
+ name: input.destinationName,
62
+ enabled: true,
63
+ deliveryTiming: { mode: 'immediate' },
64
+ },
65
+ rules: input.subscriptions.map((subscription) => ({
66
+ ruleKey: `${prefix}.${subscription.eventType}`,
67
+ groupKey: legacySlackGroupKey(input.destinationId),
68
+ enabled: subscription.enabled,
69
+ notificationType: legacySlackNotificationType(subscription.eventType),
70
+ eventName: subscription.eventType,
71
+ eventFilter: {},
72
+ trigger: { kind: 'each_match' },
73
+ repeatMode: 'every_match',
74
+ })),
75
+ };
76
+ }
77
+
78
+ export function legacySlackDestinationKey(destinationId: string) {
79
+ return `${legacyPrefix(destinationId)}.destination`;
80
+ }
@@ -0,0 +1,38 @@
1
+ export type NotificationWorkspace = {
2
+ workspaceId: string;
3
+ workspaceName?: string;
4
+ appUrl: string;
5
+ };
6
+
7
+ export function workspaceAppUrl(
8
+ baseUrl: string,
9
+ workspaceId: string,
10
+ path: string,
11
+ ): string {
12
+ const target = new URL(path, baseUrl);
13
+ if (
14
+ target.origin !== new URL(baseUrl).origin ||
15
+ path.startsWith('//') ||
16
+ path.includes('\\')
17
+ ) {
18
+ throw new Error('Notification target must be a safe internal path.');
19
+ }
20
+ const url = new URL('/notifications/open', baseUrl);
21
+ url.searchParams.set('workspace', workspaceId);
22
+ url.searchParams.set(
23
+ 'next',
24
+ `${target.pathname}${target.search}${target.hash}`,
25
+ );
26
+ return url.toString();
27
+ }
28
+
29
+ export function notificationSettingsUrl(
30
+ workspace: NotificationWorkspace,
31
+ groupKey: string,
32
+ ): string {
33
+ return workspaceAppUrl(
34
+ workspace.appUrl,
35
+ workspace.workspaceId,
36
+ `/dashboard/settings/notifications?group=${encodeURIComponent(groupKey)}`,
37
+ );
38
+ }
@@ -53,3 +53,9 @@ export function getRuntimeEnvInfo() {
53
53
  node_env: process.env.NODE_ENV ?? null,
54
54
  };
55
55
  }
56
+
57
+ /** Test seams must never be reachable from a Vercel production deployment. */
58
+ export function isTestOnlyRuntime(): boolean {
59
+ if (process.env.VERCEL_ENV === 'production') return false;
60
+ return process.env.VERCEL_ENV === 'preview' || getRuntimeEnv() !== 'prod';
61
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Guards for env-derived secrets that are sent as HTTP header values.
3
+ *
4
+ * fetch() validates header values and may echo invalid raw values in its
5
+ * error. Validate secrets before constructing headers so failures identify
6
+ * the environment variable without leaking its contents.
7
+ */
8
+
9
+ const HEADER_UNSAFE_CHAR_PATTERN = /[^\t\u0020-\u007e\u0080-\u00ff]/;
10
+
11
+ export function isHeaderSafeValue(value: string): boolean {
12
+ return !HEADER_UNSAFE_CHAR_PATTERN.test(value);
13
+ }
14
+
15
+ export function assertHeaderSafeEnvValue(input: {
16
+ envVar: string;
17
+ value: string;
18
+ }): string {
19
+ if (!isHeaderSafeValue(input.value)) {
20
+ throw new Error(
21
+ `Environment variable ${input.envVar} contains a character that is invalid in an HTTP header value ` +
22
+ `(typically an embedded newline from a multi-line paste). ` +
23
+ `Re-set ${input.envVar} as a single-line value. ` +
24
+ `The raw value is not echoed to avoid leaking a secret.`,
25
+ );
26
+ }
27
+ return input.value;
28
+ }
@@ -0,0 +1,50 @@
1
+ export const INTERNAL_EMAIL_DOMAINS = ['getaero.io', 'deepline.com'] as const;
2
+ const LOCAL_DEVELOPMENT_PLATFORM_ADMIN_EMAIL = 'local-dev@deepline.local';
3
+
4
+ export function normalizeInternalEmail(email?: string | null): string | null {
5
+ const normalized = String(email ?? '')
6
+ .trim()
7
+ .toLowerCase();
8
+ return normalized || null;
9
+ }
10
+
11
+ export function isInternalEmail(email?: string | null): boolean {
12
+ const normalized = normalizeInternalEmail(email);
13
+ if (!normalized) return false;
14
+ if (
15
+ process.env.NODE_ENV === 'development' &&
16
+ normalized === LOCAL_DEVELOPMENT_PLATFORM_ADMIN_EMAIL
17
+ ) {
18
+ return true;
19
+ }
20
+ const atIndex = normalized.lastIndexOf('@');
21
+ if (atIndex === -1) return false;
22
+ const domain = normalized.slice(atIndex + 1);
23
+ return INTERNAL_EMAIL_DOMAINS.includes(
24
+ domain as (typeof INTERNAL_EMAIL_DOMAINS)[number],
25
+ );
26
+ }
27
+
28
+ /** Actors allowed to request provider fixtures against the production API. */
29
+ export function isAuthorizedProductionFixtureActor(
30
+ email?: string | null,
31
+ ): boolean {
32
+ const normalized = normalizeInternalEmail(email);
33
+ return (
34
+ isInternalEmail(normalized) ||
35
+ /^bootstrap-[0-9a-f]{8}@deepline\.local$/i.test(normalized ?? '')
36
+ );
37
+ }
38
+
39
+ /** Whether an email belongs to a Deepline staff account. */
40
+ export function isDeeplineUser(email?: string | null): boolean {
41
+ return isInternalEmail(email);
42
+ }
43
+
44
+ export function isDeeplineInternalEmail(email?: string | null): boolean {
45
+ const normalized = normalizeInternalEmail(email);
46
+ if (!normalized) return false;
47
+ const atIndex = normalized.lastIndexOf('@');
48
+ if (atIndex === -1) return false;
49
+ return normalized.slice(atIndex + 1) === 'deepline.com';
50
+ }