create-safest-tools 0.6.0 → 0.7.0

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 (172) hide show
  1. package/README.md +3 -3
  2. package/package.json +2 -2
  3. package/src/config.mjs +2 -2
  4. package/src/update.mjs +10 -1
  5. package/template/README.md +2 -2
  6. package/template/console/README.md +2 -2
  7. package/template/console/analytics/AnalyticsWorkspace.tsx +49 -36
  8. package/template/console/analytics/controller.ts +10 -2
  9. package/template/console/analytics/types.ts +7 -0
  10. package/template/console/appeals/AppealFollowup.tsx +171 -0
  11. package/template/console/appeals/AppealWorkspace.tsx +33 -4
  12. package/template/console/appeals/RecordAppealDialog.tsx +42 -0
  13. package/template/console/appeals/appeal-corrections.css +29 -0
  14. package/template/console/appeals/controller.ts +40 -14
  15. package/template/console/appeals/events.ts +1 -0
  16. package/template/console/appeals/types.ts +6 -0
  17. package/template/console/appeals/useAppealDraftGuard.ts +12 -0
  18. package/template/console/assistant/AssistantWorkspace.tsx +28 -5
  19. package/template/console/command/CommandCentre.tsx +101 -64
  20. package/template/console/command/controller.ts +80 -70
  21. package/template/console/command/events.ts +1 -2
  22. package/template/console/command/presentation.ts +29 -0
  23. package/template/console/command/store.ts +4 -4
  24. package/template/console/command/types.ts +26 -21
  25. package/template/console/configuration/ConfigurationBuilders.tsx +101 -0
  26. package/template/console/configuration/ConfigurationDialog.tsx +28 -13
  27. package/template/console/configuration/ConfigurationWorkspace.tsx +21 -8
  28. package/template/console/configuration/types.ts +1 -1
  29. package/template/console/lib/priority.ts +13 -0
  30. package/template/console/operations/CaseEscalations.tsx +142 -0
  31. package/template/console/operations/OperationsWorkspace.tsx +2 -0
  32. package/template/console/operations/case-escalations.css +20 -0
  33. package/template/console/people/PeopleWorkspace.tsx +8 -4
  34. package/template/console/people/PersonAccessDialog.tsx +40 -0
  35. package/template/console/people/types.ts +1 -0
  36. package/template/console/profile/ProfileWorkspace.tsx +1 -1
  37. package/template/console/registry/IntegrationSetup.tsx +41 -0
  38. package/template/console/registry/IntegrationVerification.tsx +54 -0
  39. package/template/console/registry/RegistryDetailsDialog.tsx +2 -2
  40. package/template/console/registry/RegistryDialog.tsx +56 -27
  41. package/template/console/registry/RegistryWorkspace.tsx +10 -5
  42. package/template/console/registry/integration-verification.css +25 -0
  43. package/template/console/reports/CaseActions.tsx +81 -0
  44. package/template/console/reports/CaseContinuity.tsx +27 -0
  45. package/template/console/reports/CaseNotices.tsx +48 -0
  46. package/template/console/reports/CaseOwnership.tsx +78 -0
  47. package/template/console/reports/CaseReviewTasks.tsx +148 -0
  48. package/template/console/reports/EnrichmentFindings.tsx +148 -0
  49. package/template/console/reports/InvestigationBrief.tsx +29 -0
  50. package/template/console/reports/ReportDrawer.tsx +122 -67
  51. package/template/console/reports/ReportWorkspace.tsx +112 -23
  52. package/template/console/reports/controller.ts +125 -60
  53. package/template/console/reports/events.ts +9 -2
  54. package/template/console/reports/load-inbox.ts +17 -0
  55. package/template/console/reports/presence-presentation.ts +52 -0
  56. package/template/console/reports/private-drafts.ts +32 -0
  57. package/template/console/reports/store.ts +2 -2
  58. package/template/console/reports/types.ts +50 -2
  59. package/template/console/reports/use-editing-access.ts +13 -0
  60. package/template/console/settings/SettingsWorkspace.tsx +1 -1
  61. package/template/console/shell/WorkspaceShell.tsx +55 -29
  62. package/template/console/shell/WorkspaceUpdateNotice.tsx +48 -0
  63. package/template/console/shell/controller.ts +23 -16
  64. package/template/console/shell/navigation.ts +30 -39
  65. package/template/console/workflows/CreatePlaybookDialog.tsx +73 -0
  66. package/template/console/workflows/PlaybookExamples.tsx +160 -0
  67. package/template/console/workflows/PlaybookFields.tsx +109 -0
  68. package/template/console/workflows/PlaybookGuideEditor.tsx +125 -0
  69. package/template/console/workflows/PlaybookHistory.tsx +106 -0
  70. package/template/console/workflows/WorkflowDialogs.tsx +2 -1
  71. package/template/console/workflows/WorkflowStudio.tsx +108 -39
  72. package/template/console/workflows/WorkflowWorkspace.tsx +9 -7
  73. package/template/console/workflows/templates.ts +3 -3
  74. package/template/console/workflows/types.ts +11 -0
  75. package/template/migrations/0001_safest_resolve_baseline.sql +0 -1
  76. package/template/migrations/0002_playbook_publication_evidence.sql +36 -0
  77. package/template/migrations/0003_action_confirmation.sql +24 -0
  78. package/template/migrations/0004_notice_recovery.sql +22 -0
  79. package/template/migrations/0005_appeal_continuity.sql +17 -0
  80. package/template/migrations/0006_case_ownership.sql +23 -0
  81. package/template/migrations/0007_integration_verification.sql +21 -0
  82. package/template/migrations/0008_case_escalations.sql +135 -0
  83. package/template/migrations/0009_participant_continuation.sql +61 -0
  84. package/template/migrations/0010_appeal_corrections.sql +41 -0
  85. package/template/npm-shrinkwrap.json +8036 -0
  86. package/template/package.json +31 -22
  87. package/template/public/_headers +16 -1
  88. package/template/public/console/auth-shell.js +22151 -14847
  89. package/template/public/console/release.json +1 -0
  90. package/template/public/console/workspace-features.css +2 -0
  91. package/template/public/embed/embed.css +3 -0
  92. package/template/public/embed/embed.js +19 -1
  93. package/template/public/embed/index.html +2 -0
  94. package/template/public/index.html +9 -6
  95. package/template/public/participant.css +2 -0
  96. package/template/public/participant.html +13 -0
  97. package/template/public/participant.js +111 -0
  98. package/template/public/public-report.js +21 -1
  99. package/template/public/report/index.html +1 -1
  100. package/template/public/styles.css +599 -83
  101. package/template/public/workspace.css +124 -0
  102. package/template/scripts/reports-plan.mjs +1 -1
  103. package/template/shared/case-escalations.ts +31 -0
  104. package/template/shared/integration-progress.ts +27 -0
  105. package/template/shared/notice-inputs.ts +3 -0
  106. package/template/shared/playbook-resources.ts +14 -0
  107. package/template/shared/playbooks.ts +249 -0
  108. package/template/src/agent-executor.ts +25 -8
  109. package/template/src/ai-observability.ts +14 -13
  110. package/template/src/ai-registry-validation.ts +1 -1
  111. package/template/src/appeal-continuity.ts +75 -0
  112. package/template/src/appeal-correction-state.ts +46 -0
  113. package/template/src/appeal-corrections.ts +112 -0
  114. package/template/src/appeal-decisions.ts +85 -0
  115. package/template/src/assistant.ts +3 -4
  116. package/template/src/case-escalations.ts +223 -0
  117. package/template/src/case-investigator.ts +179 -0
  118. package/template/src/component-executor.ts +5 -4
  119. package/template/src/configuration-registry.ts +4 -1
  120. package/template/src/connection-oauth.ts +12 -3
  121. package/template/src/database-readiness.ts +14 -0
  122. package/template/src/human-tasks.ts +152 -55
  123. package/template/src/index.ts +191 -29
  124. package/template/src/installation-admin.ts +2 -1
  125. package/template/src/integration-progress.ts +45 -0
  126. package/template/src/integration-verification.ts +249 -0
  127. package/template/src/investigation-evidence.ts +95 -0
  128. package/template/src/investigator-fixture.ts +30 -0
  129. package/template/src/notice-delivery.ts +19 -0
  130. package/template/src/notice-recovery.ts +81 -0
  131. package/template/src/operator-scope.ts +31 -0
  132. package/template/src/participant-access.ts +163 -0
  133. package/template/src/participant-case.ts +87 -0
  134. package/template/src/participant-customer-access.ts +93 -0
  135. package/template/src/participant-recovery.ts +43 -0
  136. package/template/src/participant-routes.ts +67 -0
  137. package/template/src/platform-registry-validation.ts +25 -2
  138. package/template/src/platform-registry.ts +10 -7
  139. package/template/src/playbook-resources.ts +58 -0
  140. package/template/src/report-admin.ts +9 -2
  141. package/template/src/report-auth.ts +1 -28
  142. package/template/src/report-better-auth.ts +18 -6
  143. package/template/src/report-delivery.ts +149 -148
  144. package/template/src/report-email-inbound.ts +33 -22
  145. package/template/src/report-email-template.ts +5 -2
  146. package/template/src/report-email.ts +7 -30
  147. package/template/src/report-governance.ts +42 -45
  148. package/template/src/report-http.ts +37 -2
  149. package/template/src/report-operations.ts +30 -67
  150. package/template/src/report-operator-accounts.ts +7 -6
  151. package/template/src/report-ownership.ts +64 -0
  152. package/template/src/report-presence.ts +15 -9
  153. package/template/src/report-repository.ts +16 -7
  154. package/template/src/report-turnstile.ts +7 -3
  155. package/template/src/report-types.ts +1 -0
  156. package/template/src/report-workflow.ts +183 -249
  157. package/template/src/workflow-actions.ts +248 -156
  158. package/template/src/workflow-compiler.ts +63 -12
  159. package/template/src/workflow-dynamic-runtime.ts +20 -9
  160. package/template/src/workflow-effects.ts +117 -39
  161. package/template/src/workflow-event-transport.ts +16 -0
  162. package/template/src/workflow-expressions.ts +30 -0
  163. package/template/src/workflow-loader-code.ts +21 -0
  164. package/template/src/workflow-platform-types.ts +3 -0
  165. package/template/src/workflow-platform-validation.ts +23 -1
  166. package/template/src/workflow-repository.ts +131 -28
  167. package/template/src/workflow-runs.ts +61 -13
  168. package/template/src/workflow-simulator.ts +7 -4
  169. package/template/src/workspace-governance.ts +6 -4
  170. package/template/tsconfig.json +1 -0
  171. package/template/vite.console.config.ts +30 -2
  172. package/template/worker-configuration.d.ts +3 -2
@@ -1,4 +1,16 @@
1
+ import { changeReportOwner, eligibleReportOwners } from "./report-ownership";
2
+ import { retryParticipantNotice } from "./notice-recovery";
3
+ import { confirmAppealCorrection, reconcileAppealCompletion } from "./appeal-continuity";
4
+ import { createAppealCorrection, loadAppealCorrections, reassignAppealCorrection, retryAppealCorrection } from "./appeal-corrections";
5
+ import { loadIntegrationProgress } from "./integration-progress";
6
+ import { handleIntegrationVerification } from "./integration-verification";
7
+ import { handleParticipantRequest } from "./participant-routes";
8
+ import { handleCustomerParticipantAccess } from "./participant-customer-access";
9
+ import { issueParticipantAccessLink, pruneParticipantAccess } from "./participant-access";
10
+ import { acknowledgeCaseEscalation, getCaseEscalationPolicies, listCaseEscalations, reconcileCaseEscalations, saveCaseEscalationPolicy } from "./case-escalations";
11
+ export { CaseInvestigator } from "./case-investigator";
1
12
  import { canonicalJson } from "./audit";
13
+ import { loadPlaybookResources } from "./playbook-resources";
2
14
  import { reporterEmailConfigured } from "./report-email";
3
15
  import { decodeApiCursor, encodeApiCursor } from "./api-cursor";
4
16
  import {
@@ -177,6 +189,7 @@ import {
177
189
  loadAppealDetail,
178
190
  loadReportDetail,
179
191
  recordHumanDecision,
192
+ recordParticipantAppeal,
180
193
  releaseAppealClaim,
181
194
  releaseReportClaim,
182
195
  } from "./report-workflow";
@@ -254,6 +267,7 @@ import {
254
267
  deleteWorkflowFixture,
255
268
  diffWorkflowVersion,
256
269
  listWorkflowFixtures,
270
+ listWorkflowTestRuns,
257
271
  listWorkflowDefinitions,
258
272
  loadWorkflowTestRun,
259
273
  loadWorkflowVersion,
@@ -308,6 +322,7 @@ import {
308
322
  verifyFeature,
309
323
  } from "./installation-admin";
310
324
  import { loadWorkspaceBrand } from "./workspace-branding";
325
+ import { databaseSchemaReady } from "./database-readiness";
311
326
  import {
312
327
  createExport,
313
328
  createSavedView,
@@ -337,8 +352,6 @@ const reportStates = new Set<ReportState>([
337
352
  "received", "human_review", "ai_review", "awaiting_reporter", "resolved_by_human",
338
353
  "resolved_by_ai", "human_review_requested", "appealed", "closed",
339
354
  ]);
340
- const EXPECTED_DATABASE_MIGRATIONS = 41;
341
-
342
355
  function integer(value: string | undefined, fallback: number, minimum: number, maximum: number): number {
343
356
  const parsed = Number.parseInt(value ?? "", 10);
344
357
  return Number.isFinite(parsed) ? Math.max(minimum, Math.min(maximum, parsed)) : fallback;
@@ -368,6 +381,22 @@ function receiptJson(receipt: Awaited<ReturnType<typeof createReport>>["receipt"
368
381
  };
369
382
  }
370
383
 
384
+ async function receiptWithParticipantAccess(env: Env, result: Awaited<ReturnType<typeof createReport>>): Promise<Record<string, unknown>> {
385
+ const receipt = receiptJson(result.receipt);
386
+ // An idempotency key alone is not participant authentication. Only the
387
+ // request that created this report may receive a new private access link.
388
+ if (!result.created) return receipt;
389
+ try {
390
+ return { ...receipt, participant_url: await issueParticipantAccessLink(env, {
391
+ reportId: result.receipt.reportId, audience: "reporter", deliveryKey: `intake:${result.receipt.reportId}`,
392
+ }) };
393
+ } catch {
394
+ // The report has already committed. A failed access-link dependency must
395
+ // not imply that the submission failed or encourage another new report.
396
+ return { ...receipt, participant_access: "unavailable", participant_access_message: "Your report was received. Use the private link in your case email, or contact the service with your reference if you cannot receive updates." };
397
+ }
398
+ }
399
+
371
400
  function queueRoutingJob(env: Env, ctx: ExecutionContext, jobId: string, reportId: string): void {
372
401
  const job: ReportJob = { version: 1, jobId, type: "route_report", reportId };
373
402
  ctx.waitUntil((async () => {
@@ -615,7 +644,7 @@ async function handleWidgetReport(request: Request, env: Env, ctx: ExecutionCont
615
644
  if (result.route) queueRoutingJob(env, ctx, result.jobId, result.receipt.reportId);
616
645
  queueDeliveryJobs(env, ctx, result.deliveryJobs);
617
646
  if (result.created) publishWorkspaceEvent(env, ctx, { type: "reports_changed", reportId: result.receipt.reportId });
618
- return json(receiptJson(result.receipt), { status: result.created ? 201 : 200 });
647
+ return json(await receiptWithParticipantAccess(env, result), { status: result.created ? 201 : 200, headers: { "cache-control": "no-store", "referrer-policy": "no-referrer" } });
619
648
  }
620
649
 
621
650
  async function handleAnonymousWidgetReport(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
@@ -652,7 +681,7 @@ async function handleAnonymousWidgetReport(request: Request, env: Env, ctx: Exec
652
681
  if (result.route) queueRoutingJob(env, ctx, result.jobId, result.receipt.reportId);
653
682
  queueDeliveryJobs(env, ctx, result.deliveryJobs);
654
683
  if (result.created) publishWorkspaceEvent(env, ctx, { type: "reports_changed", reportId: result.receipt.reportId });
655
- return json(receiptJson(result.receipt), { status: result.created ? 201 : 200 });
684
+ return json(await receiptWithParticipantAccess(env, result), { status: result.created ? 201 : 200, headers: { "cache-control": "no-store", "referrer-policy": "no-referrer" } });
656
685
  }
657
686
 
658
687
  async function handleServerReport(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
@@ -674,7 +703,7 @@ async function handleServerReport(request: Request, env: Env, ctx: ExecutionCont
674
703
  if (result.route) queueRoutingJob(env, ctx, result.jobId, result.receipt.reportId);
675
704
  queueDeliveryJobs(env, ctx, result.deliveryJobs);
676
705
  if (result.created) publishWorkspaceEvent(env, ctx, { type: "reports_changed", reportId: result.receipt.reportId });
677
- return json(receiptJson(result.receipt), { status: result.created ? 201 : 200 });
706
+ return json(await receiptWithParticipantAccess(env, result), { status: result.created ? 201 : 200, headers: { "cache-control": "no-store", "referrer-policy": "no-referrer" } });
678
707
  }
679
708
 
680
709
  async function handlePublicConfig(request: Request, env: Env): Promise<Response> {
@@ -703,6 +732,7 @@ async function handlePublicConfig(request: Request, env: Env): Promise<Response>
703
732
  return json({
704
733
  schema_version: "1",
705
734
  turnstile_site_key: config.siteKey,
735
+ reporter_notifications_available: reporterEmailConfigured(env),
706
736
  form: {
707
737
  id: form.id,
708
738
  title: form.title,
@@ -748,7 +778,7 @@ async function handlePublicReport(request: Request, env: Env, ctx: ExecutionCont
748
778
  if (result.route) queueRoutingJob(env, ctx, result.jobId, result.receipt.reportId);
749
779
  queueDeliveryJobs(env, ctx, result.deliveryJobs);
750
780
  if (result.created) publishWorkspaceEvent(env, ctx, { type: "reports_changed", reportId: result.receipt.reportId });
751
- return json(receiptJson(result.receipt), { status: result.created ? 201 : 200 });
781
+ return json(await receiptWithParticipantAccess(env, result), { status: result.created ? 201 : 200, headers: { "cache-control": "no-store", "referrer-policy": "no-referrer" } });
752
782
  }
753
783
 
754
784
  async function handleAdminSession(request: Request, env: Env): Promise<Response> {
@@ -869,7 +899,7 @@ async function handleOperatorPasswordResetRequest(request: Request, env: Env, ct
869
899
  });
870
900
  if (!better.ok) {
871
901
  console.warn("Password reset request was not accepted by Better Auth", better.status);
872
- better.body?.cancel();
902
+ await better.body?.cancel().catch(() => undefined);
873
903
  }
874
904
  return json({
875
905
  schema_version: "1",
@@ -955,7 +985,7 @@ async function handleOwnerSetupAccept(request: Request, env: Env, ctx: Execution
955
985
  rememberMe: false,
956
986
  });
957
987
  if (!signUp.ok) return normalizedBetterAuthError(signUp, "The owner account could not be created.");
958
- signUp.body?.cancel();
988
+ await signUp.body?.cancel().catch(() => undefined);
959
989
  await env.DB.prepare(`UPDATE auth_user SET emailVerified = 1, updatedAt = ?2 WHERE email = ?1 COLLATE NOCASE`)
960
990
  .bind(setup.email, new Date().toISOString()).run();
961
991
  const signIn = await callBetterAuth(request, env, ctx, "/sign-in/email", {
@@ -970,7 +1000,7 @@ async function handleOwnerSetupAccept(request: Request, env: Env, ctx: Execution
970
1000
  name: displayName || setup.email.split("@", 1)[0],
971
1001
  }, null, setup.id);
972
1002
  if (!owner?.roles.includes("owner")) {
973
- signIn.body?.cancel();
1003
+ await signIn.body?.cancel().catch(() => undefined);
974
1004
  throw new ApiError(409, "owner_setup_already_used", "This owner setup link has already been used or is no longer valid.");
975
1005
  }
976
1006
  return clearOwnerSetupCookie(normalizedBetterAuthSuccess(signIn, {
@@ -1089,7 +1119,8 @@ async function handleAdminPresence(request: Request, env: Env): Promise<Response
1089
1119
  }
1090
1120
  if (request.method === "POST") {
1091
1121
  const heartbeat = parsePresenceHeartbeat(await readJsonObject(request, 4 * 1024));
1092
- const result = await touchAnalystSession(env.DB, {
1122
+ const db = env.DB.withSession("first-primary");
1123
+ const result = await touchAnalystSession(db, {
1093
1124
  ...heartbeat,
1094
1125
  actorId: session.actor.id,
1095
1126
  allowedQueueIds: session.queueScope ?? null,
@@ -1099,7 +1130,7 @@ async function handleAdminPresence(request: Request, env: Env): Promise<Response
1099
1130
  schema_version: "1",
1100
1131
  profile,
1101
1132
  claim_renewed: result.claimRenewed,
1102
- analysts: await listAnalystPresence(env.DB, 45, session.queueScope ?? null),
1133
+ analysts: await listAnalystPresence(db, 45, session.queueScope ?? null),
1103
1134
  });
1104
1135
  }
1105
1136
  return methodNotAllowed(["GET", "POST"]);
@@ -1636,6 +1667,10 @@ async function handleAdminWorkflow(
1636
1667
  }
1637
1668
  return methodNotAllowed(["GET", "POST"]);
1638
1669
  }
1670
+ if (action === "test-runs" && request.method === "GET") {
1671
+ await requireOperator(request, env, "manageWorkflows");
1672
+ return json({ schema_version: "1", test_runs: await listWorkflowTestRuns(env.DB, workflowId) });
1673
+ }
1639
1674
  if (action === "test" || action === "test-runs") {
1640
1675
  if (request.method !== "POST") return methodNotAllowed(["POST"]);
1641
1676
  const session = await requireOperator(request, env, "manageWorkflows");
@@ -1693,7 +1728,9 @@ async function handleAdminWorkflowFixture(
1693
1728
  };
1694
1729
  const name = body.name ?? current.name;
1695
1730
  if (typeof name !== "string") throw new ApiError(400, "workflow_fixture_invalid", "name must be a string.");
1731
+ if (!Number.isInteger(body.expected_revision) || Number(body.expected_revision) < 1) throw new ApiError(400, "workflow_fixture_revision_required", "expected_revision must be the saved example revision.");
1696
1732
  const result = await updateWorkflowFixture(env.DB, workflowId, fixtureId, {
1733
+ expectedRevision: Number(body.expected_revision),
1697
1734
  name,
1698
1735
  input: objectField(body.input ?? current.input, "input"),
1699
1736
  componentResults: objectField(body.component_results ?? current.componentResults, "component_results"),
@@ -1761,8 +1798,11 @@ async function handleAdminQueueWorkflowPin(request: Request, env: Env, queueId:
1761
1798
  if (body.rollback_of_pin_id !== undefined && typeof body.rollback_of_pin_id !== "string") {
1762
1799
  throw new ApiError(400, "workflow_pin_invalid", "rollback_of_pin_id must be a string when supplied.");
1763
1800
  }
1801
+ if (body.expected_pin_id !== undefined && body.expected_pin_id !== null && typeof body.expected_pin_id !== "string") throw new ApiError(400, "workflow_pin_invalid", "expected_pin_id must be a string or null.");
1802
+ assertOperatorQueueAccess(session, queueId);
1764
1803
  const result = await pinQueueWorkflow(env.DB, queueId, {
1765
1804
  workflowVersionId: body.workflow_version_id.trim(),
1805
+ ...(body.expected_pin_id !== undefined ? { expectedPinId: body.expected_pin_id as string | null } : {}),
1766
1806
  mode: body.mode,
1767
1807
  ...(typeof body.rollback_of_pin_id === "string" ? { rollbackOfPinId: body.rollback_of_pin_id } : {}),
1768
1808
  }, session, mutationKey(request));
@@ -1819,6 +1859,7 @@ async function handleAdminWorkflowRuns(request: Request, env: Env): Promise<Resp
1819
1859
  schema_version: "1",
1820
1860
  runs: await listWorkflowRuns(env.DB, {
1821
1861
  ...(state ? { state } : {}),
1862
+ workflowId: url.searchParams.get("workflow_id")?.trim() || undefined,
1822
1863
  allowedQueueIds: session.queueScope ?? null,
1823
1864
  limit: integer(url.searchParams.get("limit") ?? undefined, 50, 1, 100),
1824
1865
  }),
@@ -1989,9 +2030,9 @@ async function handleAdminAiRun(request: Request, env: Env, runId: string): Prom
1989
2030
 
1990
2031
  async function handleAdminAiUsage(request: Request, env: Env): Promise<Response> {
1991
2032
  if (request.method !== "GET") return methodNotAllowed(["GET"]);
1992
- await requireOperator(request, env, "readAi");
2033
+ const session = await requireOperator(request, env, "readAi");
1993
2034
  const days = integer(new URL(request.url).searchParams.get("days") ?? undefined, 30, 1, 366);
1994
- return json({ schema_version: "1", usage: await loadAiUsage(env.DB, days) });
2035
+ return json({ schema_version: "1", usage: await loadAiUsage(env.DB, days, session.queueScope ?? null) });
1995
2036
  }
1996
2037
 
1997
2038
  async function handleAdminBudgets(request: Request, env: Env): Promise<Response> {
@@ -2226,15 +2267,21 @@ async function handleAdminHumanTasks(request: Request, env: Env): Promise<Respon
2226
2267
  const queueId = url.searchParams.get("queue_id")?.trim() || undefined;
2227
2268
  if (queueId && queueId.length > 100) throw new ApiError(400, "human_task_queue_invalid", "queue_id is invalid.");
2228
2269
  if (queueId) assertOperatorQueueAccess(session, queueId);
2270
+ const reportId = url.searchParams.get("report_id")?.trim() || undefined;
2271
+ if (reportId) {
2272
+ if (reportId.length > 100) throw new ApiError(400, "human_task_report_invalid", "report_id is invalid.");
2273
+ await assertOperatorReportAccess(env.DB, session, reportId);
2274
+ }
2229
2275
  return json({
2230
2276
  schema_version: "1",
2231
- tasks: await listHumanTasks(env.DB, {
2277
+ tasks: projectOperatorFields(session, "task", await listHumanTasks(env.DB, {
2232
2278
  ...(state ? { state } : {}),
2279
+ ...(reportId ? { reportId } : {}),
2233
2280
  ...(queueId ? { queueId } : {}),
2234
2281
  ...(url.searchParams.get("mine") === "true" ? { operatorId: session.actor.id } : {}),
2235
2282
  allowedQueueIds: session.queueScope ?? null,
2236
2283
  limit: integer(url.searchParams.get("limit") ?? undefined, 50, 1, 100),
2237
- }),
2284
+ })),
2238
2285
  });
2239
2286
  }
2240
2287
 
@@ -2439,6 +2486,7 @@ async function handleAdminActionProposal(
2439
2486
  if (request.method !== "GET") return methodNotAllowed(["GET"]);
2440
2487
  const session = await requireOperator(request, env, "readReports");
2441
2488
  await assertOperatorReportAccess(env.DB, session, reference.reportId);
2489
+ if (!session.permissions.readSensitiveEvidence && !session.permissions.approveActions) throw new ApiError(403, "action_details_restricted", "Your role can read action progress but cannot inspect protected customer action data.");
2442
2490
  const detail = await loadActionProposalDetail(env.DB, proposalId);
2443
2491
  return json({ schema_version: "1", ...detail });
2444
2492
  }
@@ -2603,6 +2651,8 @@ async function handleAdminAiQualitySample(
2603
2651
  const report = await loadReportDetail(env.DB, String(sample.reportId), {
2604
2652
  contactEncryptionKey: env.CREDENTIAL_ENCRYPTION_KEY,
2605
2653
  includeReporterEmail: session.permissions.messageReporters,
2654
+ includeCustomerContext: session.permissions.readSensitiveEvidence,
2655
+ allowedQueueIds: session.queueScope ?? null,
2606
2656
  });
2607
2657
  if (!report) throw new ApiError(404, "report_not_found", "The sampled report does not exist.");
2608
2658
  return json({ schema_version: "1", sample: projectOperatorFields(session, "quality_sample", sample), report: projectOperatorFields(session, "report", report) });
@@ -2645,24 +2695,23 @@ async function handleAdminAiQualityReview(
2645
2695
 
2646
2696
  async function handleAdminAnalytics(request: Request, env: Env): Promise<Response> {
2647
2697
  if (request.method !== "GET") return methodNotAllowed(["GET"]);
2648
- await requireOperator(request, env, "readAnalytics");
2698
+ const session = await requireOperator(request, env, "readAnalytics");
2649
2699
  const days = integer(new URL(request.url).searchParams.get("days") ?? undefined, 30, 1, 366);
2650
- await refreshDailyAggregatesIfStale(env.DB);
2651
- return json({ schema_version: "1", analytics: await loadAnalytics(env.DB, days) });
2700
+ return json({ schema_version: "1", analytics: await loadAnalytics(env.DB, days, session.queueScope ?? null) });
2652
2701
  }
2653
2702
 
2654
2703
  async function handleAdminAnalyticsAi(request: Request, env: Env): Promise<Response> {
2655
2704
  if (request.method !== "GET") return methodNotAllowed(["GET"]);
2656
- await requireOperator(request, env, "readAi");
2705
+ const session = await requireOperator(request, env, "readAi");
2657
2706
  const days = integer(new URL(request.url).searchParams.get("days") ?? undefined, 30, 1, 366);
2658
- return json({ schema_version: "1", analytics: await loadAiUsage(env.DB, days) });
2707
+ return json({ schema_version: "1", analytics: await loadAiUsage(env.DB, days, session.queueScope ?? null) });
2659
2708
  }
2660
2709
 
2661
2710
  async function handleAdminAnalyticsWorkflows(request: Request, env: Env): Promise<Response> {
2662
2711
  if (request.method !== "GET") return methodNotAllowed(["GET"]);
2663
- await requireOperator(request, env, "readAnalytics");
2712
+ const session = await requireOperator(request, env, "readAnalytics");
2664
2713
  const days = integer(new URL(request.url).searchParams.get("days") ?? undefined, 30, 1, 366);
2665
- return json({ schema_version: "1", analytics: await loadWorkflowAnalytics(env.DB, days) });
2714
+ return json({ schema_version: "1", analytics: await loadWorkflowAnalytics(env.DB, days, session.queueScope ?? null) });
2666
2715
  }
2667
2716
 
2668
2717
  async function handleAdminSearch(request: Request, env: Env): Promise<Response> {
@@ -2854,6 +2903,8 @@ async function handleAdminReportDetail(request: Request, env: Env, reportId: str
2854
2903
  const report = await loadReportDetail(env.DB, reportId, {
2855
2904
  contactEncryptionKey: env.CREDENTIAL_ENCRYPTION_KEY,
2856
2905
  includeReporterEmail: session.permissions.messageReporters,
2906
+ includeCustomerContext: session.permissions.readSensitiveEvidence,
2907
+ allowedQueueIds: session.queueScope ?? null,
2857
2908
  });
2858
2909
  if (!report) throw new ApiError(404, "report_not_found", "The report does not exist.");
2859
2910
  return json({ schema_version: "1", report: projectOperatorFields(session, "report", report) });
@@ -3094,8 +3145,20 @@ async function handleAdminDecision(request: Request, env: Env, decisionId: strin
3094
3145
  }
3095
3146
 
3096
3147
  async function handleAdminAppeals(request: Request, env: Env): Promise<Response> {
3097
- if (request.method !== "GET") return methodNotAllowed(["GET"]);
3148
+ if (!["GET", "POST"].includes(request.method)) return methodNotAllowed(["GET", "POST"]);
3098
3149
  const session = await requireOperator(request, env, "reviewAppeals");
3150
+ if (request.method === "POST") {
3151
+ const input = await readJsonObject(request, 16 * 1024);
3152
+ const reference = typeof input.report_reference === "string" ? input.report_reference.trim() : "";
3153
+ const reason = typeof input.reason === "string" ? input.reason.trim() : "";
3154
+ const key = request.headers.get("idempotency-key")?.trim() || "";
3155
+ if (!reference || reference.length > 100 || reason.length < 3 || reason.length > 8000 || !["reporter", "affected_user"].includes(String(input.audience)) || key.length < 8 || key.length > 200) throw new ApiError(400, "appeal_input_invalid", "Enter a report reference, participant, reason (3–8000 characters), and idempotency key.");
3156
+ const report = await env.DB.prepare(`SELECT id FROM reports WHERE public_reference = ?1 LIMIT 1`).bind(reference).first<{ id: string }>();
3157
+ if (!report) throw new ApiError(404, "report_not_found", "No report matches this reference.");
3158
+ await assertOperatorReportAccess(env.DB, session, report.id);
3159
+ const result = await recordParticipantAppeal(env.DB, report.id, session, { audience: input.audience as "reporter" | "affected_user", reason, idempotencyKey: key });
3160
+ return json({ schema_version: "1", appeal_id: result.appealId, idempotent_replay: result.idempotentReplay }, { status: result.idempotentReplay ? 200 : 201 });
3161
+ }
3099
3162
  const limit = integer(new URL(request.url).searchParams.get("limit") ?? undefined, 50, 1, 100);
3100
3163
  return json({ schema_version: "1", appeals: await listAppeals(env.DB, limit, session.queueScope ?? null) });
3101
3164
  }
@@ -3171,24 +3234,26 @@ async function handleReady(request: Request, env: Env): Promise<Response> {
3171
3234
  await requireAnyOperator(request, env);
3172
3235
  try {
3173
3236
  const [migrations, features] = await Promise.all([
3174
- env.DB.prepare(`SELECT COUNT(*) AS count FROM d1_migrations`).first<{ count: number }>(),
3237
+ env.DB.prepare(`SELECT name FROM d1_migrations ORDER BY id`).all<{ name: string }>(),
3175
3238
  env.DB.prepare(`SELECT feature_key AS featureKey, state FROM installation_features WHERE installation_id = 'default'`).all<{ featureKey: string; state: string }>(),
3176
3239
  env.PROFILE_MEDIA.list({ limit: 1 }),
3177
3240
  env.WORKFLOW_ARTIFACTS.list({ limit: 1 }),
3178
3241
  env.EVIDENCE_OBJECTS.list({ limit: 1 }),
3179
3242
  env.EXPORTS.list({ limit: 1 }),
3180
3243
  ]);
3181
- const migrationCount = Number(migrations?.count ?? 0);
3244
+ const migrationNames = migrations.results.map(({ name }) => name);
3245
+ const migrationCount = migrationNames.length;
3182
3246
  const featureStates = new Map(features.results.map((feature) => [feature.featureKey, feature.state]));
3183
3247
  const requiredFeatures = ["database", "workflow_runtime", "workers_ai", "object_storage", "queues"];
3184
3248
  const unavailable = requiredFeatures.filter((feature) => featureStates.get(feature) !== "ready");
3185
3249
  const bindingsReady = Boolean(env.WORKFLOWS && env.LOADER && env.CONTROL_JOBS && env.REPORT_JOBS && env.DELIVERY_JOBS && env.AI);
3186
- const ready = migrationCount >= EXPECTED_DATABASE_MIGRATIONS && unavailable.length === 0 && bindingsReady;
3250
+ const schemaReady = databaseSchemaReady(migrationNames);
3251
+ const ready = schemaReady && unavailable.length === 0 && bindingsReady;
3187
3252
  return json({
3188
3253
  status: ready ? "ready" : "not_ready",
3189
3254
  service: "safest-resolve-reports",
3190
3255
  schema_migrations: migrationCount,
3191
- checks: { database: true, private_object_storage: true, runtime_bindings: bindingsReady, unavailable_features: unavailable },
3256
+ checks: { database: schemaReady, private_object_storage: true, runtime_bindings: bindingsReady, unavailable_features: unavailable },
3192
3257
  }, { status: ready ? 200 : 503 });
3193
3258
  } catch {
3194
3259
  return json({ status: "not_ready", service: "safest-resolve-reports", checks: { dependency_probe: false } }, { status: 503 });
@@ -3498,7 +3563,10 @@ async function handleAdminIntegration(
3498
3563
  }
3499
3564
 
3500
3565
  async function route(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
3566
+ if (new URL(request.url).pathname === "/v1/participant-access") return handleCustomerParticipantAccess(request, env);
3501
3567
  const url = new URL(request.url);
3568
+ const participantResponse = await handleParticipantRequest(request, env, reportId => publishWorkspaceEvent(env, ctx, { type: "reports_changed", reportId }));
3569
+ if (participantResponse) return participantResponse;
3502
3570
  if (url.pathname === "/api/auth" || url.pathname.startsWith("/api/auth/")) {
3503
3571
  return handleBetterAuthRequest(request, env, ctx);
3504
3572
  }
@@ -3532,6 +3600,43 @@ async function route(request: Request, env: Env, ctx: ExecutionContext): Promise
3532
3600
  if (url.pathname === "/v1/admin/installation") return handleAdminInstallation(request, env);
3533
3601
  if (url.pathname === "/v1/admin/brand") return handleAdminBrand(request, env);
3534
3602
  if (url.pathname === "/v1/admin/reporting-channels") return handleAdminReportingChannels(request, env);
3603
+ if (url.pathname === "/v1/admin/integration-progress") {
3604
+ if(request.method!=="GET")return methodNotAllowed(["GET"]);
3605
+ const session=await requireOperator(request,env,"manageComponents");
3606
+ return json(await loadIntegrationProgress(env,session));
3607
+ }
3608
+ if (/^\/v1\/admin\/integration-verifications(?:\/[^/]+(?:\/bind)?)?$/u.test(url.pathname)) {
3609
+ const session = await requireOperator(request, env, "manageIntegrations");
3610
+ return handleIntegrationVerification(request, env, session);
3611
+ }
3612
+ if (url.pathname === "/v1/admin/case-escalations" || /^\/v1\/admin\/case-escalations\/[^/]+\/acknowledge$/u.test(url.pathname)) {
3613
+ const acknowledgedId = /^\/v1\/admin\/case-escalations\/([^/]+)\/acknowledge$/u.exec(url.pathname)?.[1];
3614
+ if (request.method !== (acknowledgedId ? "POST" : "GET")) return methodNotAllowed([acknowledgedId ? "POST" : "GET"]);
3615
+ const identity = await requireAnyOperator(request, env);
3616
+ const session = await requireOperator(request, env, identity.permissions.readReports ? "readReports" : "readOperations");
3617
+ if (!acknowledgedId) return json(await listCaseEscalations(env.DB, session, url.searchParams.get("cursor") || ""));
3618
+ const body = await readJsonObject(request, 4096);
3619
+ if (typeof body.revision !== "number" || !Number.isSafeInteger(body.revision) || body.revision < 1) throw new ApiError(400, "escalation_revision_required", "Refresh the recovery item before acknowledging it.");
3620
+ await acknowledgeCaseEscalation(env.DB, session, acknowledgedId, body.revision);
3621
+ return json({ acknowledged: true });
3622
+ }
3623
+ if (url.pathname === "/v1/admin/case-escalation-policies") {
3624
+ if (!["GET", "PUT"].includes(request.method)) return methodNotAllowed(["GET", "PUT"]);
3625
+ const session = await requireOperator(request, env, "manageConfiguration");
3626
+ if (request.method === "GET") return json(await getCaseEscalationPolicies(env.DB, session));
3627
+ const body = await readJsonObject(request, 8192);
3628
+ if (![body.queueId, body.primaryOperatorId, body.backupOperatorId].every(value => value === null || (typeof value === "string" && value.length > 0 && value.length <= 100))
3629
+ || typeof body.acknowledgementMinutes !== "number" || typeof body.followUpMinutes !== "number" || typeof body.revision !== "number") {
3630
+ throw new ApiError(400, "escalation_policy_invalid", "Choose recovery contacts and response deadlines.");
3631
+ }
3632
+ await saveCaseEscalationPolicy(env.DB, session, {
3633
+ queueId: typeof body.queueId === "string" ? body.queueId : null,
3634
+ primaryOperatorId: typeof body.primaryOperatorId === "string" ? body.primaryOperatorId : null,
3635
+ backupOperatorId: typeof body.backupOperatorId === "string" ? body.backupOperatorId : null,
3636
+ acknowledgementMinutes: body.acknowledgementMinutes, followUpMinutes: body.followUpMinutes, revision: body.revision,
3637
+ });
3638
+ return json({ saved: true });
3639
+ }
3535
3640
  if (url.pathname === "/v1/admin/setup") return handleAdminSetup(request, env);
3536
3641
  if (url.pathname === "/v1/admin/integrations") return handleAdminIntegrations(request, env);
3537
3642
  if (url.pathname === "/v1/admin/people") return handleAdminPeople(request, env);
@@ -3541,6 +3646,11 @@ async function route(request: Request, env: Env, ctx: ExecutionContext): Promise
3541
3646
  if (url.pathname === "/v1/admin/reports") return handleAdminReports(request, env);
3542
3647
  if (url.pathname === "/v1/admin/queues") return handleAdminQueues(request, env);
3543
3648
  if (url.pathname === "/v1/admin/workflows") return handleAdminWorkflows(request, env);
3649
+ if (url.pathname === "/v1/admin/playbook-resources") {
3650
+ if (request.method !== "GET") return methodNotAllowed(["GET"]);
3651
+ await requireOperator(request, env, "manageWorkflows");
3652
+ return json({ schema_version: "1", ...await loadPlaybookResources(env.DB) });
3653
+ }
3544
3654
  if (url.pathname === "/v1/admin/workflow-runs") return handleAdminWorkflowRuns(request, env);
3545
3655
  if (url.pathname === "/v1/admin/prompts") return handleAdminPrompts(request, env);
3546
3656
  if (url.pathname === "/v1/admin/model-aliases") return handleAdminModelAliases(request, env);
@@ -3797,9 +3907,27 @@ async function route(request: Request, env: Env, ctx: ExecutionContext): Promise
3797
3907
  decodeURIComponent(reportEnrichmentRoute[1] ?? ""),
3798
3908
  decodeURIComponent(reportEnrichmentRoute[2] ?? ""),
3799
3909
  );
3800
- const reportRoute = /^\/v1\/admin\/reports\/([^/]+)(?:\/(claim|messages|decisions|workflow-runs|reassign|timeline|findings|actions|close|reopen))?$/u.exec(url.pathname);
3910
+ const noticeRetryRoute = /^\/v1\/admin\/reports\/([^/]+)\/notices\/([^/]+)\/retry$/u.exec(url.pathname);
3911
+ if (noticeRetryRoute) {
3912
+ if (request.method !== "POST") return methodNotAllowed(["POST"]);
3913
+ const session = await requireOperator(request, env, "messageReporters");
3914
+ const body = await readJsonObject(request, 4096);
3915
+ if (typeof body.expected_updated_at !== "string") throw new ApiError(400, "notice_revision_required", "Refresh the notice before retrying.");
3916
+ return json(await retryParticipantNotice(env, decodeURIComponent(noticeRetryRoute[1]!), decodeURIComponent(noticeRetryRoute[2]!), body.expected_updated_at, session, mutationKey(request)));
3917
+ }
3918
+ const reportRoute = /^\/v1\/admin\/reports\/([^/]+)(?:\/(ownership|claim|messages|decisions|workflow-runs|reassign|timeline|findings|actions|close|reopen))?$/u.exec(url.pathname);
3801
3919
  if (reportRoute) {
3802
3920
  const reportId = decodeURIComponent(reportRoute[1] ?? "");
3921
+ if (reportRoute[2] === "ownership") {
3922
+ const session = await requireOperator(request, env, "reassignReports");
3923
+ await assertOperatorReportAccess(env.DB, session, reportId);
3924
+ if (request.method === "GET") return json({ reviewers: await eligibleReportOwners(env.DB, reportId) });
3925
+ if (request.method !== "PATCH") return methodNotAllowed(["GET", "PATCH"]);
3926
+ const body = await readJsonObject(request, 24 * 1024);
3927
+ await changeReportOwner(env.DB, reportId, session, { reviewerId: body.reviewer_id as string | null, version: body.version as number, note: body.note as string });
3928
+ publishWorkspaceEvent(env, ctx, { type: "claim_changed", reportId });
3929
+ return new Response(null, { status: 204 });
3930
+ }
3803
3931
  if (reportRoute[2] === "claim") return handleAdminReportClaim(request, env, ctx, reportId);
3804
3932
  if (reportRoute[2] === "messages") return handleAdminReportMessage(request, env, ctx, reportId);
3805
3933
  if (reportRoute[2] === "decisions") return handleAdminReportDecision(request, env, ctx, reportId);
@@ -3813,6 +3941,38 @@ async function route(request: Request, env: Env, ctx: ExecutionContext): Promise
3813
3941
  }
3814
3942
  return handleAdminReportDetail(request, env, reportId);
3815
3943
  }
3944
+ const appealCorrectionsRoute = /^\/v1\/admin\/appeals\/([^/]+)\/corrections(?:\/([^/]+)\/(verify|retry|assignment))?$/u.exec(url.pathname);
3945
+ if (appealCorrectionsRoute) {
3946
+ const appealId = decodeURIComponent(appealCorrectionsRoute[1]!);
3947
+ const correctionId = appealCorrectionsRoute[2] ? decodeURIComponent(appealCorrectionsRoute[2]) : undefined;
3948
+ const action = appealCorrectionsRoute[3];
3949
+ if (!action && request.method === "GET") {
3950
+ const session = await requireOperator(request, env, "reviewAppeals");
3951
+ return json(await loadAppealCorrections(env.DB, appealId, session));
3952
+ }
3953
+ if (request.method !== "POST") return methodNotAllowed(action ? ["POST"] : ["GET", "POST"]);
3954
+ const session = await requireOperator(request, env, action === "verify" ? "reviewAppeals" : "decideAppeals");
3955
+ let result;
3956
+ if (action === "retry") result = await retryAppealCorrection(env, appealId, correctionId!, session, mutationKey(request));
3957
+ else {
3958
+ const input = await readJsonObject(request, 24 * 1024);
3959
+ if (action === "verify") {
3960
+ if (typeof input.reference !== "string" || typeof input.note !== "string") throw new ApiError(400, "correction_evidence_required", "Record the customer reference and verification notes.");
3961
+ result = await confirmAppealCorrection(env.DB, appealId, session, {correctionId, reference: input.reference.trim(), note: input.note.trim(), idempotencyKey: mutationKey(request)});
3962
+ } else if (action === "assignment") result = await reassignAppealCorrection(env, appealId, correctionId!, session, input, mutationKey(request));
3963
+ else result = await createAppealCorrection(env, appealId, session, input, mutationKey(request));
3964
+ }
3965
+ publishWorkspaceEvent(env, ctx, {type: "reports_changed"});
3966
+ return json(result);
3967
+ }
3968
+ const appealCorrectionRoute = /^\/v1\/admin\/appeals\/([^/]+)\/correction$/u.exec(url.pathname);
3969
+ if (appealCorrectionRoute) {
3970
+ if (request.method !== "POST") return methodNotAllowed(["POST"]);
3971
+ const session = await requireOperator(request,env,"reviewAppeals");
3972
+ const input = await readJsonObject(request, 8 * 1024);
3973
+ if (typeof input.reference !== "string" || typeof input.note !== "string") throw new ApiError(400,"correction_evidence_required","Record the customer reference and verification notes.");
3974
+ return json(await confirmAppealCorrection(env.DB,decodeURIComponent(appealCorrectionRoute[1]!),session,{reference:input.reference.trim(),note:input.note.trim(),idempotencyKey:mutationKey(request)}));
3975
+ }
3816
3976
  const appealClaimRoute = /^\/v1\/admin\/appeals\/([^/]+)\/claim$/u.exec(url.pathname);
3817
3977
  if (appealClaimRoute) return handleAdminAppealClaim(request, env, decodeURIComponent(appealClaimRoute[1] ?? ""));
3818
3978
  const appealDecisionRoute = /^\/v1\/admin\/appeals\/([^/]+)\/decision$/u.exec(url.pathname);
@@ -3999,6 +4159,7 @@ async function reconcileRouting(env: Env): Promise<ReconciliationResult> {
3999
4159
  }
4000
4160
 
4001
4161
  async function reconcileDeliveries(env: Env): Promise<ReconciliationResult> {
4162
+ await reconcileAppealCompletion(env.DB);
4002
4163
  const [jobs, actionJobs] = await Promise.all([
4003
4164
  pendingDeliveryJobs(env.DB, 50),
4004
4165
  pendingActionProposalJobs(env.DB, 50),
@@ -4161,9 +4322,10 @@ export default {
4161
4322
  async scheduled(_controller: ScheduledController, env: Env, ctx: ExecutionContext): Promise<void> {
4162
4323
  ctx.waitUntil((async () => {
4163
4324
  await Promise.all([
4164
- runRecordedReconciliation(env, "all", "system:maintenance"), escalateExpiredAiWaits(env.DB),
4325
+ runRecordedReconciliation(env, "all", "system:maintenance"), escalateExpiredAiWaits(env.DB), reconcileCaseEscalations(env.DB),
4165
4326
  maintainIntakeProtection(env.DB),
4166
4327
  pruneReportContexts(env.DB),
4328
+ pruneParticipantAccess(env.DB),
4167
4329
  compareCompletedShadowRuns(env.DB),
4168
4330
  expireExports(env),
4169
4331
  pruneConnectionOAuthStates(env.DB),
@@ -1,3 +1,4 @@
1
+ import { version as applicationVersion } from "../package.json";
1
2
  import { canonicalJson } from "./audit";
2
3
  import { sha256 } from "./report-crypto";
3
4
  import { ApiError } from "./report-http";
@@ -293,7 +294,7 @@ export async function verifyFeature(env: Env, feature: string, actorId: string):
293
294
  export async function metaVersion(db: D1Database): Promise<Record<string, unknown>> {
294
295
  const migration = await db.prepare(`SELECT name FROM d1_migrations ORDER BY id DESC LIMIT 1`).first<{ name: string }>().catch(() => null);
295
296
  return {
296
- application: "0.1.0",
297
+ application: applicationVersion,
297
298
  apiSchema: "1",
298
299
  databaseMigration: migration?.name ?? "unknown",
299
300
  workflowCompiler: WORKFLOW_COMPILER_VERSION,
@@ -0,0 +1,45 @@
1
+ import { canonicalJson } from "./audit";
2
+ import { ApiError } from "./report-http";
3
+ import { reporterEmailConfigured } from "./report-email";
4
+ import type { OperatorSession } from "./report-types";
5
+
6
+ import type { IntegrationEvidence, IntegrationProgress } from "../shared/integration-progress";
7
+
8
+ export async function loadIntegrationProgress(env: Env, session: OperatorSession): Promise<IntegrationProgress> {
9
+ if (!session.permissions.manageComponents) throw new ApiError(403,"integration_progress_denied","Your role cannot inspect customer capability setup.");
10
+ const unrestricted=session.queueScope==null?1:0,queues=canonicalJson(session.queueScope??[]);
11
+ const canReadCases=session.permissions.readReports && session.fieldScope==null;
12
+ const scope="(?1 = 1 OR r.queue_id IN (SELECT value FROM json_each(?2)))";
13
+ const evidence=async(from:string,where:string,at:string):Promise<IntegrationEvidence|null>=>canReadCases
14
+ ? env.DB.prepare(`SELECT r.id AS reportId, r.public_reference AS reference, ${at} AS observedAt ${from} WHERE ${where} AND ${scope} ORDER BY ${at} DESC, r.id DESC LIMIT 1`).bind(unrestricted,queues).first<IntegrationEvidence>()
15
+ : null;
16
+ const [routes,tools,pins,intake,context,action,notice,playbook]=await Promise.all([
17
+ session.permissions.manageIntegrations?env.DB.prepare(`SELECT
18
+ (SELECT COUNT(*) FROM application_integrations WHERE enabled = 1) +
19
+ (SELECT COUNT(*) FROM report_form_versions f JOIN installations i ON i.id=f.installation_id
20
+ WHERE i.anonymous_reporting_enabled = 1 AND f.published_at IS NOT NULL AND f.retired_at IS NULL
21
+ AND f.id IN (SELECT value FROM json_each(i.settings_json,'$.public_form_version_ids'))) AS count`).first<{count:number}>():null,
22
+ env.DB.prepare(`SELECT SUM(CASE WHEN d.effect_class='read_only' THEN 1 ELSE 0 END) AS reads,
23
+ SUM(CASE WHEN d.effect_class='side_effecting' THEN 1 ELSE 0 END) AS actions
24
+ FROM component_definitions d JOIN component_versions v ON v.id=d.active_version_id
25
+ WHERE d.owner_kind='customer' AND d.status='active' AND v.retired_at IS NULL`).first<{reads:number|null;actions:number|null}>(),
26
+ session.permissions.manageWorkflows?env.DB.prepare(`SELECT COUNT(*) AS count FROM queue_workflow_pins p JOIN workflow_versions v ON v.id=p.workflow_version_id
27
+ JOIN queue_definitions q ON q.id=p.queue_id WHERE p.deactivated_at IS NULL AND p.mode='primary' AND q.enabled=1 AND q.status='active'
28
+ AND json_type(v.graph_json,'$.playbook')='object' AND (?1=1 OR p.queue_id IN (SELECT value FROM json_each(?2)))`).bind(unrestricted,queues).first<{count:number}>():null,
29
+ evidence("FROM reports r","r.source IN ('server_api','signed_widget','public_page')","r.received_at"),
30
+ session.permissions.readEvidence?evidence(`FROM findings f JOIN reports r ON r.id=f.report_id JOIN component_versions cv ON cv.id=f.component_version_id
31
+ JOIN component_definitions cd ON cd.id=cv.definition_id JOIN workflow_runs wr ON wr.id=f.run_id`,
32
+ "cd.owner_kind='customer' AND cd.effect_class='read_only' AND f.status='available' AND f.superseded_at IS NULL AND (f.expires_at IS NULL OR f.expires_at>strftime('%Y-%m-%dT%H:%M:%fZ','now')) AND wr.run_kind='primary' AND wr.authority_mode!='shadow'","f.created_at"):null,
33
+ evidence("FROM action_proposals p JOIN reports r ON r.id=p.report_id","p.state='applied' AND p.confirmed_at IS NOT NULL","p.confirmed_at"),
34
+ evidence("FROM notices n JOIN reports r ON r.id=n.report_id","n.delivery_state='delivered' AND n.redacted_at IS NULL","n.updated_at"),
35
+ evidence("FROM workflow_runs wr JOIN workflow_versions v ON v.id=wr.workflow_version_id JOIN reports r ON r.id=wr.report_id",
36
+ `wr.state='succeeded' AND wr.run_kind='primary' AND wr.authority_mode!='shadow' AND json_type(v.graph_json,'$.playbook')='object'
37
+ AND CAST(replace(v.compiler_version,'safest-workflow-compiler-v','') AS INTEGER)>=5 AND r.state IN ('resolved_by_human','resolved_by_ai','closed')
38
+ AND EXISTS (SELECT 1 FROM decisions WHERE report_id=r.id)
39
+ AND NOT EXISTS (SELECT 1 FROM action_proposals WHERE run_id=wr.id AND state!='applied')
40
+ AND NOT EXISTS (SELECT 1 FROM case_messages WHERE workflow_run_id=wr.id AND direction='outbound' AND delivery_state!='delivered')`,"wr.updated_at"),
41
+ ]);
42
+ let localSandbox=false;try{localSandbox=env.AI_LOCAL_FIXTURE_ENABLED==="true"&&["localhost","127.0.0.1"].includes(new URL(env.PUBLIC_BASE_URL).hostname);}catch{}
43
+ return {localSandbox,canReadCases,configuration:{reportingRoutes:routes?.count??null,customerReads:tools?.reads??0,customerActions:tools?.actions??0,activePlaybooks:pins?.count??null,
44
+ noticeChannel:session.permissions.manageIntegrations||session.permissions.manageInstallation?Boolean(reporterEmailConfigured(env)||(env.NOTIFICATION_WEBHOOK_URL?.trim()&&env.NOTIFICATION_WEBHOOK_SECRET?.trim())):null},evidence:{intake,context,action,notice,playbook}};
45
+ }