create-safest-tools 0.4.0 → 0.4.2

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,6 @@
1
1
  import { AUDIT_GENESIS_HASH, canonicalJson, hashAuditEntry } from "./audit";
2
+ import { decryptReportContact } from "./report-crypto";
3
+ import { selectParticipantDelivery, type ParticipantDelivery } from "./report-email";
2
4
  import { ApiError } from "./report-http";
3
5
  import { analystAvatarColor } from "./report-presence";
4
6
  import type { DeliveryJob, OperatorSession, ParticipantAudience, ReportState } from "./report-types";
@@ -6,6 +8,7 @@ import type { AppealDecisionInput, DecisionInput, ReviewerMessageInput } from ".
6
8
 
7
9
  interface WorkflowReportRow {
8
10
  id: string;
11
+ installation_id: string;
9
12
  public_reference: string;
10
13
  state: ReportState;
11
14
  queue_id: string | null;
@@ -14,6 +17,8 @@ interface WorkflowReportRow {
14
17
  target_reference: string;
15
18
  owner_reference: string | null;
16
19
  reporter_reference: string | null;
20
+ reporter_contact_mode: string;
21
+ reporter_contact_ciphertext: string | null;
17
22
  }
18
23
 
19
24
  interface ClaimRow {
@@ -62,8 +67,9 @@ function auditEvent(
62
67
 
63
68
  async function workflowReport(db: D1Database, reportId: string): Promise<WorkflowReportRow | null> {
64
69
  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
70
+ SELECT r.id, r.installation_id, r.public_reference, r.state, r.queue_id, r.queue_version_id, r.policy_version_id,
71
+ t.target_reference, t.owner_reference, p.reference AS reporter_reference,
72
+ p.contact_mode AS reporter_contact_mode, p.contact_ciphertext AS reporter_contact_ciphertext
67
73
  FROM reports r
68
74
  JOIN report_targets t ON t.report_id = r.id
69
75
  JOIN report_participants p ON p.report_id = r.id AND p.audience = 'reporter'
@@ -155,21 +161,30 @@ export async function releaseReportClaim(db: D1Database, reportId: string, sessi
155
161
  ]);
156
162
  }
157
163
 
158
- export async function loadReportDetail(db: D1Database, reportId: string): Promise<Record<string, unknown> | null> {
164
+ export async function loadReportDetail(
165
+ db: D1Database,
166
+ reportId: string,
167
+ options: { contactEncryptionKey?: string; includeReporterEmail?: boolean } = {},
168
+ ): Promise<Record<string, unknown> | null> {
159
169
  const report = await db.prepare(`
160
170
  SELECT r.id, r.public_reference AS reference, r.source, r.state, r.priority, r.reason_code AS reasonCode,
161
171
  r.locale, r.queue_id AS queueId, r.submitted_at AS submittedAt, r.received_at AS receivedAt,
162
172
  r.updated_at AS updatedAt, r.due_at AS dueAt, r.resolved_at AS resolvedAt,
173
+ r.form_version_id AS formVersionId, form.title AS formTitle,
174
+ form.fields_json AS formFieldsJson, form.consent_notice AS consentNotice,
163
175
  r.policy_version_id AS policyVersionId, policy.title AS policyTitle,
164
176
  policy.rules_json AS policyRulesJson, r.queue_version_id AS queuePolicyVersionId,
165
177
  qv.management_mode AS queueManagementMode, qv.reviewer_instructions AS reviewerInstructions,
166
178
  t.target_type AS targetType, t.target_reference AS targetReference,
167
179
  t.owner_reference AS ownerReference, t.customer_url AS customerUrl,
168
- p.reference AS reporterReference
180
+ p.reference AS reporterReference, p.contact_mode AS reporterContactMode,
181
+ p.contact_ciphertext AS reporterContactCiphertext,
182
+ p.reference_redacted_at AS reporterReferenceRedactedAt
169
183
  FROM reports r
170
184
  JOIN report_targets t ON t.report_id = r.id
171
185
  JOIN report_participants p ON p.report_id = r.id AND p.audience = 'reporter'
172
186
  LEFT JOIN policy_versions policy ON policy.id = r.policy_version_id
187
+ LEFT JOIN report_form_versions form ON form.id = r.form_version_id
173
188
  LEFT JOIN queue_versions qv ON qv.id = r.queue_version_id
174
189
  WHERE r.id = ?1
175
190
  LIMIT 1
@@ -227,8 +242,66 @@ export async function loadReportDetail(db: D1Database, reportId: string): Promis
227
242
  LIMIT 500
228
243
  `).bind(reportId).all<Record<string, unknown>>(),
229
244
  ]);
245
+ const formFieldsValue = parsedJson(report.formFieldsJson, []);
246
+ const formFields = Array.isArray(formFieldsValue)
247
+ ? formFieldsValue.filter((field): field is Record<string, unknown> => Boolean(field) && typeof field === "object" && !Array.isArray(field))
248
+ : [];
249
+ const answerRows = new Map(answers.results.map((row) => [String(row.fieldKey), row]));
250
+ const projectedAnswers = formFields.flatMap((field) => {
251
+ const fieldKey = typeof field.key === "string" ? field.key : "";
252
+ if (!fieldKey) return [];
253
+ const row = answerRows.get(fieldKey);
254
+ answerRows.delete(fieldKey);
255
+ return [{
256
+ fieldKey,
257
+ label: typeof field.label === "string" ? field.label : fieldKey,
258
+ type: typeof field.type === "string" ? field.type : "unknown",
259
+ required: field.required === true,
260
+ options: Array.isArray(field.options) ? field.options : [],
261
+ answered: Boolean(row),
262
+ value: row ? parsedJson(row.valueJson, null) : null,
263
+ }];
264
+ });
265
+ for (const [fieldKey, row] of answerRows) {
266
+ projectedAnswers.push({
267
+ fieldKey,
268
+ label: fieldKey,
269
+ type: "unknown",
270
+ required: false,
271
+ options: [],
272
+ answered: true,
273
+ value: parsedJson(row.valueJson, null),
274
+ });
275
+ }
276
+ let reporterEmail: string | null = null;
277
+ let reporterContactStatus: "available" | "not_provided" | "redacted" | "restricted" | "unavailable";
278
+ if (report.reporterReferenceRedactedAt) {
279
+ reporterContactStatus = "redacted";
280
+ } else if (!report.reporterContactCiphertext) {
281
+ reporterContactStatus = "not_provided";
282
+ } else if (!options.includeReporterEmail) {
283
+ reporterContactStatus = "restricted";
284
+ } else if (!options.contactEncryptionKey) {
285
+ reporterContactStatus = "unavailable";
286
+ } else {
287
+ try {
288
+ reporterEmail = (await decryptReportContact(
289
+ options.contactEncryptionKey,
290
+ String(report.reporterContactCiphertext),
291
+ reportId,
292
+ "reporter",
293
+ )).trim().toLowerCase();
294
+ reporterContactStatus = reporterEmail ? "available" : "unavailable";
295
+ } catch {
296
+ reporterContactStatus = "unavailable";
297
+ }
298
+ }
230
299
  return {
231
300
  ...report,
301
+ reporterEmail,
302
+ reporterContactStatus,
303
+ reporterContactCiphertext: undefined,
304
+ reporterReferenceRedactedAt: undefined,
232
305
  policyRules: (() => {
233
306
  try {
234
307
  const value: unknown = JSON.parse(String(report.policyRulesJson ?? "[]"));
@@ -236,7 +309,8 @@ export async function loadReportDetail(db: D1Database, reportId: string): Promis
236
309
  } catch { return []; }
237
310
  })(),
238
311
  policyRulesJson: undefined,
239
- answers: answers.results.map((row) => ({ fieldKey: row.fieldKey, value: JSON.parse(String(row.valueJson)) })),
312
+ formFieldsJson: undefined,
313
+ answers: projectedAnswers,
240
314
  evidence: evidence.results,
241
315
  messages: messages.results.map((row) => ({ ...row, automated: row.automated === 1 })),
242
316
  decisions: decisions.results.map((row) => ({
@@ -278,20 +352,43 @@ export async function loadReportDetail(db: D1Database, reportId: string): Promis
278
352
  }
279
353
 
280
354
  export interface WorkflowDeliveryOptions {
355
+ emailEnabled: boolean;
356
+ contactEncryptionKey: string;
357
+ recipientHashPepper: string;
281
358
  notificationEnabled: boolean;
282
359
  }
283
360
 
361
+ async function reporterDelivery(
362
+ report: WorkflowReportRow,
363
+ options: WorkflowDeliveryOptions,
364
+ ): Promise<ParticipantDelivery> {
365
+ return selectParticipantDelivery({
366
+ reportId: report.id,
367
+ audience: "reporter",
368
+ contactMode: report.reporter_contact_mode,
369
+ contactCiphertext: report.reporter_contact_ciphertext,
370
+ reference: report.reporter_reference,
371
+ ...options,
372
+ });
373
+ }
374
+
284
375
  export async function addReviewerMessage(
285
376
  db: D1Database,
286
377
  reportId: string,
287
378
  session: OperatorSession,
288
379
  input: ReviewerMessageInput,
289
380
  options: WorkflowDeliveryOptions,
290
- ): Promise<{ messageId: string; jobs: DeliveryJob[]; idempotentReplay: boolean }> {
381
+ ): Promise<{ messageId: string; jobs: DeliveryJob[]; deliveryState: string; deliveryChannel: ParticipantDelivery["channel"]; idempotentReplay: boolean }> {
291
382
  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 };
383
+ SELECT m.id, m.delivery_state AS deliveryState,
384
+ CASE
385
+ WHEN EXISTS (SELECT 1 FROM email_deliveries e WHERE e.message_id = m.id) THEN 'email'
386
+ 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'
387
+ ELSE NULL
388
+ END AS deliveryChannel
389
+ FROM case_messages m WHERE m.report_id = ?1 AND m.idempotency_key = ?2 LIMIT 1
390
+ `).bind(reportId, input.idempotencyKey).first<{ id: string; deliveryState: string; deliveryChannel: ParticipantDelivery["channel"] }>();
391
+ if (existing) return { messageId: existing.id, jobs: [], deliveryState: existing.deliveryState, deliveryChannel: existing.deliveryChannel, idempotentReplay: true };
295
392
  const createdAt = now();
296
393
  await requireActiveClaim(db, reportId, session.actor.id, createdAt);
297
394
  const report = await workflowReport(db, reportId);
@@ -301,24 +398,27 @@ export async function addReviewerMessage(
301
398
  }
302
399
  const messageId = crypto.randomUUID();
303
400
  const noticeId = crypto.randomUUID();
304
- const notificationId = crypto.randomUUID();
401
+ const deliveryId = crypto.randomUUID();
402
+ const delivery = await reporterDelivery(report, options);
305
403
  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
- : [];
404
+ const jobs: DeliveryJob[] = delivery.channel === "email"
405
+ ? [{ version: 1, jobId: `email:${deliveryId}`, type: "deliver_email", outboxId: deliveryId }]
406
+ : delivery.channel === "customer_webhook"
407
+ ? [{ version: 1, jobId: `notification:${deliveryId}`, type: "deliver_notification", outboxId: deliveryId }]
408
+ : [];
309
409
  const statements: D1PreparedStatement[] = [
310
410
  db.prepare(`
311
411
  INSERT INTO case_messages (
312
412
  id, report_id, audience, direction, sender_type, sender_id, body, template_version_id,
313
413
  automated, delivery_state, created_at, idempotency_key
314
414
  ) 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),
415
+ `).bind(messageId, reportId, session.actor.id, input.body, delivery.state, createdAt, input.idempotencyKey),
316
416
  db.prepare(`
317
417
  INSERT INTO notices (
318
418
  id, report_id, audience, template_version_id, rendered_body, delivery_state,
319
419
  created_at, updated_at, idempotency_key, message_id
320
420
  ) 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),
421
+ `).bind(noticeId, reportId, input.body, delivery.state, createdAt, input.idempotencyKey, messageId),
322
422
  auditEvent(db, {
323
423
  idempotencyKey: `reviewer-message:${reportId}:${input.idempotencyKey}`,
324
424
  action: "report.reporter_message_created",
@@ -326,7 +426,7 @@ export async function addReviewerMessage(
326
426
  actorId: session.actor.id,
327
427
  targetType: "report",
328
428
  targetId: reportId,
329
- details: { messageId, audience: "reporter", nextState },
429
+ details: { messageId, audience: "reporter", nextState, deliveryChannel: delivery.channel, deliveryState: delivery.state },
330
430
  createdAt,
331
431
  }),
332
432
  ];
@@ -339,15 +439,22 @@ export async function addReviewerMessage(
339
439
  `).bind(crypto.randomUUID(), reportId, report.state, nextState, session.actor.type, session.actor.id, createdAt),
340
440
  );
341
441
  }
342
- if (options.notificationEnabled) {
442
+ if (delivery.channel === "email" && delivery.recipientHash) {
443
+ statements.push(db.prepare(`
444
+ INSERT INTO email_deliveries (
445
+ id, installation_id, message_id, audience, recipient_hash,
446
+ provider, state, attempt_count, next_attempt_at, created_at, updated_at
447
+ ) VALUES (?1, ?2, ?3, 'reporter', ?4, 'cloudflare_email_service', 'pending', 0, ?5, ?5, ?5)
448
+ `).bind(deliveryId, report.installation_id, messageId, delivery.recipientHash, createdAt));
449
+ } else if (delivery.channel === "customer_webhook") {
343
450
  statements.push(db.prepare(`
344
451
  INSERT INTO notification_outbox (
345
452
  id, notice_id, idempotency_key, channel, payload_json, status, next_attempt_at, created_at, updated_at
346
453
  ) VALUES (?1, ?2, ?3, 'customer_webhook', ?4, 'pending', ?5, ?5, ?5)
347
- `).bind(notificationId, noticeId, `notice:${noticeId}`, canonicalJson({
454
+ `).bind(deliveryId, noticeId, `notice:${noticeId}`, canonicalJson({
348
455
  version: 1,
349
456
  event: "report.message.available",
350
- notification_id: notificationId,
457
+ notification_id: deliveryId,
351
458
  report_reference: report.public_reference,
352
459
  ...(report.reporter_reference ? { recipient_reference: report.reporter_reference } : {}),
353
460
  audience: "reporter",
@@ -355,7 +462,7 @@ export async function addReviewerMessage(
355
462
  }), createdAt));
356
463
  }
357
464
  await db.batch(statements);
358
- return { messageId, jobs, idempotentReplay: false };
465
+ return { messageId, jobs, deliveryState: delivery.state, deliveryChannel: delivery.channel, idempotentReplay: false };
359
466
  }
360
467
 
361
468
 
@@ -416,6 +523,7 @@ export async function recordHumanDecision(
416
523
  const reporterMessageId = crypto.randomUUID();
417
524
  const reporterNoticeId = crypto.randomUUID();
418
525
  const reporterNotificationId = crypto.randomUUID();
526
+ const reporterDeliveryPlan = await reporterDelivery(report, options);
419
527
  const actionId = input.actionCode ? crypto.randomUUID() : null;
420
528
  const jobs: DeliveryJob[] = [];
421
529
  const statements: D1PreparedStatement[] = [
@@ -435,13 +543,13 @@ export async function recordHumanDecision(
435
543
  id, report_id, audience, direction, sender_type, sender_id, body, template_version_id,
436
544
  automated, delivery_state, created_at, idempotency_key
437
545
  ) 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`),
546
+ `).bind(reporterMessageId, reportId, session.actor.id, input.reporterNotice, reporterDeliveryPlan.state, createdAt, `decision:${decisionId}:reporter-message`),
439
547
  db.prepare(`
440
548
  INSERT INTO notices (
441
549
  id, report_id, decision_id, audience, template_version_id, rendered_body,
442
550
  delivery_state, created_at, updated_at, idempotency_key, message_id
443
551
  ) 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),
552
+ `).bind(reporterNoticeId, reportId, decisionId, input.reporterNotice, reporterDeliveryPlan.state, createdAt, `decision:${decisionId}:reporter-notice`, reporterMessageId),
445
553
  db.prepare(`
446
554
  UPDATE reports SET state = 'resolved_by_human', resolved_at = ?2, updated_at = ?2 WHERE id = ?1
447
555
  `).bind(reportId, createdAt),
@@ -475,7 +583,15 @@ export async function recordHumanDecision(
475
583
  `).bind(reportId, crypto.randomUUID(), createdAt),
476
584
  ];
477
585
 
478
- if (options.notificationEnabled) {
586
+ if (reporterDeliveryPlan.channel === "email" && reporterDeliveryPlan.recipientHash) {
587
+ statements.push(db.prepare(`
588
+ INSERT INTO email_deliveries (
589
+ id, installation_id, message_id, audience, recipient_hash,
590
+ provider, state, attempt_count, next_attempt_at, created_at, updated_at
591
+ ) VALUES (?1, ?2, ?3, 'reporter', ?4, 'cloudflare_email_service', 'pending', 0, ?5, ?5, ?5)
592
+ `).bind(reporterNotificationId, report.installation_id, reporterMessageId, reporterDeliveryPlan.recipientHash, createdAt));
593
+ jobs.push({ version: 1, jobId: `email:${reporterNotificationId}`, type: "deliver_email", outboxId: reporterNotificationId });
594
+ } else if (reporterDeliveryPlan.channel === "customer_webhook") {
479
595
  statements.push(db.prepare(`
480
596
  INSERT INTO notification_outbox (
481
597
  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
 
@@ -1,11 +1,13 @@
1
1
  /* eslint-disable */
2
- // Generated by Wrangler by running `wrangler types --env-file=config/dev.vars.example --include-runtime=false --strict-vars=false` (hash: 2cca359aa7fa3dcf738a197c47478066)
2
+ // Generated by Wrangler by running `wrangler types --env-file=config/dev.vars.example --include-runtime=false --strict-vars=false` (hash: 2d2c98e01480f3c1492835530279c5f1)
3
3
  interface __BaseEnv_Env {
4
4
  PROFILE_MEDIA: R2Bucket;
5
5
  WORKFLOW_ARTIFACTS: R2Bucket;
6
6
  EVIDENCE_OBJECTS: R2Bucket;
7
7
  EXPORTS: R2Bucket;
8
8
  DB: D1Database;
9
+ AUTH_EMAIL: SendEmail;
10
+ REPORTER_EMAIL: SendEmail;
9
11
  CONTROL_JOBS: Queue;
10
12
  REPORT_JOBS: Queue;
11
13
  DELIVERY_JOBS: Queue;
@@ -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
  }