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.
- package/README.md +3 -3
- package/package.json +2 -2
- package/src/config.mjs +2 -2
- package/src/update.mjs +10 -1
- package/template/README.md +2 -2
- package/template/console/README.md +2 -2
- package/template/console/analytics/AnalyticsWorkspace.tsx +49 -36
- package/template/console/analytics/controller.ts +10 -2
- package/template/console/analytics/types.ts +7 -0
- package/template/console/appeals/AppealFollowup.tsx +171 -0
- package/template/console/appeals/AppealWorkspace.tsx +33 -4
- package/template/console/appeals/RecordAppealDialog.tsx +42 -0
- package/template/console/appeals/appeal-corrections.css +29 -0
- package/template/console/appeals/controller.ts +40 -14
- package/template/console/appeals/events.ts +1 -0
- package/template/console/appeals/types.ts +6 -0
- package/template/console/appeals/useAppealDraftGuard.ts +12 -0
- package/template/console/assistant/AssistantWorkspace.tsx +28 -5
- package/template/console/command/CommandCentre.tsx +101 -64
- package/template/console/command/controller.ts +80 -70
- package/template/console/command/events.ts +1 -2
- package/template/console/command/presentation.ts +29 -0
- package/template/console/command/store.ts +4 -4
- package/template/console/command/types.ts +26 -21
- package/template/console/configuration/ConfigurationBuilders.tsx +101 -0
- package/template/console/configuration/ConfigurationDialog.tsx +28 -13
- package/template/console/configuration/ConfigurationWorkspace.tsx +21 -8
- package/template/console/configuration/types.ts +1 -1
- package/template/console/lib/priority.ts +13 -0
- package/template/console/operations/CaseEscalations.tsx +142 -0
- package/template/console/operations/OperationsWorkspace.tsx +2 -0
- package/template/console/operations/case-escalations.css +20 -0
- package/template/console/people/PeopleWorkspace.tsx +8 -4
- package/template/console/people/PersonAccessDialog.tsx +40 -0
- package/template/console/people/types.ts +1 -0
- package/template/console/profile/ProfileWorkspace.tsx +1 -1
- package/template/console/registry/IntegrationSetup.tsx +41 -0
- package/template/console/registry/IntegrationVerification.tsx +54 -0
- package/template/console/registry/RegistryDetailsDialog.tsx +2 -2
- package/template/console/registry/RegistryDialog.tsx +56 -27
- package/template/console/registry/RegistryWorkspace.tsx +10 -5
- package/template/console/registry/integration-verification.css +25 -0
- package/template/console/reports/CaseActions.tsx +81 -0
- package/template/console/reports/CaseContinuity.tsx +27 -0
- package/template/console/reports/CaseNotices.tsx +48 -0
- package/template/console/reports/CaseOwnership.tsx +78 -0
- package/template/console/reports/CaseReviewTasks.tsx +148 -0
- package/template/console/reports/EnrichmentFindings.tsx +148 -0
- package/template/console/reports/InvestigationBrief.tsx +29 -0
- package/template/console/reports/ReportDrawer.tsx +122 -67
- package/template/console/reports/ReportWorkspace.tsx +112 -23
- package/template/console/reports/controller.ts +125 -60
- package/template/console/reports/events.ts +9 -2
- package/template/console/reports/load-inbox.ts +17 -0
- package/template/console/reports/presence-presentation.ts +52 -0
- package/template/console/reports/private-drafts.ts +32 -0
- package/template/console/reports/store.ts +2 -2
- package/template/console/reports/types.ts +50 -2
- package/template/console/reports/use-editing-access.ts +13 -0
- package/template/console/settings/SettingsWorkspace.tsx +1 -1
- package/template/console/shell/WorkspaceShell.tsx +55 -29
- package/template/console/shell/WorkspaceUpdateNotice.tsx +48 -0
- package/template/console/shell/controller.ts +23 -16
- package/template/console/shell/navigation.ts +30 -39
- package/template/console/workflows/CreatePlaybookDialog.tsx +73 -0
- package/template/console/workflows/PlaybookExamples.tsx +160 -0
- package/template/console/workflows/PlaybookFields.tsx +109 -0
- package/template/console/workflows/PlaybookGuideEditor.tsx +125 -0
- package/template/console/workflows/PlaybookHistory.tsx +106 -0
- package/template/console/workflows/WorkflowDialogs.tsx +2 -1
- package/template/console/workflows/WorkflowStudio.tsx +108 -39
- package/template/console/workflows/WorkflowWorkspace.tsx +9 -7
- package/template/console/workflows/templates.ts +3 -3
- package/template/console/workflows/types.ts +11 -0
- package/template/migrations/0001_safest_resolve_baseline.sql +0 -1
- package/template/migrations/0002_playbook_publication_evidence.sql +36 -0
- package/template/migrations/0003_action_confirmation.sql +24 -0
- package/template/migrations/0004_notice_recovery.sql +22 -0
- package/template/migrations/0005_appeal_continuity.sql +17 -0
- package/template/migrations/0006_case_ownership.sql +23 -0
- package/template/migrations/0007_integration_verification.sql +21 -0
- package/template/migrations/0008_case_escalations.sql +135 -0
- package/template/migrations/0009_participant_continuation.sql +61 -0
- package/template/migrations/0010_appeal_corrections.sql +41 -0
- package/template/npm-shrinkwrap.json +8036 -0
- package/template/package.json +31 -22
- package/template/public/_headers +16 -1
- package/template/public/console/auth-shell.js +22151 -14847
- package/template/public/console/release.json +1 -0
- package/template/public/console/workspace-features.css +2 -0
- package/template/public/embed/embed.css +3 -0
- package/template/public/embed/embed.js +19 -1
- package/template/public/embed/index.html +2 -0
- package/template/public/index.html +9 -6
- package/template/public/participant.css +2 -0
- package/template/public/participant.html +13 -0
- package/template/public/participant.js +111 -0
- package/template/public/public-report.js +21 -1
- package/template/public/report/index.html +1 -1
- package/template/public/styles.css +599 -83
- package/template/public/workspace.css +124 -0
- package/template/scripts/reports-plan.mjs +1 -1
- package/template/shared/case-escalations.ts +31 -0
- package/template/shared/integration-progress.ts +27 -0
- package/template/shared/notice-inputs.ts +3 -0
- package/template/shared/playbook-resources.ts +14 -0
- package/template/shared/playbooks.ts +249 -0
- package/template/src/agent-executor.ts +25 -8
- package/template/src/ai-observability.ts +14 -13
- package/template/src/ai-registry-validation.ts +1 -1
- package/template/src/appeal-continuity.ts +75 -0
- package/template/src/appeal-correction-state.ts +46 -0
- package/template/src/appeal-corrections.ts +112 -0
- package/template/src/appeal-decisions.ts +85 -0
- package/template/src/assistant.ts +3 -4
- package/template/src/case-escalations.ts +223 -0
- package/template/src/case-investigator.ts +179 -0
- package/template/src/component-executor.ts +5 -4
- package/template/src/configuration-registry.ts +4 -1
- package/template/src/connection-oauth.ts +12 -3
- package/template/src/database-readiness.ts +14 -0
- package/template/src/human-tasks.ts +152 -55
- package/template/src/index.ts +191 -29
- package/template/src/installation-admin.ts +2 -1
- package/template/src/integration-progress.ts +45 -0
- package/template/src/integration-verification.ts +249 -0
- package/template/src/investigation-evidence.ts +95 -0
- package/template/src/investigator-fixture.ts +30 -0
- package/template/src/notice-delivery.ts +19 -0
- package/template/src/notice-recovery.ts +81 -0
- package/template/src/operator-scope.ts +31 -0
- package/template/src/participant-access.ts +163 -0
- package/template/src/participant-case.ts +87 -0
- package/template/src/participant-customer-access.ts +93 -0
- package/template/src/participant-recovery.ts +43 -0
- package/template/src/participant-routes.ts +67 -0
- package/template/src/platform-registry-validation.ts +25 -2
- package/template/src/platform-registry.ts +10 -7
- package/template/src/playbook-resources.ts +58 -0
- package/template/src/report-admin.ts +9 -2
- package/template/src/report-auth.ts +1 -28
- package/template/src/report-better-auth.ts +18 -6
- package/template/src/report-delivery.ts +149 -148
- package/template/src/report-email-inbound.ts +33 -22
- package/template/src/report-email-template.ts +5 -2
- package/template/src/report-email.ts +7 -30
- package/template/src/report-governance.ts +42 -45
- package/template/src/report-http.ts +37 -2
- package/template/src/report-operations.ts +30 -67
- package/template/src/report-operator-accounts.ts +7 -6
- package/template/src/report-ownership.ts +64 -0
- package/template/src/report-presence.ts +15 -9
- package/template/src/report-repository.ts +16 -7
- package/template/src/report-turnstile.ts +7 -3
- package/template/src/report-types.ts +1 -0
- package/template/src/report-workflow.ts +183 -249
- package/template/src/workflow-actions.ts +248 -156
- package/template/src/workflow-compiler.ts +63 -12
- package/template/src/workflow-dynamic-runtime.ts +20 -9
- package/template/src/workflow-effects.ts +117 -39
- package/template/src/workflow-event-transport.ts +16 -0
- package/template/src/workflow-expressions.ts +30 -0
- package/template/src/workflow-loader-code.ts +21 -0
- package/template/src/workflow-platform-types.ts +3 -0
- package/template/src/workflow-platform-validation.ts +23 -1
- package/template/src/workflow-repository.ts +131 -28
- package/template/src/workflow-runs.ts +61 -13
- package/template/src/workflow-simulator.ts +7 -4
- package/template/src/workspace-governance.ts +6 -4
- package/template/tsconfig.json +1 -0
- package/template/vite.console.config.ts +30 -2
- package/template/worker-configuration.d.ts +3 -2
package/template/src/index.ts
CHANGED
|
@@ -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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
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
|
+
}
|