deepline 0.3.87 → 0.3.88

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/dist/bundling-sources/sdk/src/client.ts +2 -2
  2. package/dist/bundling-sources/sdk/src/monitor-fleet-contract.ts +1 -1
  3. package/dist/bundling-sources/sdk/src/plays/bundle-play-file.ts +1 -1
  4. package/dist/bundling-sources/sdk/src/release.ts +1 -1
  5. package/dist/bundling-sources/shared_libs/monitors/org-monitor-limits.ts +1 -1
  6. package/dist/bundling-sources/shared_libs/play-runtime/runtime-env.ts +0 -6
  7. package/dist/bundling-sources/shared_libs/play-runtime/tool-http-errors.ts +1 -1
  8. package/dist/bundling-sources/shared_libs/play-runtime/tool-result.ts +1 -1
  9. package/dist/bundling-sources/shared_libs/plays/authoring-contract.ts +0 -302
  10. package/dist/bundling-sources/shared_libs/plays/email-status.ts +1 -1
  11. package/dist/bundling-sources/shared_libs/plays/tool-category-descriptions.ts +2 -2
  12. package/dist/bundling-sources/shared_libs/runtime-env.ts +0 -6
  13. package/dist/cli/index.js +4 -6
  14. package/dist/cli/index.mjs +4 -6
  15. package/dist/index.d.mts +1 -1
  16. package/dist/index.d.ts +1 -1
  17. package/dist/index.js +3 -3
  18. package/dist/index.mjs +3 -3
  19. package/dist/install-integrity.json +2 -45
  20. package/package.json +1 -3
  21. package/dist/bundling-sources/shared_libs/integration-contracts/AGENTS.md +0 -9
  22. package/dist/bundling-sources/shared_libs/integration-contracts/README.md +0 -3
  23. package/dist/bundling-sources/shared_libs/integration-contracts/index.ts +0 -2
  24. package/dist/bundling-sources/shared_libs/monitors/analytics-install-guides.ts +0 -77
  25. package/dist/bundling-sources/shared_libs/monitors/billing/charges.ts +0 -125
  26. package/dist/bundling-sources/shared_libs/monitors/billing/policy-builders.ts +0 -686
  27. package/dist/bundling-sources/shared_libs/monitors/billing/pricing.ts +0 -54
  28. package/dist/bundling-sources/shared_libs/monitors/billing/public-pricing.ts +0 -72
  29. package/dist/bundling-sources/shared_libs/monitors/billing/session.ts +0 -45
  30. package/dist/bundling-sources/shared_libs/monitors/billing/state.ts +0 -454
  31. package/dist/bundling-sources/shared_libs/monitors/billing/tamradar.ts +0 -260
  32. package/dist/bundling-sources/shared_libs/monitors/billing/types.ts +0 -215
  33. package/dist/bundling-sources/shared_libs/monitors/callback-security.ts +0 -333
  34. package/dist/bundling-sources/shared_libs/monitors/cursor-page.ts +0 -42
  35. package/dist/bundling-sources/shared_libs/monitors/framework-managed-payload-fields.ts +0 -71
  36. package/dist/bundling-sources/shared_libs/monitors/generated/provider-monitor-callback-admission-targets.ts +0 -16
  37. package/dist/bundling-sources/shared_libs/monitors/http-error-response.ts +0 -267
  38. package/dist/bundling-sources/shared_libs/monitors/inbound-publish-key.ts +0 -39
  39. package/dist/bundling-sources/shared_libs/monitors/inbound-queue.ts +0 -82
  40. package/dist/bundling-sources/shared_libs/monitors/lifecycle-notifications.ts +0 -473
  41. package/dist/bundling-sources/shared_libs/monitors/package.json +0 -29
  42. package/dist/bundling-sources/shared_libs/monitors/provider-callback-verification.ts +0 -1125
  43. package/dist/bundling-sources/shared_libs/monitors/provider-monitor-capability.ts +0 -964
  44. package/dist/bundling-sources/shared_libs/monitors/provider-monitor-consumer-health.ts +0 -56
  45. package/dist/bundling-sources/shared_libs/monitors/provider-monitor-errors.ts +0 -131
  46. package/dist/bundling-sources/shared_libs/monitors/provider-monitor-identity-claims.ts +0 -527
  47. package/dist/bundling-sources/shared_libs/monitors/provider-monitor-legacy-aliases.ts +0 -16
  48. package/dist/bundling-sources/shared_libs/monitors/provider-monitor-priority-execution.ts +0 -56
  49. package/dist/bundling-sources/shared_libs/monitors/provider-monitor-public-aliases.ts +0 -28
  50. package/dist/bundling-sources/shared_libs/monitors/provider-monitor-runtime-read-eligibility.ts +0 -38
  51. package/dist/bundling-sources/shared_libs/monitors/provider-monitor-status.ts +0 -10
  52. package/dist/bundling-sources/shared_libs/monitors/provider-monitor-upstream-identity.ts +0 -112
  53. package/dist/bundling-sources/shared_libs/monitors/provider-webhook-monitor-runtime.ts +0 -300
  54. package/dist/bundling-sources/shared_libs/monitors/quickstart-prompts.ts +0 -22
  55. package/dist/bundling-sources/shared_libs/monitors/snitcher-install-validation.ts +0 -93
  56. package/dist/bundling-sources/shared_libs/monitors/snitcher-ip2company.ts +0 -332
  57. package/dist/bundling-sources/shared_libs/monitors/snitcher-radar.ts +0 -221
  58. package/dist/bundling-sources/shared_libs/monitors/starter-templates.ts +0 -105
  59. package/dist/bundling-sources/shared_libs/monitors/tracking-provider-guidance.ts +0 -73
  60. package/dist/bundling-sources/shared_libs/monitors/tracking-provider-integrations.ts +0 -81
  61. package/dist/bundling-sources/shared_libs/monitors/tsconfig.json +0 -10
  62. package/dist/bundling-sources/shared_libs/security/header-safe-env.ts +0 -28
  63. package/dist/bundling-sources/shared_libs/security/secret-redaction.ts +0 -267
  64. /package/dist/bundling-sources/shared_libs/{integration-contracts/bettercontact/execution-policy.ts → integrations/bettercontact-execution-policy.ts} +0 -0
  65. /package/dist/bundling-sources/shared_libs/{integration-contracts/theirstack/execution-policy.ts → integrations/theirstack-execution-policy.ts} +0 -0
@@ -1,1125 +0,0 @@
1
- import {
2
- createHash,
3
- createHmac,
4
- randomBytes,
5
- timingSafeEqual,
6
- } from 'node:crypto';
7
- import { unwrapTamradarPayload } from '@deepline/ingestion-v2/provider-native-monitor-writer';
8
-
9
- export const MONITOR_CALLBACK_SECRET_HEADER = 'x-deepline-monitor-secret';
10
-
11
- export type ProviderCallbackVerificationFailure = {
12
- ok: false;
13
- status: 401 | 409 | 422 | 500;
14
- code: string;
15
- message: string;
16
- fields?: string[];
17
- };
18
-
19
- export type ProviderCallbackVerificationOk = {
20
- ok: true;
21
- mode:
22
- | 'hmac_signature'
23
- | 'shared_secret_body'
24
- | 'secret_header'
25
- | 'callback_token_only';
26
- residualRisk?: 'unsigned_provider_webhook';
27
- };
28
-
29
- export type ProviderCallbackAuthenticityResult =
30
- | ProviderCallbackVerificationOk
31
- | ProviderCallbackVerificationFailure;
32
-
33
- export type ProviderCallbackEventContext = {
34
- providerEventId: string;
35
- idempotencyKey: string;
36
- payload: Record<string, unknown>;
37
- verification: {
38
- mode: ProviderCallbackVerificationOk['mode'];
39
- residual_risk?: ProviderCallbackVerificationOk['residualRisk'];
40
- provider_event_id: string;
41
- idempotency_key: string;
42
- binding_fields: string[];
43
- };
44
- };
45
-
46
- export type ProviderCallbackBindingResult =
47
- | { ok: true; event: ProviderCallbackEventContext }
48
- | ProviderCallbackVerificationFailure;
49
-
50
- const SIGNED_PROVIDERS = new Set(['attio', 'theirstack', 'snitcher']);
51
- const BODY_SECRET_PROVIDERS = new Set(['lemlist']);
52
-
53
- const SENSITIVE_HEADER_NAMES = new Set([
54
- 'authorization',
55
- 'cookie',
56
- 'set-cookie',
57
- 'attio-signature',
58
- 'x-attio-signature',
59
- 'x-theirstack-signature-256',
60
- 'signature',
61
- 'x-dev-bootstrap-secret',
62
- MONITOR_CALLBACK_SECRET_HEADER,
63
- ]);
64
-
65
- const PROVENANCE_HEADER_NAMES = new Set([
66
- 'content-type',
67
- 'user-agent',
68
- 'idempotency-key',
69
- 'x-request-id',
70
- 'x-webhook-level',
71
- ]);
72
-
73
- function isRecord(value: unknown): value is Record<string, unknown> {
74
- return Boolean(value && typeof value === 'object' && !Array.isArray(value));
75
- }
76
-
77
- function readRecord(value: unknown): Record<string, unknown> {
78
- return isRecord(value) ? value : {};
79
- }
80
-
81
- function readString(value: unknown) {
82
- if (typeof value === 'number' && Number.isFinite(value)) return String(value);
83
- if (typeof value !== 'string') return null;
84
- const trimmed = value.trim();
85
- return trimmed || null;
86
- }
87
-
88
- function readStringAtPath(record: Record<string, unknown>, path: string) {
89
- const value = path.split('.').reduce<unknown>((current, part) => {
90
- if (!isRecord(current)) return undefined;
91
- return current[part];
92
- }, record);
93
- return readString(value);
94
- }
95
-
96
- function readArray(value: unknown): unknown[] {
97
- return Array.isArray(value) ? value : [];
98
- }
99
-
100
- function readStringArray(value: unknown) {
101
- return readArray(value)
102
- .map((item) => readString(item))
103
- .filter((item): item is string => Boolean(item));
104
- }
105
-
106
- function stableJsonStringify(value: unknown): string {
107
- if (Array.isArray(value)) {
108
- return `[${value.map(stableJsonStringify).join(',')}]`;
109
- }
110
- if (value && typeof value === 'object') {
111
- return `{${Object.entries(value as Record<string, unknown>)
112
- .sort(([left], [right]) => left.localeCompare(right))
113
- .map(
114
- ([key, entry]) =>
115
- `${JSON.stringify(key)}:${stableJsonStringify(entry)}`,
116
- )
117
- .join(',')}}`;
118
- }
119
- return JSON.stringify(value);
120
- }
121
-
122
- function sha256(value: string) {
123
- return createHash('sha256').update(value).digest('hex');
124
- }
125
-
126
- function timingSafeStringEqual(expected: string, actual: string) {
127
- const expectedBuffer = Buffer.from(expected);
128
- const actualBuffer = Buffer.from(actual);
129
- if (expectedBuffer.length !== actualBuffer.length) return false;
130
- return timingSafeEqual(expectedBuffer, actualBuffer);
131
- }
132
-
133
- function timingSafeHexEqual(expectedHex: string, actualHex: string) {
134
- if (!/^[a-f0-9]+$/i.test(expectedHex) || !/^[a-f0-9]+$/i.test(actualHex)) {
135
- return false;
136
- }
137
- const expected = Buffer.from(expectedHex, 'hex');
138
- const actual = Buffer.from(actualHex, 'hex');
139
- if (expected.length === 0 || expected.length !== actual.length) return false;
140
- return timingSafeEqual(expected, actual);
141
- }
142
-
143
- function timingSafeBase64Equal(expectedBase64: string, actualBase64: string) {
144
- const expected = Buffer.from(expectedBase64, 'base64');
145
- const actual = Buffer.from(actualBase64, 'base64');
146
- if (expected.length === 0 || expected.length !== actual.length) return false;
147
- return timingSafeEqual(expected, actual);
148
- }
149
-
150
- // Exported for the GitHub webhook receiver: GitHub's x-hub-signature-256
151
- // (`sha256=<hex>`) parses under the same candidate grammar and constant-time
152
- // comparison used for provider-monitor callbacks.
153
- export function signatureCandidates(header: string) {
154
- return header
155
- .split(/[,\s]+/)
156
- .map((part) => part.trim())
157
- .filter(Boolean)
158
- .map((part) => {
159
- const [, value] =
160
- /^(?:sha256|v1|sig|signature)=([A-Za-z0-9+/=_-]+)$/i.exec(part) ?? [];
161
- return value ?? part;
162
- });
163
- }
164
-
165
- export function verifyHmacSha256(input: {
166
- secret: string;
167
- rawBody: string;
168
- signatureHeader: string | null;
169
- }) {
170
- if (!input.signatureHeader) return false;
171
- const expectedHex = createHmac('sha256', input.secret)
172
- .update(input.rawBody)
173
- .digest('hex');
174
- const expectedBase64 = createHmac('sha256', input.secret)
175
- .update(input.rawBody)
176
- .digest('base64');
177
- return signatureCandidates(input.signatureHeader).some(
178
- (candidate) =>
179
- timingSafeHexEqual(expectedHex, candidate) ||
180
- timingSafeBase64Equal(expectedBase64, candidate),
181
- );
182
- }
183
-
184
- function lowerCaseHeaders(headers: Headers) {
185
- const entries = new Map<string, string>();
186
- headers.forEach((value, key) => {
187
- entries.set(key.toLowerCase(), value);
188
- });
189
- return entries;
190
- }
191
-
192
- function header(headers: Headers, name: string) {
193
- return headers.get(name) ?? headers.get(name.toLowerCase());
194
- }
195
-
196
- function firstStringAtPaths(
197
- record: Record<string, unknown>,
198
- paths: readonly string[],
199
- ) {
200
- for (const path of paths) {
201
- const value = readStringAtPath(record, path);
202
- if (value) return value;
203
- }
204
- return null;
205
- }
206
-
207
- function providerBindingRecords(providerBinding: Record<string, unknown>) {
208
- const runtimePayload = readRecord(providerBinding.runtime_payload);
209
- const runtimeWebhook = readRecord(runtimePayload.webhook);
210
- const upstreamResponse = readRecord(providerBinding.upstream_response);
211
- const upstreamData = readRecord(upstreamResponse.data);
212
- const upstreamWebhook = readRecord(upstreamResponse.webhook);
213
- const upstreamResultData = readRecord(
214
- readRecord(upstreamResponse.result).data,
215
- );
216
- return [
217
- providerBinding,
218
- runtimePayload,
219
- runtimeWebhook,
220
- upstreamResponse,
221
- upstreamData,
222
- upstreamWebhook,
223
- upstreamResultData,
224
- ];
225
- }
226
-
227
- function firstBindingSecret(
228
- providerBinding: Record<string, unknown>,
229
- names: readonly string[],
230
- ) {
231
- for (const record of providerBindingRecords(providerBinding)) {
232
- for (const name of names) {
233
- const value = readString(record[name]);
234
- if (value) return value;
235
- }
236
- }
237
- return null;
238
- }
239
-
240
- function runtimeHeaderSecret(providerBinding: Record<string, unknown>) {
241
- const runtimePayload = readRecord(providerBinding.runtime_payload);
242
- const headers = readRecord(runtimePayload.headers);
243
- const webhookHeaders = readRecord(readRecord(runtimePayload.webhook).headers);
244
- for (const record of [headers, webhookHeaders]) {
245
- for (const [key, value] of Object.entries(record)) {
246
- if (key.toLowerCase() === MONITOR_CALLBACK_SECRET_HEADER) {
247
- return readString(value);
248
- }
249
- }
250
- }
251
- return firstBindingSecret(providerBinding, [
252
- 'deepline_callback_secret',
253
- 'callback_secret',
254
- ]);
255
- }
256
-
257
- function signingSecret(
258
- provider: string,
259
- providerBinding: Record<string, unknown>,
260
- ) {
261
- switch (provider) {
262
- case 'attio':
263
- return firstBindingSecret(providerBinding, [
264
- 'attio_signing_secret',
265
- 'webhook_secret',
266
- 'signing_secret',
267
- 'secret',
268
- ]);
269
- case 'theirstack':
270
- return firstBindingSecret(providerBinding, [
271
- 'theirstack_signing_secret',
272
- 'signing_secret',
273
- 'secret',
274
- ]);
275
- case 'snitcher':
276
- return firstBindingSecret(providerBinding, ['webhook_secret', 'secret']);
277
- default:
278
- return null;
279
- }
280
- }
281
-
282
- function missingSecret(provider: string): ProviderCallbackVerificationFailure {
283
- return {
284
- ok: false,
285
- status: 500,
286
- code: 'provider_callback_signing_secret_missing',
287
- message: `Deepline monitor callback signing secret is not configured for ${provider}.`,
288
- };
289
- }
290
-
291
- function missingSignature(
292
- provider: string,
293
- ): ProviderCallbackVerificationFailure {
294
- return {
295
- ok: false,
296
- status: 401,
297
- code: 'provider_callback_signature_missing',
298
- message: `${provider} callback signature header is missing.`,
299
- };
300
- }
301
-
302
- function invalidSignature(
303
- provider: string,
304
- ): ProviderCallbackVerificationFailure {
305
- return {
306
- ok: false,
307
- status: 401,
308
- code: 'provider_callback_signature_invalid',
309
- message: `${provider} callback signature is invalid.`,
310
- };
311
- }
312
-
313
- export function createProviderCallbackSecret(prefix = 'monsec') {
314
- return `${prefix}_${randomBytes(32).toString('hex')}`;
315
- }
316
-
317
- export function providerCallbackVerificationRequiresParsedPayload(
318
- provider: string,
319
- ) {
320
- return BODY_SECRET_PROVIDERS.has(provider);
321
- }
322
-
323
- export function parseProviderCallbackPayload(input: {
324
- contentType: string | null;
325
- rawBody: string;
326
- }): Record<string, unknown> {
327
- const rawBody = input.rawBody.trim();
328
- if (!rawBody) return {};
329
- const contentType = input.contentType ?? '';
330
- if (contentType.includes('application/json') || rawBody.startsWith('{')) {
331
- try {
332
- return readRecord(JSON.parse(rawBody) as unknown);
333
- } catch {
334
- return {};
335
- }
336
- }
337
- try {
338
- return readRecord(JSON.parse(rawBody) as unknown);
339
- } catch {
340
- return { raw_body: input.rawBody };
341
- }
342
- }
343
-
344
- export function verifyProviderCallbackAuthenticity(input: {
345
- provider: string;
346
- headers: Headers;
347
- rawBody: string;
348
- providerBinding: Record<string, unknown>;
349
- payload?: Record<string, unknown>;
350
- }): ProviderCallbackAuthenticityResult {
351
- if (input.provider === 'attio') {
352
- const secret = signingSecret(input.provider, input.providerBinding);
353
- if (!secret) return missingSecret(input.provider);
354
- const signature =
355
- header(input.headers, 'attio-signature') ??
356
- header(input.headers, 'x-attio-signature');
357
- if (!signature) return missingSignature(input.provider);
358
- return verifyHmacSha256({
359
- secret,
360
- rawBody: input.rawBody,
361
- signatureHeader: signature,
362
- })
363
- ? { ok: true, mode: 'hmac_signature' }
364
- : invalidSignature(input.provider);
365
- }
366
-
367
- if (input.provider === 'theirstack') {
368
- const secret = signingSecret(input.provider, input.providerBinding);
369
- if (!secret) return missingSecret(input.provider);
370
- const signature = header(input.headers, 'x-theirstack-signature-256');
371
- if (!signature) return missingSignature(input.provider);
372
- return verifyHmacSha256({
373
- secret,
374
- rawBody: input.rawBody,
375
- signatureHeader: signature,
376
- })
377
- ? { ok: true, mode: 'hmac_signature' }
378
- : invalidSignature(input.provider);
379
- }
380
-
381
- if (input.provider === 'snitcher') {
382
- const secret = signingSecret(input.provider, input.providerBinding);
383
- if (!secret) return missingSecret(input.provider);
384
- const signature = header(input.headers, 'signature');
385
- if (!signature) return missingSignature(input.provider);
386
- return verifyHmacSha256({
387
- secret,
388
- rawBody: input.rawBody,
389
- signatureHeader: signature,
390
- })
391
- ? { ok: true, mode: 'hmac_signature' }
392
- : invalidSignature(input.provider);
393
- }
394
-
395
- if (input.provider === 'lemlist') {
396
- const expected = firstBindingSecret(input.providerBinding, [
397
- 'lemlist_secret',
398
- 'webhook_secret',
399
- 'secret',
400
- ]);
401
- if (!expected) return missingSecret(input.provider);
402
- const actual = readString(input.payload?.secret);
403
- if (!actual) {
404
- return {
405
- ok: false,
406
- status: 401,
407
- code: 'provider_callback_secret_missing',
408
- message: 'Lemlist callback shared secret is missing.',
409
- };
410
- }
411
- return timingSafeStringEqual(expected, actual)
412
- ? { ok: true, mode: 'shared_secret_body' }
413
- : {
414
- ok: false,
415
- status: 401,
416
- code: 'provider_callback_secret_invalid',
417
- message: 'Lemlist callback shared secret is invalid.',
418
- };
419
- }
420
-
421
- const configuredHeaderSecret = runtimeHeaderSecret(input.providerBinding);
422
- if (configuredHeaderSecret) {
423
- const actual = header(input.headers, MONITOR_CALLBACK_SECRET_HEADER);
424
- if (!actual) {
425
- return {
426
- ok: false,
427
- status: 401,
428
- code: 'provider_callback_secret_header_missing',
429
- message: 'Deepline monitor callback secret header is missing.',
430
- };
431
- }
432
- return timingSafeStringEqual(configuredHeaderSecret, actual)
433
- ? { ok: true, mode: 'secret_header' }
434
- : {
435
- ok: false,
436
- status: 401,
437
- code: 'provider_callback_secret_header_invalid',
438
- message: 'Deepline monitor callback secret header is invalid.',
439
- };
440
- }
441
-
442
- return {
443
- ok: true,
444
- mode: 'callback_token_only',
445
- residualRisk: 'unsigned_provider_webhook',
446
- };
447
- }
448
-
449
- function sanitizedPayloadForProvider(
450
- provider: string,
451
- payload: Record<string, unknown>,
452
- ) {
453
- if (
454
- provider !== 'lemlist' ||
455
- !Object.prototype.hasOwnProperty.call(payload, 'secret')
456
- ) {
457
- return payload;
458
- }
459
- return {
460
- ...payload,
461
- secret: '[redacted]',
462
- };
463
- }
464
-
465
- function addMismatch(
466
- fields: string[],
467
- input: { field: string; expected?: string | null; actual?: string | null },
468
- ) {
469
- if (!input.expected) return;
470
- if (!input.actual) {
471
- fields.push(input.field);
472
- return;
473
- }
474
- if (input.expected !== input.actual) fields.push(input.field);
475
- }
476
-
477
- function addSetMismatch(
478
- fields: string[],
479
- input: {
480
- field: string;
481
- expected: readonly string[];
482
- actual: readonly string[];
483
- allowAll?: string;
484
- },
485
- ) {
486
- if (!input.expected.length) return;
487
- if (input.allowAll && input.expected.includes(input.allowAll)) return;
488
- if (!input.actual.length) {
489
- fields.push(input.field);
490
- return;
491
- }
492
- const expected = new Set(input.expected.map((value) => value.toLowerCase()));
493
- if (input.actual.some((value) => !expected.has(value.toLowerCase()))) {
494
- fields.push(input.field);
495
- }
496
- }
497
-
498
- function upstreamWebhookId(providerBinding: Record<string, unknown>) {
499
- return (
500
- readString(providerBinding.upstream_resource_id) ??
501
- readString(providerBinding.upstream_monitor_id)
502
- );
503
- }
504
-
505
- /**
506
- * TamRadar delivers its upstream resource identity inside the event envelope.
507
- * This is the identity Deepline persisted when the monitor was installed; the
508
- * callback URL/token is only an authentication route and can be shared across
509
- * updates. Never use payload filters or provider response metadata as a
510
- * substitute for this binding fence.
511
- */
512
- export function tamradarCallbackRadarId(payload: Record<string, unknown>) {
513
- const unwrapped = unwrapTamradarPayload(payload);
514
- return (
515
- readStringAtPath(unwrapped, 'data.radar_id') ??
516
- // Legacy provider payloads placed the id at the root. Keep that documented
517
- // wire compatibility, but still require it to match the stored binding.
518
- readString(unwrapped.radar_id)
519
- );
520
- }
521
-
522
- function genericWebhookIds(payload: Record<string, unknown>) {
523
- return [
524
- 'webhook_id',
525
- 'webhookId',
526
- 'hook_id',
527
- 'hookId',
528
- 'webhook.id',
529
- 'data.webhook_id',
530
- 'data.webhookId',
531
- ]
532
- .map((path) => readStringAtPath(payload, path))
533
- .filter((value): value is string => Boolean(value));
534
- }
535
-
536
- function runtimePayload(providerBinding: Record<string, unknown>) {
537
- return readRecord(providerBinding.runtime_payload);
538
- }
539
-
540
- function upstreamResponse(providerBinding: Record<string, unknown>) {
541
- return readRecord(providerBinding.upstream_response);
542
- }
543
-
544
- function expectedRuntimeEventTypes(providerBinding: Record<string, unknown>) {
545
- const runtime = runtimePayload(providerBinding);
546
- const webhook = readRecord(runtime.webhook);
547
- const subscriptions = readArray(runtime.subscriptions);
548
- return [
549
- readString(runtime.event_type),
550
- readString(runtime.type),
551
- ...readStringArray(runtime.event_types),
552
- ...readStringArray(webhook.event_types),
553
- ...subscriptions
554
- .map((item) => readString(readRecord(item).event_type))
555
- .filter((value): value is string => Boolean(value)),
556
- ].filter((value): value is string => Boolean(value));
557
- }
558
-
559
- function attioEventTypes(payload: Record<string, unknown>) {
560
- const events = Array.isArray(payload.events) ? payload.events : [payload];
561
- return events
562
- .map((item) => {
563
- const event = readRecord(item);
564
- return readString(event.event_type) ?? readString(event.eventType);
565
- })
566
- .filter((value): value is string => Boolean(value));
567
- }
568
-
569
- function instantlyProviderEventId(payload: Record<string, unknown>) {
570
- return (
571
- firstStringAtPaths(payload, [
572
- 'event_id',
573
- 'id',
574
- 'webhook_event_id',
575
- 'delivery_id',
576
- ]) ??
577
- `deterministic:${sha256(
578
- stableJsonStringify({
579
- event_type: readString(payload.event_type) ?? readString(payload.type),
580
- workspace:
581
- readString(payload.workspace) ?? readString(payload.workspace_id),
582
- campaign_id: readString(payload.campaign_id),
583
- lead_id: readString(payload.lead_id),
584
- lead_email: readString(payload.lead_email),
585
- email_id: readString(payload.email_id),
586
- timestamp: readString(payload.timestamp),
587
- }),
588
- ).slice(0, 32)}`
589
- );
590
- }
591
-
592
- function lemlistProviderEventId(payload: Record<string, unknown>) {
593
- return (
594
- readString(payload._id) ??
595
- readString(payload.id) ??
596
- readString(payload.activityId) ??
597
- `deterministic:${sha256(stableJsonStringify(payload)).slice(0, 32)}`
598
- );
599
- }
600
-
601
- function heyreachProviderEventId(payload: Record<string, unknown>) {
602
- return (
603
- firstStringAtPaths(payload, [
604
- 'delivery_id',
605
- 'event_id',
606
- 'id',
607
- 'webhook_event_id',
608
- ]) ??
609
- `deterministic:${sha256(
610
- stableJsonStringify({
611
- event_type:
612
- readString(payload.event_type) ??
613
- readString(payload.eventType) ??
614
- readString(payload.type),
615
- campaign_id: readString(payload.campaign_id),
616
- profile_url:
617
- readString(payload.profile_url) ?? readString(payload.linkedin_url),
618
- message_id: readString(payload.message_id),
619
- lead_id: readString(payload.lead_id),
620
- payload,
621
- }),
622
- ).slice(0, 32)}`
623
- );
624
- }
625
-
626
- function theirstackProviderEventId(payload: Record<string, unknown>) {
627
- const id = readString(payload.id);
628
- const type = readString(payload.type) ?? readString(payload.event_type);
629
- if (id && type) return `${type}:${id}`;
630
- return `deterministic:${sha256(stableJsonStringify(payload)).slice(0, 32)}`;
631
- }
632
-
633
- // Providers whose event id is guaranteed by their contract (Attio Idempotency-Key
634
- // header, TheirStack payload id+type). If we ever fall back to the sha256 hash for
635
- // one of these, that is a real bug in extraction (or a provider contract change),
636
- // so make it observable loudly rather than silently content-addressing.
637
- const GUARANTEED_EVENT_ID_PROVIDERS = new Set(['attio', 'theirstack']);
638
-
639
- function observeHashFallback(input: {
640
- provider: string;
641
- eventId: string;
642
- payload: Record<string, unknown>;
643
- }) {
644
- if (!input.eventId.startsWith('deterministic:')) return;
645
- // Structured signal so we can measure real exposure of the no-upstream-id
646
- // fallback per provider (audit query in scripts/check-signal-radar-hash-fallback).
647
- const detail = {
648
- provider: input.provider,
649
- fallback: 'sha256_payload',
650
- event_id: input.eventId,
651
- guaranteed_provider: GUARANTEED_EVENT_ID_PROVIDERS.has(input.provider),
652
- };
653
- if (GUARANTEED_EVENT_ID_PROVIDERS.has(input.provider)) {
654
- // Loud: a provider that is supposed to always supply an id did not.
655
- console.error('[signal-radar.dedupe.hash_fallback]', detail);
656
- } else {
657
- console.warn('[signal-radar.dedupe.hash_fallback]', detail);
658
- }
659
- }
660
-
661
- function providerEventId(input: {
662
- provider: string;
663
- headers: Headers;
664
- payload: Record<string, unknown>;
665
- }) {
666
- const eventId = providerEventIdInner(input);
667
- observeHashFallback({
668
- provider: input.provider,
669
- eventId,
670
- payload: input.payload,
671
- });
672
- return eventId;
673
- }
674
-
675
- function providerEventIdInner(input: {
676
- provider: string;
677
- headers: Headers;
678
- payload: Record<string, unknown>;
679
- }) {
680
- switch (input.provider) {
681
- case 'attio':
682
- return (
683
- header(input.headers, 'idempotency-key') ??
684
- firstStringAtPaths(input.payload, ['event_id', 'delivery_id', 'id']) ??
685
- `deterministic:${sha256(stableJsonStringify(input.payload)).slice(0, 32)}`
686
- );
687
- case 'instantly':
688
- return instantlyProviderEventId(input.payload);
689
- case 'lemlist':
690
- return lemlistProviderEventId(input.payload);
691
- case 'smartlead':
692
- return (
693
- header(input.headers, 'x-request-id') ??
694
- firstStringAtPaths(input.payload, [
695
- 'event_id',
696
- 'id',
697
- 'webhook_event_id',
698
- 'delivery_id',
699
- ]) ??
700
- `deterministic:${sha256(stableJsonStringify(input.payload)).slice(0, 32)}`
701
- );
702
- case 'heyreach':
703
- return heyreachProviderEventId(input.payload);
704
- case 'theirstack':
705
- return theirstackProviderEventId(input.payload);
706
- case 'deepline_native':
707
- // Radar (deepline_native) finding-charge idempotency keys on the stable
708
- // per-update id (design §1/§11), not a full-payload hash — a redelivery of
709
- // the same update_id with any differing byte (timestamp/envelope) must
710
- // dedupe. Unwrap first so this works for both the raw webhook body and the
711
- // `{status,code,payload}` execute-envelope shape (matching refund + writer).
712
- return (
713
- firstStringAtPaths(unwrapTamradarPayload(input.payload), [
714
- 'update_id',
715
- 'event_id',
716
- ]) ??
717
- `deterministic:${sha256(stableJsonStringify(input.payload)).slice(0, 32)}`
718
- );
719
- default:
720
- return `deterministic:${sha256(stableJsonStringify(input.payload)).slice(0, 32)}`;
721
- }
722
- }
723
-
724
- export function monitorEventIdempotencyKey(input: {
725
- provider: string;
726
- monitorKey: string;
727
- sourceCapability: string;
728
- upstreamWebhookId?: string | null;
729
- providerEventId: string;
730
- }) {
731
- return [
732
- 'monitor_event',
733
- input.provider,
734
- input.monitorKey,
735
- input.sourceCapability,
736
- input.upstreamWebhookId ?? 'unbound',
737
- input.providerEventId,
738
- ]
739
- .map(encodeURIComponent)
740
- .join(':');
741
- }
742
-
743
- function validateAttio(input: {
744
- payload: Record<string, unknown>;
745
- providerBinding: Record<string, unknown>;
746
- fields: string[];
747
- }) {
748
- const expectedWebhookId = upstreamWebhookId(input.providerBinding);
749
- for (const actual of genericWebhookIds(input.payload)) {
750
- addMismatch(input.fields, {
751
- field: 'webhook_id',
752
- expected: expectedWebhookId,
753
- actual,
754
- });
755
- }
756
- addSetMismatch(input.fields, {
757
- field: 'event_type',
758
- expected: expectedRuntimeEventTypes(input.providerBinding),
759
- actual: attioEventTypes(input.payload),
760
- });
761
- }
762
-
763
- function validateInstantly(input: {
764
- payload: Record<string, unknown>;
765
- providerBinding: Record<string, unknown>;
766
- fields: string[];
767
- }) {
768
- const runtime = runtimePayload(input.providerBinding);
769
- const upstream = upstreamResponse(input.providerBinding);
770
- const expectedWebhookId = upstreamWebhookId(input.providerBinding);
771
- for (const actual of genericWebhookIds(input.payload)) {
772
- addMismatch(input.fields, {
773
- field: 'webhook_id',
774
- expected: expectedWebhookId,
775
- actual,
776
- });
777
- }
778
- addMismatch(input.fields, {
779
- field: 'workspace',
780
- expected:
781
- readString(upstream.organization) ??
782
- readString(runtime.organization) ??
783
- readString(runtime.workspace),
784
- actual:
785
- readString(input.payload.workspace) ??
786
- readString(input.payload.workspace_id),
787
- });
788
- addMismatch(input.fields, {
789
- field: 'campaign_id',
790
- expected: readString(runtime.campaign) ?? readString(upstream.campaign),
791
- actual: readString(input.payload.campaign_id),
792
- });
793
- addSetMismatch(input.fields, {
794
- field: 'event_type',
795
- expected: expectedRuntimeEventTypes(input.providerBinding),
796
- actual: [
797
- readString(input.payload.event_type) ??
798
- readString(input.payload.eventType) ??
799
- readString(input.payload.type),
800
- ].filter((value): value is string => Boolean(value)),
801
- allowAll: 'all_events',
802
- });
803
- }
804
-
805
- function validateLemlist(input: {
806
- payload: Record<string, unknown>;
807
- providerBinding: Record<string, unknown>;
808
- fields: string[];
809
- }) {
810
- const runtime = runtimePayload(input.providerBinding);
811
- const upstream = upstreamResponse(input.providerBinding);
812
- for (const actual of genericWebhookIds(input.payload)) {
813
- addMismatch(input.fields, {
814
- field: 'webhook_id',
815
- expected: upstreamWebhookId(input.providerBinding),
816
- actual,
817
- });
818
- }
819
- addMismatch(input.fields, {
820
- field: 'team_id',
821
- expected: readString(upstream.teamId) ?? readString(upstream.team_id),
822
- actual:
823
- readString(input.payload.teamId) ?? readString(input.payload.team_id),
824
- });
825
- addMismatch(input.fields, {
826
- field: 'event_type',
827
- expected: readString(runtime.type) ?? readString(upstream.type),
828
- actual:
829
- readString(input.payload.type) ?? readString(input.payload.event_type),
830
- });
831
- addMismatch(input.fields, {
832
- field: 'campaign_id',
833
- expected:
834
- readString(runtime.campaign_id) ?? readString(upstream.campaignId),
835
- actual:
836
- readString(input.payload.campaignId) ??
837
- readString(input.payload.campaign_id) ??
838
- readString(readRecord(input.payload.metadata).campaignId),
839
- });
840
- }
841
-
842
- function validateSmartlead(input: {
843
- headers: Headers;
844
- payload: Record<string, unknown>;
845
- providerBinding: Record<string, unknown>;
846
- fields: string[];
847
- }) {
848
- const runtime = runtimePayload(input.providerBinding);
849
- const level = header(input.headers, 'x-webhook-level');
850
- if (!level) input.fields.push('x-webhook-level');
851
- addMismatch(input.fields, {
852
- field: 'x-webhook-level',
853
- expected: 'campaign',
854
- actual: level?.toLowerCase() ?? null,
855
- });
856
- for (const actual of genericWebhookIds(input.payload)) {
857
- addMismatch(input.fields, {
858
- field: 'webhook_id',
859
- expected: upstreamWebhookId(input.providerBinding),
860
- actual,
861
- });
862
- }
863
- addMismatch(input.fields, {
864
- field: 'campaign_id',
865
- expected: readString(runtime.campaign_id),
866
- actual: readString(input.payload.campaign_id),
867
- });
868
- addSetMismatch(input.fields, {
869
- field: 'event_type',
870
- expected: readStringArray(readRecord(runtime.webhook).event_types),
871
- actual: [
872
- readString(input.payload.event_type) ?? readString(input.payload.event),
873
- ].filter((value): value is string => Boolean(value)),
874
- });
875
- }
876
-
877
- function validateHeyreach(input: {
878
- payload: Record<string, unknown>;
879
- providerBinding: Record<string, unknown>;
880
- fields: string[];
881
- }) {
882
- const runtime = runtimePayload(input.providerBinding);
883
- for (const actual of genericWebhookIds(input.payload)) {
884
- addMismatch(input.fields, {
885
- field: 'webhook_id',
886
- expected: upstreamWebhookId(input.providerBinding),
887
- actual,
888
- });
889
- }
890
- const expectedCampaigns = readStringArray(runtime.campaign_ids);
891
- const campaignId = readString(input.payload.campaign_id);
892
- addSetMismatch(input.fields, {
893
- field: 'campaign_id',
894
- expected: expectedCampaigns,
895
- actual: campaignId ? [campaignId] : [],
896
- });
897
- addSetMismatch(input.fields, {
898
- field: 'event_type',
899
- expected: expectedRuntimeEventTypes(input.providerBinding),
900
- actual: [
901
- readString(input.payload.event_type) ??
902
- readString(input.payload.eventType) ??
903
- readString(input.payload.type),
904
- ].filter((value): value is string => Boolean(value)),
905
- });
906
- }
907
-
908
- function theirstackSavedSearchId(providerBinding: Record<string, unknown>) {
909
- const upstream = upstreamResponse(providerBinding);
910
- const savedSearch = readRecord(upstream.saved_search);
911
- const webhook = readRecord(upstream.webhook);
912
- return (
913
- readString(savedSearch.id) ??
914
- readString(readRecord(savedSearch.data).id) ??
915
- readString(webhook.search_id) ??
916
- readString(readRecord(webhook.data).search_id)
917
- );
918
- }
919
-
920
- function validateTheirstack(input: {
921
- payload: Record<string, unknown>;
922
- providerBinding: Record<string, unknown>;
923
- fields: string[];
924
- }) {
925
- addMismatch(input.fields, {
926
- field: 'webhook_id',
927
- expected: upstreamWebhookId(input.providerBinding),
928
- actual:
929
- readString(input.payload.webhook_id) ??
930
- readString(input.payload.webhookId),
931
- });
932
- addMismatch(input.fields, {
933
- field: 'search_id',
934
- expected: theirstackSavedSearchId(input.providerBinding),
935
- actual:
936
- readString(input.payload.search_id) ??
937
- readString(input.payload.saved_search_id),
938
- });
939
- const expectedType = readStringAtPath(
940
- runtimePayload(input.providerBinding),
941
- 'saved_search.type',
942
- );
943
- const actualEventType =
944
- readString(input.payload.type) ?? readString(input.payload.event_type);
945
- if (
946
- expectedType === 'jobs' &&
947
- actualEventType &&
948
- !actualEventType.startsWith('job.')
949
- ) {
950
- input.fields.push('event_type');
951
- }
952
- if (
953
- expectedType === 'companies' &&
954
- actualEventType &&
955
- actualEventType !== 'company.new'
956
- ) {
957
- input.fields.push('event_type');
958
- }
959
- }
960
-
961
- function validateDeeplineNative(input: {
962
- payload: Record<string, unknown>;
963
- providerBinding: Record<string, unknown>;
964
- fields: string[];
965
- }) {
966
- const expected = upstreamWebhookId(input.providerBinding);
967
- const actual = tamradarCallbackRadarId(input.payload);
968
- // A callback without an attributable upstream radar is not safe to ingest.
969
- // Fail closed before the raw receipt, billing, typed writer, or dispatch.
970
- if (!expected || !actual || expected !== actual) {
971
- input.fields.push('radar_id');
972
- }
973
- }
974
-
975
- export function validateProviderCallbackBindingAndIdempotency(input: {
976
- provider: string;
977
- headers: Headers;
978
- payload: Record<string, unknown>;
979
- providerBinding: Record<string, unknown>;
980
- monitorKey: string;
981
- sourceCapability: string;
982
- authenticity: ProviderCallbackVerificationOk;
983
- }): ProviderCallbackBindingResult {
984
- const fields: string[] = [];
985
- const payload = sanitizedPayloadForProvider(input.provider, input.payload);
986
-
987
- switch (input.provider) {
988
- case 'attio':
989
- validateAttio({
990
- payload,
991
- providerBinding: input.providerBinding,
992
- fields,
993
- });
994
- break;
995
- case 'instantly':
996
- validateInstantly({
997
- payload,
998
- providerBinding: input.providerBinding,
999
- fields,
1000
- });
1001
- break;
1002
- case 'lemlist':
1003
- validateLemlist({
1004
- payload,
1005
- providerBinding: input.providerBinding,
1006
- fields,
1007
- });
1008
- break;
1009
- case 'smartlead':
1010
- validateSmartlead({
1011
- headers: input.headers,
1012
- payload,
1013
- providerBinding: input.providerBinding,
1014
- fields,
1015
- });
1016
- break;
1017
- case 'heyreach':
1018
- validateHeyreach({
1019
- payload,
1020
- providerBinding: input.providerBinding,
1021
- fields,
1022
- });
1023
- break;
1024
- case 'theirstack':
1025
- validateTheirstack({
1026
- payload,
1027
- providerBinding: input.providerBinding,
1028
- fields,
1029
- });
1030
- break;
1031
- case 'deepline_native':
1032
- validateDeeplineNative({
1033
- payload,
1034
- providerBinding: input.providerBinding,
1035
- fields,
1036
- });
1037
- break;
1038
- }
1039
-
1040
- const uniqueFields = Array.from(new Set(fields));
1041
- if (uniqueFields.length > 0) {
1042
- return {
1043
- ok: false,
1044
- status: 409,
1045
- code: 'provider_callback_binding_mismatch',
1046
- message:
1047
- 'Callback payload provider identifiers do not match the authenticated Deepline monitor binding.',
1048
- fields: uniqueFields,
1049
- };
1050
- }
1051
-
1052
- const eventId = providerEventId({
1053
- provider: input.provider,
1054
- headers: input.headers,
1055
- payload,
1056
- });
1057
- const idempotencyKey = monitorEventIdempotencyKey({
1058
- provider: input.provider,
1059
- monitorKey: input.monitorKey,
1060
- sourceCapability: input.sourceCapability,
1061
- upstreamWebhookId: upstreamWebhookId(input.providerBinding),
1062
- providerEventId: eventId,
1063
- });
1064
-
1065
- return {
1066
- ok: true,
1067
- event: {
1068
- providerEventId: eventId,
1069
- idempotencyKey,
1070
- payload,
1071
- verification: {
1072
- mode: input.authenticity.mode,
1073
- ...(input.authenticity.residualRisk
1074
- ? { residual_risk: input.authenticity.residualRisk }
1075
- : {}),
1076
- provider_event_id: eventId,
1077
- idempotency_key: idempotencyKey,
1078
- binding_fields: uniqueFields,
1079
- },
1080
- },
1081
- };
1082
- }
1083
-
1084
- // Neon-free provider event id: derives a stable per-event id from ONLY what is on
1085
- // the wire (headers + parsed payload). Used by the thin inbound receiver to
1086
- // compute a publish idempotency key BEFORE any Neon binding lookup. Same
1087
- // derivation the binding validator uses downstream, so the publish-side dedupe
1088
- // grain matches the consumer-side dedupe grain exactly.
1089
- export function providerCallbackWireEventId(input: {
1090
- provider: string;
1091
- headers: Headers;
1092
- payload: Record<string, unknown>;
1093
- }): string {
1094
- return providerEventId(input);
1095
- }
1096
-
1097
- export function redactedProviderCallbackHeaders(headers: Headers) {
1098
- const normalized = lowerCaseHeaders(headers);
1099
- const result: Record<string, string> = {};
1100
- for (const [key, value] of normalized) {
1101
- if (SENSITIVE_HEADER_NAMES.has(key)) continue;
1102
- if (!PROVENANCE_HEADER_NAMES.has(key)) continue;
1103
- result[key] = value;
1104
- }
1105
- return result;
1106
- }
1107
-
1108
- export function providerCallbackProvenance(input: {
1109
- headers: Headers;
1110
- verification: ProviderCallbackEventContext['verification'];
1111
- monitorTool?: string | null;
1112
- runtimeAction?: string | null;
1113
- }) {
1114
- return {
1115
- callback_route: 'provider_webhook_monitor_callback',
1116
- ...(input.monitorTool ? { monitor_tool: input.monitorTool } : {}),
1117
- ...(input.runtimeAction ? { runtime_action: input.runtimeAction } : {}),
1118
- verification: input.verification,
1119
- headers: redactedProviderCallbackHeaders(input.headers),
1120
- };
1121
- }
1122
-
1123
- export function providerUsesRequiredSignature(provider: string) {
1124
- return SIGNED_PROVIDERS.has(provider);
1125
- }