create-safest-tools 0.4.0 → 0.4.1

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.
@@ -1,4 +1,5 @@
1
1
  import { AUDIT_GENESIS_HASH, canonicalJson, hashAuditEntry } from "./audit";
2
+ import { selectParticipantDelivery, type ParticipantDelivery } from "./report-email";
2
3
  import { ApiError } from "./report-http";
3
4
  import { analystAvatarColor } from "./report-presence";
4
5
  import type { DeliveryJob, OperatorSession, ParticipantAudience, ReportState } from "./report-types";
@@ -6,6 +7,7 @@ import type { AppealDecisionInput, DecisionInput, ReviewerMessageInput } from ".
6
7
 
7
8
  interface WorkflowReportRow {
8
9
  id: string;
10
+ installation_id: string;
9
11
  public_reference: string;
10
12
  state: ReportState;
11
13
  queue_id: string | null;
@@ -14,6 +16,8 @@ interface WorkflowReportRow {
14
16
  target_reference: string;
15
17
  owner_reference: string | null;
16
18
  reporter_reference: string | null;
19
+ reporter_contact_mode: string;
20
+ reporter_contact_ciphertext: string | null;
17
21
  }
18
22
 
19
23
  interface ClaimRow {
@@ -62,8 +66,9 @@ function auditEvent(
62
66
 
63
67
  async function workflowReport(db: D1Database, reportId: string): Promise<WorkflowReportRow | null> {
64
68
  return db.prepare(`
65
- SELECT r.id, r.public_reference, r.state, r.queue_id, r.queue_version_id, r.policy_version_id,
66
- t.target_reference, t.owner_reference, p.reference AS reporter_reference
69
+ SELECT r.id, r.installation_id, r.public_reference, r.state, r.queue_id, r.queue_version_id, r.policy_version_id,
70
+ t.target_reference, t.owner_reference, p.reference AS reporter_reference,
71
+ p.contact_mode AS reporter_contact_mode, p.contact_ciphertext AS reporter_contact_ciphertext
67
72
  FROM reports r
68
73
  JOIN report_targets t ON t.report_id = r.id
69
74
  JOIN report_participants p ON p.report_id = r.id AND p.audience = 'reporter'
@@ -160,6 +165,8 @@ export async function loadReportDetail(db: D1Database, reportId: string): Promis
160
165
  SELECT r.id, r.public_reference AS reference, r.source, r.state, r.priority, r.reason_code AS reasonCode,
161
166
  r.locale, r.queue_id AS queueId, r.submitted_at AS submittedAt, r.received_at AS receivedAt,
162
167
  r.updated_at AS updatedAt, r.due_at AS dueAt, r.resolved_at AS resolvedAt,
168
+ r.form_version_id AS formVersionId, form.title AS formTitle,
169
+ form.fields_json AS formFieldsJson, form.consent_notice AS consentNotice,
163
170
  r.policy_version_id AS policyVersionId, policy.title AS policyTitle,
164
171
  policy.rules_json AS policyRulesJson, r.queue_version_id AS queuePolicyVersionId,
165
172
  qv.management_mode AS queueManagementMode, qv.reviewer_instructions AS reviewerInstructions,
@@ -170,6 +177,7 @@ export async function loadReportDetail(db: D1Database, reportId: string): Promis
170
177
  JOIN report_targets t ON t.report_id = r.id
171
178
  JOIN report_participants p ON p.report_id = r.id AND p.audience = 'reporter'
172
179
  LEFT JOIN policy_versions policy ON policy.id = r.policy_version_id
180
+ LEFT JOIN report_form_versions form ON form.id = r.form_version_id
173
181
  LEFT JOIN queue_versions qv ON qv.id = r.queue_version_id
174
182
  WHERE r.id = ?1
175
183
  LIMIT 1
@@ -227,6 +235,37 @@ export async function loadReportDetail(db: D1Database, reportId: string): Promis
227
235
  LIMIT 500
228
236
  `).bind(reportId).all<Record<string, unknown>>(),
229
237
  ]);
238
+ const formFieldsValue = parsedJson(report.formFieldsJson, []);
239
+ const formFields = Array.isArray(formFieldsValue)
240
+ ? formFieldsValue.filter((field): field is Record<string, unknown> => Boolean(field) && typeof field === "object" && !Array.isArray(field))
241
+ : [];
242
+ const answerRows = new Map(answers.results.map((row) => [String(row.fieldKey), row]));
243
+ const projectedAnswers = formFields.flatMap((field) => {
244
+ const fieldKey = typeof field.key === "string" ? field.key : "";
245
+ if (!fieldKey) return [];
246
+ const row = answerRows.get(fieldKey);
247
+ answerRows.delete(fieldKey);
248
+ return [{
249
+ fieldKey,
250
+ label: typeof field.label === "string" ? field.label : fieldKey,
251
+ type: typeof field.type === "string" ? field.type : "unknown",
252
+ required: field.required === true,
253
+ options: Array.isArray(field.options) ? field.options : [],
254
+ answered: Boolean(row),
255
+ value: row ? parsedJson(row.valueJson, null) : null,
256
+ }];
257
+ });
258
+ for (const [fieldKey, row] of answerRows) {
259
+ projectedAnswers.push({
260
+ fieldKey,
261
+ label: fieldKey,
262
+ type: "unknown",
263
+ required: false,
264
+ options: [],
265
+ answered: true,
266
+ value: parsedJson(row.valueJson, null),
267
+ });
268
+ }
230
269
  return {
231
270
  ...report,
232
271
  policyRules: (() => {
@@ -236,7 +275,8 @@ export async function loadReportDetail(db: D1Database, reportId: string): Promis
236
275
  } catch { return []; }
237
276
  })(),
238
277
  policyRulesJson: undefined,
239
- answers: answers.results.map((row) => ({ fieldKey: row.fieldKey, value: JSON.parse(String(row.valueJson)) })),
278
+ formFieldsJson: undefined,
279
+ answers: projectedAnswers,
240
280
  evidence: evidence.results,
241
281
  messages: messages.results.map((row) => ({ ...row, automated: row.automated === 1 })),
242
282
  decisions: decisions.results.map((row) => ({
@@ -278,20 +318,43 @@ export async function loadReportDetail(db: D1Database, reportId: string): Promis
278
318
  }
279
319
 
280
320
  export interface WorkflowDeliveryOptions {
321
+ emailEnabled: boolean;
322
+ contactEncryptionKey: string;
323
+ recipientHashPepper: string;
281
324
  notificationEnabled: boolean;
282
325
  }
283
326
 
327
+ async function reporterDelivery(
328
+ report: WorkflowReportRow,
329
+ options: WorkflowDeliveryOptions,
330
+ ): Promise<ParticipantDelivery> {
331
+ return selectParticipantDelivery({
332
+ reportId: report.id,
333
+ audience: "reporter",
334
+ contactMode: report.reporter_contact_mode,
335
+ contactCiphertext: report.reporter_contact_ciphertext,
336
+ reference: report.reporter_reference,
337
+ ...options,
338
+ });
339
+ }
340
+
284
341
  export async function addReviewerMessage(
285
342
  db: D1Database,
286
343
  reportId: string,
287
344
  session: OperatorSession,
288
345
  input: ReviewerMessageInput,
289
346
  options: WorkflowDeliveryOptions,
290
- ): Promise<{ messageId: string; jobs: DeliveryJob[]; idempotentReplay: boolean }> {
347
+ ): Promise<{ messageId: string; jobs: DeliveryJob[]; deliveryState: string; deliveryChannel: ParticipantDelivery["channel"]; idempotentReplay: boolean }> {
291
348
  const existing = await db.prepare(`
292
- SELECT id FROM case_messages WHERE report_id = ?1 AND idempotency_key = ?2 LIMIT 1
293
- `).bind(reportId, input.idempotencyKey).first<{ id: string }>();
294
- if (existing) return { messageId: existing.id, jobs: [], idempotentReplay: true };
349
+ SELECT m.id, m.delivery_state AS deliveryState,
350
+ CASE
351
+ WHEN EXISTS (SELECT 1 FROM email_deliveries e WHERE e.message_id = m.id) THEN 'email'
352
+ WHEN EXISTS (SELECT 1 FROM notices n JOIN notification_outbox o ON o.notice_id = n.id WHERE n.message_id = m.id) THEN 'customer_webhook'
353
+ ELSE NULL
354
+ END AS deliveryChannel
355
+ FROM case_messages m WHERE m.report_id = ?1 AND m.idempotency_key = ?2 LIMIT 1
356
+ `).bind(reportId, input.idempotencyKey).first<{ id: string; deliveryState: string; deliveryChannel: ParticipantDelivery["channel"] }>();
357
+ if (existing) return { messageId: existing.id, jobs: [], deliveryState: existing.deliveryState, deliveryChannel: existing.deliveryChannel, idempotentReplay: true };
295
358
  const createdAt = now();
296
359
  await requireActiveClaim(db, reportId, session.actor.id, createdAt);
297
360
  const report = await workflowReport(db, reportId);
@@ -301,24 +364,27 @@ export async function addReviewerMessage(
301
364
  }
302
365
  const messageId = crypto.randomUUID();
303
366
  const noticeId = crypto.randomUUID();
304
- const notificationId = crypto.randomUUID();
367
+ const deliveryId = crypto.randomUUID();
368
+ const delivery = await reporterDelivery(report, options);
305
369
  const nextState: ReportState = input.awaitReporter ? "awaiting_reporter" : report.state;
306
- const jobs: DeliveryJob[] = options.notificationEnabled
307
- ? [{ version: 1, jobId: `notification:${notificationId}`, type: "deliver_notification", outboxId: notificationId }]
308
- : [];
370
+ const jobs: DeliveryJob[] = delivery.channel === "email"
371
+ ? [{ version: 1, jobId: `email:${deliveryId}`, type: "deliver_email", outboxId: deliveryId }]
372
+ : delivery.channel === "customer_webhook"
373
+ ? [{ version: 1, jobId: `notification:${deliveryId}`, type: "deliver_notification", outboxId: deliveryId }]
374
+ : [];
309
375
  const statements: D1PreparedStatement[] = [
310
376
  db.prepare(`
311
377
  INSERT INTO case_messages (
312
378
  id, report_id, audience, direction, sender_type, sender_id, body, template_version_id,
313
379
  automated, delivery_state, created_at, idempotency_key
314
380
  ) VALUES (?1, ?2, 'reporter', 'outbound', 'human', ?3, ?4, 'reporter-follow-up-v1', 0, ?5, ?6, ?7)
315
- `).bind(messageId, reportId, session.actor.id, input.body, options.notificationEnabled ? "queued" : "suppressed", createdAt, input.idempotencyKey),
381
+ `).bind(messageId, reportId, session.actor.id, input.body, delivery.state, createdAt, input.idempotencyKey),
316
382
  db.prepare(`
317
383
  INSERT INTO notices (
318
384
  id, report_id, audience, template_version_id, rendered_body, delivery_state,
319
385
  created_at, updated_at, idempotency_key, message_id
320
386
  ) VALUES (?1, ?2, 'reporter', 'reporter-follow-up-v1', ?3, ?4, ?5, ?5, ?6, ?7)
321
- `).bind(noticeId, reportId, input.body, options.notificationEnabled ? "queued" : "suppressed", createdAt, input.idempotencyKey, messageId),
387
+ `).bind(noticeId, reportId, input.body, delivery.state, createdAt, input.idempotencyKey, messageId),
322
388
  auditEvent(db, {
323
389
  idempotencyKey: `reviewer-message:${reportId}:${input.idempotencyKey}`,
324
390
  action: "report.reporter_message_created",
@@ -326,7 +392,7 @@ export async function addReviewerMessage(
326
392
  actorId: session.actor.id,
327
393
  targetType: "report",
328
394
  targetId: reportId,
329
- details: { messageId, audience: "reporter", nextState },
395
+ details: { messageId, audience: "reporter", nextState, deliveryChannel: delivery.channel, deliveryState: delivery.state },
330
396
  createdAt,
331
397
  }),
332
398
  ];
@@ -339,15 +405,22 @@ export async function addReviewerMessage(
339
405
  `).bind(crypto.randomUUID(), reportId, report.state, nextState, session.actor.type, session.actor.id, createdAt),
340
406
  );
341
407
  }
342
- if (options.notificationEnabled) {
408
+ if (delivery.channel === "email" && delivery.recipientHash) {
409
+ statements.push(db.prepare(`
410
+ INSERT INTO email_deliveries (
411
+ id, installation_id, message_id, audience, recipient_hash,
412
+ provider, state, attempt_count, next_attempt_at, created_at, updated_at
413
+ ) VALUES (?1, ?2, ?3, 'reporter', ?4, 'cloudflare_email_service', 'pending', 0, ?5, ?5, ?5)
414
+ `).bind(deliveryId, report.installation_id, messageId, delivery.recipientHash, createdAt));
415
+ } else if (delivery.channel === "customer_webhook") {
343
416
  statements.push(db.prepare(`
344
417
  INSERT INTO notification_outbox (
345
418
  id, notice_id, idempotency_key, channel, payload_json, status, next_attempt_at, created_at, updated_at
346
419
  ) VALUES (?1, ?2, ?3, 'customer_webhook', ?4, 'pending', ?5, ?5, ?5)
347
- `).bind(notificationId, noticeId, `notice:${noticeId}`, canonicalJson({
420
+ `).bind(deliveryId, noticeId, `notice:${noticeId}`, canonicalJson({
348
421
  version: 1,
349
422
  event: "report.message.available",
350
- notification_id: notificationId,
423
+ notification_id: deliveryId,
351
424
  report_reference: report.public_reference,
352
425
  ...(report.reporter_reference ? { recipient_reference: report.reporter_reference } : {}),
353
426
  audience: "reporter",
@@ -355,7 +428,7 @@ export async function addReviewerMessage(
355
428
  }), createdAt));
356
429
  }
357
430
  await db.batch(statements);
358
- return { messageId, jobs, idempotentReplay: false };
431
+ return { messageId, jobs, deliveryState: delivery.state, deliveryChannel: delivery.channel, idempotentReplay: false };
359
432
  }
360
433
 
361
434
 
@@ -416,6 +489,7 @@ export async function recordHumanDecision(
416
489
  const reporterMessageId = crypto.randomUUID();
417
490
  const reporterNoticeId = crypto.randomUUID();
418
491
  const reporterNotificationId = crypto.randomUUID();
492
+ const reporterDeliveryPlan = await reporterDelivery(report, options);
419
493
  const actionId = input.actionCode ? crypto.randomUUID() : null;
420
494
  const jobs: DeliveryJob[] = [];
421
495
  const statements: D1PreparedStatement[] = [
@@ -435,13 +509,13 @@ export async function recordHumanDecision(
435
509
  id, report_id, audience, direction, sender_type, sender_id, body, template_version_id,
436
510
  automated, delivery_state, created_at, idempotency_key
437
511
  ) VALUES (?1, ?2, 'reporter', 'outbound', 'human', ?3, ?4, 'reporter-outcome-v1', 0, ?5, ?6, ?7)
438
- `).bind(reporterMessageId, reportId, session.actor.id, input.reporterNotice, options.notificationEnabled ? "queued" : "suppressed", createdAt, `decision:${decisionId}:reporter-message`),
512
+ `).bind(reporterMessageId, reportId, session.actor.id, input.reporterNotice, reporterDeliveryPlan.state, createdAt, `decision:${decisionId}:reporter-message`),
439
513
  db.prepare(`
440
514
  INSERT INTO notices (
441
515
  id, report_id, decision_id, audience, template_version_id, rendered_body,
442
516
  delivery_state, created_at, updated_at, idempotency_key, message_id
443
517
  ) VALUES (?1, ?2, ?3, 'reporter', 'reporter-outcome-v1', ?4, ?5, ?6, ?6, ?7, ?8)
444
- `).bind(reporterNoticeId, reportId, decisionId, input.reporterNotice, options.notificationEnabled ? "queued" : "suppressed", createdAt, `decision:${decisionId}:reporter-notice`, reporterMessageId),
518
+ `).bind(reporterNoticeId, reportId, decisionId, input.reporterNotice, reporterDeliveryPlan.state, createdAt, `decision:${decisionId}:reporter-notice`, reporterMessageId),
445
519
  db.prepare(`
446
520
  UPDATE reports SET state = 'resolved_by_human', resolved_at = ?2, updated_at = ?2 WHERE id = ?1
447
521
  `).bind(reportId, createdAt),
@@ -475,7 +549,15 @@ export async function recordHumanDecision(
475
549
  `).bind(reportId, crypto.randomUUID(), createdAt),
476
550
  ];
477
551
 
478
- if (options.notificationEnabled) {
552
+ if (reporterDeliveryPlan.channel === "email" && reporterDeliveryPlan.recipientHash) {
553
+ statements.push(db.prepare(`
554
+ INSERT INTO email_deliveries (
555
+ id, installation_id, message_id, audience, recipient_hash,
556
+ provider, state, attempt_count, next_attempt_at, created_at, updated_at
557
+ ) VALUES (?1, ?2, ?3, 'reporter', ?4, 'cloudflare_email_service', 'pending', 0, ?5, ?5, ?5)
558
+ `).bind(reporterNotificationId, report.installation_id, reporterMessageId, reporterDeliveryPlan.recipientHash, createdAt));
559
+ jobs.push({ version: 1, jobId: `email:${reporterNotificationId}`, type: "deliver_email", outboxId: reporterNotificationId });
560
+ } else if (reporterDeliveryPlan.channel === "customer_webhook") {
479
561
  statements.push(db.prepare(`
480
562
  INSERT INTO notification_outbox (
481
563
  id, notice_id, idempotency_key, channel, payload_json, status, next_attempt_at, created_at, updated_at
@@ -1,6 +1,7 @@
1
1
  import { canonicalJson } from "./audit";
2
2
  import { loadWorkflowExecutionContext } from "./component-executor";
3
3
  import { decryptReportContact, sha256 } from "./report-crypto";
4
+ import { reporterEmailBinding, reporterEmailFrom } from "./report-email";
4
5
  import type { DeliveryJob } from "./report-types";
5
6
  import { evaluateInputMapping } from "./workflow-expressions";
6
7
  import type { JsonObject, JsonValue, WorkflowNodeV1 } from "./workflow-platform-types";
@@ -96,11 +97,11 @@ export async function sendWorkflowMessage(
96
97
  const deliveryChannels = Array.isArray(input.node.config.delivery_channels)
97
98
  ? input.node.config.delivery_channels.filter((entry): entry is string => typeof entry === "string")
98
99
  : audience === "internal" ? [] : ["customer_webhook", "email"];
99
- const emailBinding = (env as unknown as { EMAIL?: { send(message: Record<string, unknown>): Promise<{ messageId?: string }> } }).EMAIL;
100
+ const emailBinding = reporterEmailBinding(env);
100
101
  const emailAddress = participant?.contactCiphertext
101
102
  ? (await decryptReportContact(env.CREDENTIAL_ENCRYPTION_KEY, participant.contactCiphertext, run.reportId, audience as "reporter" | "affected_user")).trim().toLowerCase()
102
103
  : "";
103
- const emailFrom = env.EMAIL_FROM_ADDRESS?.trim().toLowerCase() ?? "";
104
+ const emailFrom = reporterEmailFrom(env);
104
105
  const emailEnabled = Boolean(
105
106
  audience !== "internal" && deliveryChannels.includes("email")
106
107
  && participant?.contactMode === "customer_notification"
@@ -187,7 +188,7 @@ export async function sendWorkflowMessage(
187
188
  deliveryChannels, emailEnabled, notificationEnabled,
188
189
  }), timestamp));
189
190
  await env.DB.batch(statements);
190
- if (job) await env.REPORT_JOBS.send(job, { contentType: "json" }).catch(() => {});
191
+ if (job) await env.DELIVERY_JOBS.send(job, { contentType: "json" }).catch(() => {});
191
192
  return { port: "sent", messageId, deliveryState: audience === "internal" ? "not_required" : externalDeliveryEnabled ? "queued" : "suppressed", idempotentReplay: false };
192
193
  }
193
194
 
@@ -12,6 +12,8 @@ interface __BaseEnv_Env {
12
12
  LOADER: WorkerLoader;
13
13
  AI: Ai;
14
14
  ASSETS: Fetcher;
15
+ AUTH_EMAIL: SendEmail;
16
+ REPORTER_EMAIL: SendEmail;
15
17
  PUBLIC_BASE_URL: string;
16
18
  ALLOWED_ORIGINS: string;
17
19
  CONTEXT_TOKEN_TTL_SECONDS: string;
@@ -25,8 +27,8 @@ interface __BaseEnv_Env {
25
27
  PUBLIC_REPORT_LIMIT_PER_10_MINUTES: string;
26
28
  PUBLIC_REPORT_SURGE_PER_5_MINUTES: string;
27
29
  OPERATOR_NOTIFICATION_WEBHOOK_URL: string;
28
- EMAIL_FROM_ADDRESS: string;
29
- EMAIL_SUBJECT_PREFIX: string;
30
+ REPORTER_EMAIL_FROM: string;
31
+ REPORTER_EMAIL_SUBJECT_PREFIX: string;
30
32
  ADMIN_API_KEY: string;
31
33
  BETTER_AUTH_SECRET: string;
32
34
  AUTH_EMAIL_FROM: string;
@@ -59,5 +61,5 @@ type StringifyValues<EnvType extends Record<string, unknown>> = {
59
61
  [Binding in keyof EnvType]: EnvType[Binding] extends string ? EnvType[Binding] : string;
60
62
  };
61
63
  declare namespace NodeJS {
62
- interface ProcessEnv extends StringifyValues<Pick<Cloudflare.Env, "PUBLIC_BASE_URL" | "ALLOWED_ORIGINS" | "CONTEXT_TOKEN_TTL_SECONDS" | "CLAIM_TTL_MINUTES" | "REPORT_RETENTION_DAYS" | "MESSAGE_RETENTION_DAYS" | "APPEAL_WINDOW_DAYS" | "ALLOWED_ACTION_CODES" | "OPERATIONS_DLQ_NAME" | "PUBLIC_INTEGRATION_ID" | "PUBLIC_REPORT_LIMIT_PER_10_MINUTES" | "PUBLIC_REPORT_SURGE_PER_5_MINUTES" | "OPERATOR_NOTIFICATION_WEBHOOK_URL" | "EMAIL_FROM_ADDRESS" | "EMAIL_SUBJECT_PREFIX" | "ADMIN_API_KEY" | "BETTER_AUTH_SECRET" | "AUTH_EMAIL_FROM" | "CONTEXT_SIGNING_SECRET" | "CREDENTIAL_ENCRYPTION_KEY" | "RATE_LIMIT_PEPPER" | "INTEGRATION_API_KEYS_JSON" | "TURNSTILE_SITE_KEY" | "TURNSTILE_SECRET_KEY" | "ACTION_WEBHOOK_URL" | "ACTION_WEBHOOK_SECRET" | "NOTIFICATION_WEBHOOK_URL" | "NOTIFICATION_WEBHOOK_SECRET" | "OPERATOR_NOTIFICATION_WEBHOOK_SECRET" | "EXAMPLE_ENRICHMENT_SECRET" | "AI_LOCAL_FIXTURE_ENABLED" | "AI_LOCAL_FIXTURE_JSON">> {}
64
+ interface ProcessEnv extends StringifyValues<Pick<Cloudflare.Env, "PUBLIC_BASE_URL" | "ALLOWED_ORIGINS" | "CONTEXT_TOKEN_TTL_SECONDS" | "CLAIM_TTL_MINUTES" | "REPORT_RETENTION_DAYS" | "MESSAGE_RETENTION_DAYS" | "APPEAL_WINDOW_DAYS" | "ALLOWED_ACTION_CODES" | "OPERATIONS_DLQ_NAME" | "PUBLIC_INTEGRATION_ID" | "PUBLIC_REPORT_LIMIT_PER_10_MINUTES" | "PUBLIC_REPORT_SURGE_PER_5_MINUTES" | "OPERATOR_NOTIFICATION_WEBHOOK_URL" | "REPORTER_EMAIL_FROM" | "REPORTER_EMAIL_SUBJECT_PREFIX" | "ADMIN_API_KEY" | "BETTER_AUTH_SECRET" | "AUTH_EMAIL_FROM" | "CONTEXT_SIGNING_SECRET" | "CREDENTIAL_ENCRYPTION_KEY" | "RATE_LIMIT_PEPPER" | "INTEGRATION_API_KEYS_JSON" | "TURNSTILE_SITE_KEY" | "TURNSTILE_SECRET_KEY" | "ACTION_WEBHOOK_URL" | "ACTION_WEBHOOK_SECRET" | "NOTIFICATION_WEBHOOK_URL" | "NOTIFICATION_WEBHOOK_SECRET" | "OPERATOR_NOTIFICATION_WEBHOOK_SECRET" | "EXAMPLE_ENRICHMENT_SECRET" | "AI_LOCAL_FIXTURE_ENABLED" | "AI_LOCAL_FIXTURE_JSON">> {}
63
65
  }