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.
- package/dist/bundling-sources/sdk/src/client.ts +534 -4
- 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/monitor-fleet-contract.ts +1 -1
- package/dist/bundling-sources/sdk/src/play.ts +15 -1
- package/dist/bundling-sources/sdk/src/plays/bundle-play-file.ts +2 -2
- 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/integration-contracts/AGENTS.md +9 -0
- package/dist/bundling-sources/shared_libs/integration-contracts/README.md +3 -0
- package/dist/bundling-sources/shared_libs/integration-contracts/index.ts +2 -0
- package/dist/bundling-sources/shared_libs/monitors/analytics-install-guides.ts +77 -0
- package/dist/bundling-sources/shared_libs/monitors/billing/charges.ts +125 -0
- package/dist/bundling-sources/shared_libs/monitors/billing/policy-builders.ts +686 -0
- package/dist/bundling-sources/shared_libs/monitors/billing/pricing.ts +54 -0
- package/dist/bundling-sources/shared_libs/monitors/billing/public-pricing.ts +72 -0
- package/dist/bundling-sources/shared_libs/monitors/billing/session.ts +45 -0
- package/dist/bundling-sources/shared_libs/monitors/billing/state.ts +454 -0
- package/dist/bundling-sources/shared_libs/monitors/billing/tamradar.ts +260 -0
- package/dist/bundling-sources/shared_libs/monitors/billing/types.ts +215 -0
- package/dist/bundling-sources/shared_libs/monitors/callback-security.ts +333 -0
- package/dist/bundling-sources/shared_libs/monitors/cursor-page.ts +42 -0
- package/dist/bundling-sources/shared_libs/monitors/deactivation-reasons.ts +16 -0
- package/dist/bundling-sources/shared_libs/monitors/framework-managed-payload-fields.ts +71 -0
- package/dist/bundling-sources/shared_libs/monitors/generated/provider-monitor-callback-admission-targets.ts +16 -0
- package/dist/bundling-sources/shared_libs/monitors/http-error-response.ts +267 -0
- package/dist/bundling-sources/shared_libs/monitors/inbound-publish-key.ts +39 -0
- package/dist/bundling-sources/shared_libs/monitors/inbound-queue.ts +82 -0
- package/dist/bundling-sources/shared_libs/monitors/lifecycle-notifications.ts +473 -0
- package/dist/bundling-sources/shared_libs/monitors/org-monitor-limits.ts +1 -1
- package/dist/bundling-sources/shared_libs/monitors/package.json +29 -0
- package/dist/bundling-sources/shared_libs/monitors/provider-callback-verification.ts +1125 -0
- package/dist/bundling-sources/shared_libs/monitors/provider-monitor-batch-operation.ts +263 -0
- package/dist/bundling-sources/shared_libs/monitors/provider-monitor-capability.ts +964 -0
- package/dist/bundling-sources/shared_libs/monitors/provider-monitor-consumer-health.ts +56 -0
- package/dist/bundling-sources/shared_libs/monitors/provider-monitor-errors.ts +131 -0
- package/dist/bundling-sources/shared_libs/monitors/provider-monitor-identity-claims.ts +535 -0
- package/dist/bundling-sources/shared_libs/monitors/provider-monitor-legacy-aliases.ts +16 -0
- package/dist/bundling-sources/shared_libs/monitors/provider-monitor-priority-execution.ts +56 -0
- package/dist/bundling-sources/shared_libs/monitors/provider-monitor-public-aliases.ts +28 -0
- package/dist/bundling-sources/shared_libs/monitors/provider-monitor-runtime-read-eligibility.ts +38 -0
- package/dist/bundling-sources/shared_libs/monitors/provider-monitor-state.ts +122 -0
- package/dist/bundling-sources/shared_libs/monitors/provider-monitor-status.ts +10 -0
- package/dist/bundling-sources/shared_libs/monitors/provider-monitor-upstream-identity.ts +112 -0
- package/dist/bundling-sources/shared_libs/monitors/provider-webhook-monitor-runtime.ts +300 -0
- package/dist/bundling-sources/shared_libs/monitors/quickstart-prompts.ts +22 -0
- package/dist/bundling-sources/shared_libs/monitors/snitcher-install-validation.ts +93 -0
- package/dist/bundling-sources/shared_libs/monitors/snitcher-ip2company.ts +332 -0
- package/dist/bundling-sources/shared_libs/monitors/snitcher-radar.ts +221 -0
- package/dist/bundling-sources/shared_libs/monitors/starter-templates.ts +105 -0
- package/dist/bundling-sources/shared_libs/monitors/tracking-provider-guidance.ts +73 -0
- package/dist/bundling-sources/shared_libs/monitors/tracking-provider-integrations.ts +81 -0
- package/dist/bundling-sources/shared_libs/monitors/tsconfig.json +10 -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-env.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/play-runtime/tool-http-errors.ts +1 -1
- package/dist/bundling-sources/shared_libs/play-runtime/tool-result.ts +1 -1
- package/dist/bundling-sources/shared_libs/plays/artifact-types.ts +6 -3
- package/dist/bundling-sources/shared_libs/plays/authoring-contract.ts +454 -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/email-status.ts +1 -1
- 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/plays/tool-category-descriptions.ts +2 -2
- 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/runtime-env.ts +6 -0
- package/dist/bundling-sources/shared_libs/security/header-safe-env.ts +28 -0
- package/dist/bundling-sources/shared_libs/security/internal-actor.ts +50 -0
- package/dist/bundling-sources/shared_libs/security/secret-redaction.ts +267 -0
- package/dist/cli/index.js +1070 -193
- package/dist/cli/index.mjs +1057 -180
- 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 +834 -12
- package/dist/index.d.ts +834 -12
- package/dist/index.js +469 -40
- package/dist/index.mjs +469 -40
- package/dist/install-integrity.json +65 -4
- 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 +3 -1
- /package/dist/bundling-sources/shared_libs/{integrations/bettercontact-execution-policy.ts → integration-contracts/bettercontact/execution-policy.ts} +0 -0
- /package/dist/bundling-sources/shared_libs/{integrations/theirstack-execution-policy.ts → integration-contracts/theirstack/execution-policy.ts} +0 -0
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import {
|
|
2
|
+
normalizeProductNotificationEmail,
|
|
3
|
+
normalizeProductNotificationEmailList,
|
|
4
|
+
type ProductNotificationDestinationSettings,
|
|
5
|
+
} from './configuration';
|
|
6
|
+
import {
|
|
7
|
+
workspaceAppUrl,
|
|
8
|
+
notificationSettingsUrl,
|
|
9
|
+
type NotificationWorkspace,
|
|
10
|
+
} from './workspace';
|
|
11
|
+
import { notificationText, type NotificationText } from './content';
|
|
12
|
+
|
|
13
|
+
export {
|
|
14
|
+
normalizeProductNotificationEmail,
|
|
15
|
+
normalizeProductNotificationEmailList,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type ProductNotificationEmailRequest = {
|
|
19
|
+
to: string[];
|
|
20
|
+
cc?: string[];
|
|
21
|
+
bcc?: string[];
|
|
22
|
+
headers?: Record<string, string>;
|
|
23
|
+
subject: string;
|
|
24
|
+
html: string;
|
|
25
|
+
text: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
type ProductNotificationEmailContent = {
|
|
29
|
+
groupName: string;
|
|
30
|
+
title: string;
|
|
31
|
+
summary: NotificationText;
|
|
32
|
+
details: NotificationText[];
|
|
33
|
+
targetUrl?: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const MAX_RECIPIENTS = 50;
|
|
37
|
+
|
|
38
|
+
export function productNotificationEmailAddressVersion(email: string): string {
|
|
39
|
+
const normalized = normalizeProductNotificationEmail(email);
|
|
40
|
+
if (!normalized) throw new Error('Notification email address is invalid.');
|
|
41
|
+
let hash = 0x811c9dc5;
|
|
42
|
+
for (const character of normalized) {
|
|
43
|
+
hash ^= character.charCodeAt(0);
|
|
44
|
+
hash = Math.imul(hash, 0x01000193);
|
|
45
|
+
}
|
|
46
|
+
return `email-v1-${(hash >>> 0).toString(36)}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function escapeHtml(value: string): string {
|
|
50
|
+
return value.replace(/[&<>"']/g, (character) => {
|
|
51
|
+
if (character === '&') return '&';
|
|
52
|
+
if (character === '<') return '<';
|
|
53
|
+
if (character === '>') return '>';
|
|
54
|
+
if (character === '"') return '"';
|
|
55
|
+
return ''';
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function unsubscribeUrl(value: string): string {
|
|
60
|
+
const url = new URL(value);
|
|
61
|
+
if (url.protocol !== 'https:' && url.protocol !== 'http:') {
|
|
62
|
+
throw new Error('Notification unsubscribe URL must use HTTP(S).');
|
|
63
|
+
}
|
|
64
|
+
return url.toString();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function contentHtml(
|
|
68
|
+
content: NotificationText,
|
|
69
|
+
workspaceName?: string,
|
|
70
|
+
): string {
|
|
71
|
+
if (typeof content === 'string') return escapeHtml(content);
|
|
72
|
+
return content
|
|
73
|
+
.map((part) => {
|
|
74
|
+
if (typeof part === 'string') return escapeHtml(part);
|
|
75
|
+
if ('strong' in part)
|
|
76
|
+
return `<strong>${escapeHtml(part.strong)}</strong>`;
|
|
77
|
+
return workspaceName
|
|
78
|
+
? `workspace <strong>${escapeHtml(workspaceName)}</strong>`
|
|
79
|
+
: 'workspace';
|
|
80
|
+
})
|
|
81
|
+
.join('');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function renderProductNotificationEmail(
|
|
85
|
+
input: NotificationWorkspace & {
|
|
86
|
+
groupKey: string;
|
|
87
|
+
content: ProductNotificationEmailContent;
|
|
88
|
+
settings: Extract<
|
|
89
|
+
ProductNotificationDestinationSettings,
|
|
90
|
+
{ kind: 'email_addresses' }
|
|
91
|
+
>;
|
|
92
|
+
unsubscribeUrl?: string;
|
|
93
|
+
oneClickUnsubscribeUrl?: string;
|
|
94
|
+
},
|
|
95
|
+
): ProductNotificationEmailRequest {
|
|
96
|
+
const to = normalizeProductNotificationEmailList(
|
|
97
|
+
input.settings.to,
|
|
98
|
+
'Email recipients',
|
|
99
|
+
);
|
|
100
|
+
const cc = normalizeProductNotificationEmailList(
|
|
101
|
+
input.settings.cc,
|
|
102
|
+
'Email CC',
|
|
103
|
+
);
|
|
104
|
+
const bcc = normalizeProductNotificationEmailList(
|
|
105
|
+
input.settings.bcc,
|
|
106
|
+
'Email BCC',
|
|
107
|
+
);
|
|
108
|
+
if (to.length === 0) throw new Error('Email delivery requires a recipient.');
|
|
109
|
+
if (to.length + cc.length + bcc.length > MAX_RECIPIENTS) {
|
|
110
|
+
throw new Error(
|
|
111
|
+
`Email delivery exceeds the ${MAX_RECIPIENTS}-recipient limit.`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const targetUrl = input.content.targetUrl
|
|
116
|
+
? workspaceAppUrl(input.appUrl, input.workspaceId, input.content.targetUrl)
|
|
117
|
+
: null;
|
|
118
|
+
const settingsUrl = notificationSettingsUrl(input, input.groupKey);
|
|
119
|
+
const logoUrl =
|
|
120
|
+
targetUrl ?? workspaceAppUrl(input.appUrl, input.workspaceId, '/dashboard');
|
|
121
|
+
const optOutUrl = input.unsubscribeUrl
|
|
122
|
+
? unsubscribeUrl(input.unsubscribeUrl)
|
|
123
|
+
: null;
|
|
124
|
+
const oneClickOptOutUrl = input.oneClickUnsubscribeUrl
|
|
125
|
+
? unsubscribeUrl(input.oneClickUnsubscribeUrl)
|
|
126
|
+
: optOutUrl;
|
|
127
|
+
const text = [
|
|
128
|
+
input.content.title,
|
|
129
|
+
`Notification group: ${input.content.groupName}`,
|
|
130
|
+
...(targetUrl ? [`Open Deepline: ${targetUrl}`] : []),
|
|
131
|
+
'',
|
|
132
|
+
notificationText(input.content.summary, input.workspaceName),
|
|
133
|
+
...input.content.details.map((detail) =>
|
|
134
|
+
notificationText(detail, input.workspaceName),
|
|
135
|
+
),
|
|
136
|
+
'',
|
|
137
|
+
`Manage notifications: ${settingsUrl}`,
|
|
138
|
+
...(optOutUrl
|
|
139
|
+
? [`Unsubscribe from ${input.content.groupName} email: ${optOutUrl}`]
|
|
140
|
+
: []),
|
|
141
|
+
].join('\n');
|
|
142
|
+
const html = [
|
|
143
|
+
'<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="background-color:#fff;color:#111827"><tr><td align="center" style="padding:32px 16px">',
|
|
144
|
+
'<table role="presentation" width="600" cellspacing="0" cellpadding="0" style="width:100%;max-width:600px"><tr><td style="font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:1.5;text-align:left">',
|
|
145
|
+
`<a href="${escapeHtml(logoUrl)}"><img src="https://code.deepline.com/brand/logo_full_dark.png" width="140" height="26" alt="Deepline" style="display:block;border:0;margin:0 0 32px"></a>`,
|
|
146
|
+
`<h1 style="font-size:24px;line-height:1.25;font-weight:600;letter-spacing:-0.02em;margin:0 0 4px">${escapeHtml(input.content.title)}</h1>`,
|
|
147
|
+
'<div style="padding-left:20px">',
|
|
148
|
+
`<p style="margin:0 0 4px"><strong>Notification group:</strong> ${escapeHtml(input.content.groupName)}</p>`,
|
|
149
|
+
...(targetUrl
|
|
150
|
+
? [
|
|
151
|
+
`<p style="margin:0"><a href="${escapeHtml(targetUrl)}" style="color:#1919ff">Open Deepline</a></p>`,
|
|
152
|
+
]
|
|
153
|
+
: []),
|
|
154
|
+
'</div>',
|
|
155
|
+
'<div style="margin:24px 0;padding:20px;border:1px solid #1919ff;border-radius:8px;color:#111827;font-size:18px;line-height:1.5">',
|
|
156
|
+
`<p style="margin:0">${contentHtml(input.content.summary, input.workspaceName)}</p>`,
|
|
157
|
+
...input.content.details.map(
|
|
158
|
+
(detail) =>
|
|
159
|
+
`<p style="margin:4px 0 0">${contentHtml(detail, input.workspaceName)}</p>`,
|
|
160
|
+
),
|
|
161
|
+
'</div>',
|
|
162
|
+
'<hr style="border:0;border-top:1px solid #e5e7eb;margin:32px 0 24px">',
|
|
163
|
+
`<p style="color:#666;font-size:12px"><a href="${escapeHtml(settingsUrl)}" style="color:#666">Manage notifications</a></p>`,
|
|
164
|
+
...(optOutUrl
|
|
165
|
+
? [
|
|
166
|
+
`<p style="color:#666;font-size:12px"><a href="${escapeHtml(optOutUrl)}">Unsubscribe from ${escapeHtml(input.content.groupName)} email</a></p>`,
|
|
167
|
+
]
|
|
168
|
+
: []),
|
|
169
|
+
'</td></tr></table></td></tr></table>',
|
|
170
|
+
].join('');
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
to,
|
|
174
|
+
...(cc.length > 0 ? { cc } : {}),
|
|
175
|
+
...(bcc.length > 0 ? { bcc } : {}),
|
|
176
|
+
...(oneClickOptOutUrl
|
|
177
|
+
? {
|
|
178
|
+
headers: {
|
|
179
|
+
'List-Unsubscribe': `<${oneClickOptOutUrl}>`,
|
|
180
|
+
'List-Unsubscribe-Post': 'List-Unsubscribe=One-Click',
|
|
181
|
+
},
|
|
182
|
+
}
|
|
183
|
+
: {}),
|
|
184
|
+
subject: input.workspaceName
|
|
185
|
+
? `${input.content.title} — ${input.workspaceName}`
|
|
186
|
+
: input.content.title,
|
|
187
|
+
html,
|
|
188
|
+
text,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function parseProductNotificationEmailRequest(
|
|
193
|
+
value: string,
|
|
194
|
+
): ProductNotificationEmailRequest {
|
|
195
|
+
let parsed: unknown;
|
|
196
|
+
try {
|
|
197
|
+
parsed = JSON.parse(value);
|
|
198
|
+
} catch {
|
|
199
|
+
throw new Error('Email delivery request is not valid JSON.');
|
|
200
|
+
}
|
|
201
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
202
|
+
throw new Error('Email delivery request is invalid.');
|
|
203
|
+
}
|
|
204
|
+
const request = parsed as Partial<ProductNotificationEmailRequest>;
|
|
205
|
+
if (
|
|
206
|
+
!Array.isArray(request.to) ||
|
|
207
|
+
typeof request.subject !== 'string' ||
|
|
208
|
+
!request.subject.trim() ||
|
|
209
|
+
typeof request.html !== 'string' ||
|
|
210
|
+
!request.html ||
|
|
211
|
+
typeof request.text !== 'string' ||
|
|
212
|
+
!request.text
|
|
213
|
+
) {
|
|
214
|
+
throw new Error('Email delivery request is incomplete.');
|
|
215
|
+
}
|
|
216
|
+
const to = normalizeProductNotificationEmailList(
|
|
217
|
+
request.to,
|
|
218
|
+
'Email recipients',
|
|
219
|
+
);
|
|
220
|
+
const cc = normalizeProductNotificationEmailList(request.cc, 'Email CC');
|
|
221
|
+
const bcc = normalizeProductNotificationEmailList(request.bcc, 'Email BCC');
|
|
222
|
+
const headers = request.headers;
|
|
223
|
+
if (
|
|
224
|
+
headers !== undefined &&
|
|
225
|
+
(!headers ||
|
|
226
|
+
typeof headers !== 'object' ||
|
|
227
|
+
Array.isArray(headers) ||
|
|
228
|
+
Object.entries(headers).some(
|
|
229
|
+
([name, value]) => !name.trim() || typeof value !== 'string',
|
|
230
|
+
))
|
|
231
|
+
) {
|
|
232
|
+
throw new Error('Email delivery headers are invalid.');
|
|
233
|
+
}
|
|
234
|
+
if (to.length === 0 || to.length + cc.length + bcc.length > MAX_RECIPIENTS) {
|
|
235
|
+
throw new Error('Email delivery recipient count is invalid.');
|
|
236
|
+
}
|
|
237
|
+
return {
|
|
238
|
+
to,
|
|
239
|
+
...(cc.length > 0 ? { cc } : {}),
|
|
240
|
+
...(bcc.length > 0 ? { bcc } : {}),
|
|
241
|
+
...(headers ? { headers } : {}),
|
|
242
|
+
subject: request.subject,
|
|
243
|
+
html: request.html,
|
|
244
|
+
text: request.text,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ProductNotificationFacts,
|
|
3
|
+
ProductNotificationTrigger,
|
|
4
|
+
} from './configuration';
|
|
5
|
+
|
|
6
|
+
export const PRODUCT_NOTIFICATION_DECISION_HORIZON_MS =
|
|
7
|
+
30 * 24 * 60 * 60 * 1_000;
|
|
8
|
+
|
|
9
|
+
export type ProductNotificationOutcomeFact = {
|
|
10
|
+
orderingKey: string;
|
|
11
|
+
outcome: string;
|
|
12
|
+
occurredAt: number;
|
|
13
|
+
sourceEventId: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type ProductNotificationObservation =
|
|
17
|
+
| { kind: 'match' }
|
|
18
|
+
| {
|
|
19
|
+
kind: 'state_transition';
|
|
20
|
+
previousState?: string;
|
|
21
|
+
currentState: string;
|
|
22
|
+
previousReasonCode?: string;
|
|
23
|
+
currentReasonCode?: string;
|
|
24
|
+
/** Compatibility alias for currentReasonCode. */
|
|
25
|
+
reasonCode?: string;
|
|
26
|
+
}
|
|
27
|
+
| {
|
|
28
|
+
kind: 'threshold_crossing';
|
|
29
|
+
previousValue: number;
|
|
30
|
+
currentValue: number;
|
|
31
|
+
unit?: string;
|
|
32
|
+
}
|
|
33
|
+
| { kind: 'threshold_state'; currentValue: number; unit?: string }
|
|
34
|
+
| {
|
|
35
|
+
kind: 'outcome_history';
|
|
36
|
+
/** Canonical newest-first history, beginning with the current event. */
|
|
37
|
+
outcomes: ProductNotificationOutcomeFact[];
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type ProductNotificationTriggerDecision = {
|
|
41
|
+
fires: boolean;
|
|
42
|
+
reason:
|
|
43
|
+
| 'each_match'
|
|
44
|
+
| 'entered_state'
|
|
45
|
+
| 'crossed_threshold'
|
|
46
|
+
| 'condition_active'
|
|
47
|
+
| 'completed_streak'
|
|
48
|
+
| 'observation_mismatch'
|
|
49
|
+
| 'reason_mismatch'
|
|
50
|
+
| 'unit_mismatch'
|
|
51
|
+
| 'not_a_transition'
|
|
52
|
+
| 'not_a_crossing'
|
|
53
|
+
| 'insufficient_history'
|
|
54
|
+
| 'streak_already_active';
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export function productNotificationFactsMatch(
|
|
58
|
+
filter: ProductNotificationFacts,
|
|
59
|
+
facts: ProductNotificationFacts,
|
|
60
|
+
): boolean {
|
|
61
|
+
return Object.entries(filter).every(([key, value]) => facts[key] === value);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function compareCanonicalOrder(
|
|
65
|
+
left: ProductNotificationOutcomeFact,
|
|
66
|
+
right: ProductNotificationOutcomeFact,
|
|
67
|
+
) {
|
|
68
|
+
return (
|
|
69
|
+
left.occurredAt - right.occurredAt ||
|
|
70
|
+
left.sourceEventId.localeCompare(right.sourceEventId)
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function validateOutcomeHistory(
|
|
75
|
+
outcomes: ProductNotificationOutcomeFact[],
|
|
76
|
+
maximum: number,
|
|
77
|
+
) {
|
|
78
|
+
if (outcomes.length > maximum) {
|
|
79
|
+
throw new Error(`Outcome history exceeds the ${maximum}-row bound.`);
|
|
80
|
+
}
|
|
81
|
+
const orderingKey = outcomes[0]?.orderingKey;
|
|
82
|
+
for (let index = 0; index < outcomes.length; index += 1) {
|
|
83
|
+
const outcome = outcomes[index]!;
|
|
84
|
+
if (
|
|
85
|
+
!outcome.orderingKey ||
|
|
86
|
+
outcome.orderingKey !== orderingKey ||
|
|
87
|
+
!outcome.outcome ||
|
|
88
|
+
!outcome.sourceEventId ||
|
|
89
|
+
!Number.isFinite(outcome.occurredAt)
|
|
90
|
+
) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
'Outcome history contains an invalid or cross-stream fact.',
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
const next = outcomes[index + 1];
|
|
96
|
+
if (next && compareCanonicalOrder(outcome, next) <= 0) {
|
|
97
|
+
throw new Error(
|
|
98
|
+
'Outcome history must be in canonical newest-first order.',
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function decision(
|
|
105
|
+
fires: boolean,
|
|
106
|
+
reason: ProductNotificationTriggerDecision['reason'],
|
|
107
|
+
): ProductNotificationTriggerDecision {
|
|
108
|
+
return { fires, reason };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export type ProductNotificationCondition = {
|
|
112
|
+
previousActive?: boolean;
|
|
113
|
+
currentActive: boolean;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
function thresholdIsActive(
|
|
117
|
+
trigger: Extract<ProductNotificationTrigger, { kind: 'threshold_crossing' }>,
|
|
118
|
+
value: number,
|
|
119
|
+
) {
|
|
120
|
+
return trigger.direction === 'below'
|
|
121
|
+
? value < trigger.threshold
|
|
122
|
+
: trigger.direction === 'at_or_above'
|
|
123
|
+
? value >= trigger.threshold
|
|
124
|
+
: value > trigger.threshold;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Map an observation to the one shared condition shape when it has durable state. */
|
|
128
|
+
export function productNotificationCondition(input: {
|
|
129
|
+
trigger: ProductNotificationTrigger;
|
|
130
|
+
observation: ProductNotificationObservation;
|
|
131
|
+
}): ProductNotificationCondition | null {
|
|
132
|
+
const { trigger, observation } = input;
|
|
133
|
+
if (trigger.kind === 'state_transition') {
|
|
134
|
+
if (observation.kind !== 'state_transition') return null;
|
|
135
|
+
const matches = (state: string, reasonCode: string | undefined) =>
|
|
136
|
+
state === trigger.targetState &&
|
|
137
|
+
(trigger.reasonCode === undefined || reasonCode === trigger.reasonCode);
|
|
138
|
+
const currentReasonCode =
|
|
139
|
+
observation.currentReasonCode ?? observation.reasonCode;
|
|
140
|
+
const previousReasonCode =
|
|
141
|
+
observation.previousReasonCode ??
|
|
142
|
+
(observation.previousState === observation.currentState
|
|
143
|
+
? currentReasonCode
|
|
144
|
+
: undefined);
|
|
145
|
+
return {
|
|
146
|
+
previousActive:
|
|
147
|
+
observation.previousState === undefined
|
|
148
|
+
? undefined
|
|
149
|
+
: matches(observation.previousState, previousReasonCode),
|
|
150
|
+
currentActive: matches(observation.currentState, currentReasonCode),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
if (trigger.kind === 'threshold_crossing') {
|
|
154
|
+
if (
|
|
155
|
+
observation.kind !== 'threshold_crossing' &&
|
|
156
|
+
observation.kind !== 'threshold_state'
|
|
157
|
+
) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
if (trigger.unit !== undefined && observation.unit !== trigger.unit) {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
if (observation.kind === 'threshold_state') {
|
|
164
|
+
return {
|
|
165
|
+
currentActive: thresholdIsActive(trigger, observation.currentValue),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
previousActive: thresholdIsActive(trigger, observation.previousValue),
|
|
170
|
+
currentActive: thresholdIsActive(trigger, observation.currentValue),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
if (trigger.kind !== 'consecutive_outcomes') return null;
|
|
174
|
+
if (observation.kind !== 'outcome_history') return null;
|
|
175
|
+
validateOutcomeHistory(observation.outcomes, trigger.maxLookback + 1);
|
|
176
|
+
const currentOccurredAt = observation.outcomes[0]?.occurredAt;
|
|
177
|
+
const outcomes =
|
|
178
|
+
currentOccurredAt === undefined
|
|
179
|
+
? []
|
|
180
|
+
: observation.outcomes.filter(
|
|
181
|
+
(outcome) =>
|
|
182
|
+
outcome.occurredAt >=
|
|
183
|
+
currentOccurredAt - PRODUCT_NOTIFICATION_DECISION_HORIZON_MS,
|
|
184
|
+
);
|
|
185
|
+
const active = (offset: number) => {
|
|
186
|
+
const streak = outcomes.slice(offset, offset + trigger.count);
|
|
187
|
+
return (
|
|
188
|
+
streak.length === trigger.count &&
|
|
189
|
+
streak.every((outcome) => outcome.outcome === trigger.outcome)
|
|
190
|
+
);
|
|
191
|
+
};
|
|
192
|
+
return {
|
|
193
|
+
previousActive: active(1),
|
|
194
|
+
currentActive: active(0),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Deterministic provider-independent evaluation for the four V1 policies. */
|
|
199
|
+
export function evaluateProductNotificationTrigger(input: {
|
|
200
|
+
trigger: ProductNotificationTrigger;
|
|
201
|
+
observation: ProductNotificationObservation;
|
|
202
|
+
occurredAt: number;
|
|
203
|
+
}): ProductNotificationTriggerDecision {
|
|
204
|
+
const { trigger, observation, occurredAt } = input;
|
|
205
|
+
if (!Number.isFinite(occurredAt)) {
|
|
206
|
+
throw new Error('occurredAt must be finite.');
|
|
207
|
+
}
|
|
208
|
+
if (trigger.kind === 'each_match') return decision(true, 'each_match');
|
|
209
|
+
|
|
210
|
+
if (trigger.kind === 'state_transition') {
|
|
211
|
+
if (observation.kind !== 'state_transition') {
|
|
212
|
+
return decision(false, 'observation_mismatch');
|
|
213
|
+
}
|
|
214
|
+
const condition = productNotificationCondition({ trigger, observation });
|
|
215
|
+
const currentReasonCode =
|
|
216
|
+
observation.currentReasonCode ?? observation.reasonCode;
|
|
217
|
+
if (
|
|
218
|
+
trigger.reasonCode !== undefined &&
|
|
219
|
+
currentReasonCode !== trigger.reasonCode
|
|
220
|
+
) {
|
|
221
|
+
return decision(false, 'reason_mismatch');
|
|
222
|
+
}
|
|
223
|
+
const fires =
|
|
224
|
+
condition?.previousActive === false && condition.currentActive === true;
|
|
225
|
+
return decision(fires, fires ? 'entered_state' : 'not_a_transition');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (trigger.kind === 'threshold_crossing') {
|
|
229
|
+
if (
|
|
230
|
+
observation.kind !== 'threshold_crossing' &&
|
|
231
|
+
observation.kind !== 'threshold_state'
|
|
232
|
+
) {
|
|
233
|
+
return decision(false, 'observation_mismatch');
|
|
234
|
+
}
|
|
235
|
+
const condition = productNotificationCondition({ trigger, observation });
|
|
236
|
+
if (!condition) {
|
|
237
|
+
return decision(false, 'unit_mismatch');
|
|
238
|
+
}
|
|
239
|
+
if (observation.kind === 'threshold_state') {
|
|
240
|
+
return decision(
|
|
241
|
+
condition.currentActive,
|
|
242
|
+
condition.currentActive ? 'condition_active' : 'not_a_crossing',
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
const fires =
|
|
246
|
+
condition.previousActive === false && condition.currentActive === true;
|
|
247
|
+
return decision(fires, fires ? 'crossed_threshold' : 'not_a_crossing');
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (observation.kind !== 'outcome_history') {
|
|
251
|
+
return decision(false, 'observation_mismatch');
|
|
252
|
+
}
|
|
253
|
+
if (
|
|
254
|
+
!Number.isInteger(trigger.count) ||
|
|
255
|
+
trigger.count < 1 ||
|
|
256
|
+
!Number.isInteger(trigger.maxLookback) ||
|
|
257
|
+
trigger.maxLookback < trigger.count ||
|
|
258
|
+
trigger.maxLookback > 100
|
|
259
|
+
) {
|
|
260
|
+
throw new Error('Consecutive-outcome bounds are invalid.');
|
|
261
|
+
}
|
|
262
|
+
validateOutcomeHistory(observation.outcomes, trigger.maxLookback + 1);
|
|
263
|
+
const withinHorizon = observation.outcomes.filter(
|
|
264
|
+
(outcome) =>
|
|
265
|
+
outcome.occurredAt <= occurredAt &&
|
|
266
|
+
outcome.occurredAt >=
|
|
267
|
+
occurredAt - PRODUCT_NOTIFICATION_DECISION_HORIZON_MS,
|
|
268
|
+
);
|
|
269
|
+
const streak = withinHorizon.slice(0, trigger.count);
|
|
270
|
+
if (
|
|
271
|
+
streak.length < trigger.count ||
|
|
272
|
+
streak.some((outcome) => outcome.outcome !== trigger.outcome)
|
|
273
|
+
) {
|
|
274
|
+
return decision(false, 'insufficient_history');
|
|
275
|
+
}
|
|
276
|
+
if (withinHorizon[trigger.count]?.outcome === trigger.outcome) {
|
|
277
|
+
return decision(false, 'streak_already_active');
|
|
278
|
+
}
|
|
279
|
+
return decision(true, 'completed_streak');
|
|
280
|
+
}
|