deepline 0.3.87 → 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.
- package/dist/bundling-sources/sdk/src/client.ts +532 -2
- package/dist/bundling-sources/sdk/src/config.ts +77 -0
- package/dist/bundling-sources/sdk/src/http.ts +76 -30
- package/dist/bundling-sources/sdk/src/index.ts +6 -0
- package/dist/bundling-sources/sdk/src/play.ts +15 -1
- package/dist/bundling-sources/sdk/src/plays/bundle-play-file.ts +1 -1
- package/dist/bundling-sources/sdk/src/release.ts +1 -1
- package/dist/bundling-sources/sdk/src/types.ts +83 -4
- package/dist/bundling-sources/shared_libs/monitors/deactivation-reasons.ts +16 -0
- package/dist/bundling-sources/shared_libs/monitors/provider-monitor-batch-operation.ts +263 -0
- package/dist/bundling-sources/shared_libs/monitors/provider-monitor-identity-claims.ts +9 -1
- package/dist/bundling-sources/shared_libs/monitors/provider-monitor-state.ts +122 -0
- package/dist/bundling-sources/shared_libs/play-runtime/activity-observation.ts +6 -11
- package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +90 -12
- package/dist/bundling-sources/shared_libs/play-runtime/async-play-invocation.ts +78 -0
- package/dist/bundling-sources/shared_libs/play-runtime/checkpoint-boundaries.ts +70 -0
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +407 -53
- package/dist/bundling-sources/shared_libs/play-runtime/coordinator-headers.ts +1 -32
- package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +57 -0
- package/dist/bundling-sources/shared_libs/play-runtime/default-batch-strategies.ts +20 -1
- package/dist/bundling-sources/shared_libs/play-runtime/execution-capabilities.ts +6 -0
- package/dist/bundling-sources/shared_libs/play-runtime/execution-protocol.ts +7 -1
- package/dist/bundling-sources/shared_libs/play-runtime/lane-retirement-gate.ts +189 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-runtime-semantics.ts +28 -0
- package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +2 -0
- package/dist/bundling-sources/shared_libs/play-runtime/run-failure.ts +23 -0
- package/dist/bundling-sources/shared_libs/play-runtime/run-snapshot-stream.ts +1 -1
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-lifecycle.ts +18 -4
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-start-admission.ts +68 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona.ts +2 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/runtime-sandbox-reconciliation.ts +1 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +10 -8
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-capacity-policy.ts +20 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-contract.ts +6 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-scheduler-topology.ts +24 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backend.ts +30 -1
- package/dist/bundling-sources/shared_libs/plays/artifact-types.ts +6 -3
- package/dist/bundling-sources/shared_libs/plays/authoring-contract.ts +152 -17
- package/dist/bundling-sources/shared_libs/plays/bundling/index.ts +19 -4
- package/dist/bundling-sources/shared_libs/plays/contracts.ts +6 -14
- package/dist/bundling-sources/shared_libs/plays/core.ts +11 -6
- package/dist/bundling-sources/shared_libs/plays/docflow-binding-owner.ts +16 -11
- package/dist/bundling-sources/shared_libs/plays/resolve-static-pipeline.ts +1 -1
- package/dist/bundling-sources/shared_libs/plays/slug.ts +115 -0
- package/dist/bundling-sources/shared_libs/plays/static-pipeline.ts +44 -14
- package/dist/bundling-sources/shared_libs/product-notifications/catalog.ts +877 -0
- package/dist/bundling-sources/shared_libs/product-notifications/configuration-draft.ts +456 -0
- package/dist/bundling-sources/shared_libs/product-notifications/configuration.ts +560 -0
- package/dist/bundling-sources/shared_libs/product-notifications/content.ts +28 -0
- package/dist/bundling-sources/shared_libs/product-notifications/default-policy.ts +59 -0
- package/dist/bundling-sources/shared_libs/product-notifications/email.ts +246 -0
- package/dist/bundling-sources/shared_libs/product-notifications/evaluation.ts +280 -0
- package/dist/bundling-sources/shared_libs/product-notifications/events.ts +433 -0
- package/dist/bundling-sources/shared_libs/product-notifications/legacy-slack.ts +80 -0
- package/dist/bundling-sources/shared_libs/product-notifications/workspace.ts +38 -0
- package/dist/bundling-sources/shared_libs/security/internal-actor.ts +50 -0
- package/dist/cli/index.js +1065 -190
- package/dist/cli/index.mjs +1052 -177
- package/dist/{compiler-manifest-Dbov16F7.d.mts → compiler-manifest-DsLbQaew.d.mts} +132 -8
- package/dist/{compiler-manifest-Dbov16F7.d.ts → compiler-manifest-DsLbQaew.d.ts} +132 -8
- package/dist/index.d.mts +833 -11
- package/dist/index.d.ts +833 -11
- package/dist/index.js +467 -38
- package/dist/index.mjs +467 -38
- package/dist/install-integrity.json +20 -2
- package/dist/plays/bundle-play-file.d.mts +8 -3
- package/dist/plays/bundle-play-file.d.ts +8 -3
- package/dist/plays/bundle-play-file.mjs +100 -20
- package/package.json +1 -1
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|