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.
- package/README.md +2 -2
- package/package.json +1 -1
- package/src/cli.mjs +8 -6
- package/src/config.mjs +26 -5
- package/src/update.mjs +32 -2
- package/template/README.md +3 -3
- package/template/console/reports/ReportDrawer.tsx +60 -6
- package/template/console/reports/controller.ts +11 -5
- package/template/console/reports/store.ts +1 -1
- package/template/console/reports/types.ts +26 -0
- package/template/package.json +2 -2
- package/template/public/console/auth-shell.js +4360 -4257
- package/template/public/styles.css +17 -2
- package/template/reports.config.example.json +2 -1
- package/template/reports.schema.json +2 -1
- package/template/scripts/reports-auth-onboarding.mjs +6 -5
- package/template/scripts/reports-plan.mjs +13 -4
- package/template/src/index.ts +20 -5
- package/template/src/installation-admin.ts +2 -1
- package/template/src/report-ai.ts +30 -5
- package/template/src/report-better-auth.ts +5 -14
- package/template/src/report-delivery.ts +4 -3
- package/template/src/report-email.ts +106 -0
- package/template/src/report-workflow.ts +139 -23
- package/template/src/workflow-effects.ts +4 -3
- package/template/worker-configuration.d.ts +6 -4
|
@@ -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(
|
|
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
|
-
|
|
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
|
|
293
|
-
|
|
294
|
-
|
|
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
|
|
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[] =
|
|
307
|
-
? [{ version: 1, jobId: `
|
|
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,
|
|
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,
|
|
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 (
|
|
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(
|
|
454
|
+
`).bind(deliveryId, noticeId, `notice:${noticeId}`, canonicalJson({
|
|
348
455
|
version: 1,
|
|
349
456
|
event: "report.message.available",
|
|
350
|
-
notification_id:
|
|
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,
|
|
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,
|
|
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 (
|
|
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
|
|
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
|
|
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.
|
|
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:
|
|
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
|
-
|
|
29
|
-
|
|
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" | "
|
|
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
|
}
|