create-safest-tools 0.3.0 → 0.4.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 (56) hide show
  1. package/README.md +9 -3
  2. package/package.json +1 -1
  3. package/src/cli.mjs +45 -2
  4. package/src/config.mjs +4 -3
  5. package/src/scaffold.mjs +4 -1
  6. package/src/update.mjs +265 -0
  7. package/template/README.md +5 -3
  8. package/template/console/shell/WorkspaceShell.tsx +0 -4
  9. package/template/migrations/0001_safest_resolve_baseline.sql +2446 -0
  10. package/template/package.json +4 -3
  11. package/template/public/console/auth-shell.js +3 -9
  12. package/template/public/styles.css +2 -15
  13. package/template/scripts/reports-owner-setup.mjs +22 -7
  14. package/template/scripts/reports-plan.mjs +1 -1
  15. package/template/migrations/0001_reports_foundation.sql +0 -444
  16. package/template/migrations/0002_human_report_loop.sql +0 -65
  17. package/template/migrations/0003_delivery_reliability.sql +0 -18
  18. package/template/migrations/0004_public_intake.sql +0 -14
  19. package/template/migrations/0005_operations_visibility.sql +0 -24
  20. package/template/migrations/0006_ai_governance.sql +0 -198
  21. package/template/migrations/0007_ai_release_gates.sql +0 -6
  22. package/template/migrations/0008_retention_analytics_exports.sql +0 -52
  23. package/template/migrations/0009_retention_derived_copies.sql +0 -14
  24. package/template/migrations/0010_ai_quality_controls.sql +0 -26
  25. package/template/migrations/0011_analyst_presence.sql +0 -28
  26. package/template/migrations/0012_queue_policies.sql +0 -87
  27. package/template/migrations/0013_routing_agents.sql +0 -63
  28. package/template/migrations/0014_webhook_enrichments.sql +0 -122
  29. package/template/migrations/0015_queue_owned_ai.sql +0 -55
  30. package/template/migrations/0016_operator_accounts.sql +0 -67
  31. package/template/migrations/0017_operator_profiles_and_recovery.sql +0 -33
  32. package/template/migrations/0018_platform_configuration.sql +0 -382
  33. package/template/migrations/0019_workflow_authoring_runtime.sql +0 -372
  34. package/template/migrations/0020_tasks_findings_assistant_budgets.sql +0 -427
  35. package/template/migrations/0021_abuse_evidence_operations.sql +0 -324
  36. package/template/migrations/0022_workflow_dispatch_operations.sql +0 -42
  37. package/template/migrations/0023_component_connection_execution.sql +0 -75
  38. package/template/migrations/0024_access_runtime_integrity.sql +0 -72
  39. package/template/migrations/0025_installation_timezone.sql +0 -11
  40. package/template/migrations/0026_ai_and_egress_execution_controls.sql +0 -49
  41. package/template/migrations/0027_prompt_and_ai_registry.sql +0 -37
  42. package/template/migrations/0028_step_attempt_ai_provenance.sql +0 -13
  43. package/template/migrations/0029_evidence_fetch_transport.sql +0 -5
  44. package/template/migrations/0030_evidence_dlq_incidents.sql +0 -45
  45. package/template/migrations/0031_shadow_quality_integrity.sql +0 -7
  46. package/template/migrations/0032_action_delivery_outbox.sql +0 -55
  47. package/template/migrations/0033_configuration_and_assistant_drafts.sql +0 -43
  48. package/template/migrations/0034_installation_integrations.sql +0 -31
  49. package/template/migrations/0035_workspace_governance.sql +0 -21
  50. package/template/migrations/0036_published_routing_baseline.sql +0 -15
  51. package/template/migrations/0037_builtin_phishing_specialist.sql +0 -71
  52. package/template/migrations/0038_remove_deprecated_enrichment_runtime.sql +0 -228
  53. package/template/migrations/0039_secure_reporting_channels.sql +0 -45
  54. package/template/migrations/0040_notification_only_reporting.sql +0 -24
  55. package/template/migrations/0041_better_auth_credentials.sql +0 -17
  56. package/template/migrations/0042_owner_setup_tokens.sql +0 -29
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "safest-resolve-installation",
3
- "version": "0.3.0",
4
- "safestToolsVersion": "0.3.0-resolve",
3
+ "version": "0.4.0",
4
+ "safestToolsVersion": "0.4.0-resolve",
5
5
  "private": true,
6
6
  "license": "Apache-2.0",
7
7
  "type": "module",
@@ -13,7 +13,8 @@
13
13
  "setup:plan": "node scripts/reports-deploy.mjs --plan",
14
14
  "owner:setup": "node scripts/reports-owner-setup.mjs",
15
15
  "owner:recover": "node scripts/reports-owner-setup.mjs --recover",
16
- "upgrade": "npm run backup && npm run build:console && node scripts/reports-deploy.mjs --upgrade",
16
+ "upgrade": "npm run backup && npm run upgrade:deploy",
17
+ "upgrade:deploy": "npm run build:console && node scripts/reports-deploy.mjs --upgrade",
17
18
  "upgrade:plan": "node scripts/reports-deploy.mjs --upgrade --plan",
18
19
  "backup": "node scripts/reports-backup.mjs",
19
20
  "backup:plan": "node scripts/reports-backup.mjs --plan",
@@ -17705,11 +17705,11 @@ function Aj({ session: l, currentView: r, reportCount: c, open: u, onClose: d })
17705
17705
  const m = (0, v.useMemo)(() => L0(l), [l]), S = (p, x) => {
17706
17706
  p.preventDefault(), tn(x), d();
17707
17707
  };
17708
- return /* @__PURE__ */ (0, n.jsxs)(n.Fragment, { children: [/* @__PURE__ */ (0, n.jsxs)("aside", {
17708
+ return /* @__PURE__ */ (0, n.jsxs)(n.Fragment, { children: [/* @__PURE__ */ (0, n.jsx)("aside", {
17709
17709
  id: "workspace-navigation",
17710
17710
  className: `sidebar${u ? " open" : ""}`,
17711
17711
  "aria-label": "Workspace navigation",
17712
- children: [/* @__PURE__ */ (0, n.jsx)("nav", { children: m.map((p) => /* @__PURE__ */ (0, n.jsxs)(v.Fragment, { children: [/* @__PURE__ */ (0, n.jsx)("small", { children: p.label }), p.items.map((x) => /* @__PURE__ */ (0, n.jsxs)("a", {
17712
+ children: /* @__PURE__ */ (0, n.jsx)("nav", { children: m.map((p) => /* @__PURE__ */ (0, n.jsxs)(v.Fragment, { children: [/* @__PURE__ */ (0, n.jsx)("small", { children: p.label }), p.items.map((x) => /* @__PURE__ */ (0, n.jsxs)("a", {
17713
17713
  className: x.view === r ? "active" : void 0,
17714
17714
  "data-nav": x.view,
17715
17715
  href: `#${x.view}`,
@@ -17727,13 +17727,7 @@ function Aj({ session: l, currentView: r, reportCount: c, open: u, onClose: d })
17727
17727
  children: c
17728
17728
  }) : null
17729
17729
  ]
17730
- }, x.view))] }, p.label)) }), /* @__PURE__ */ (0, n.jsxs)("div", {
17731
- className: "principle",
17732
- children: [/* @__PURE__ */ (0, n.jsx)("span", {
17733
- className: "principle-dot",
17734
- "aria-hidden": "true"
17735
- }), /* @__PURE__ */ (0, n.jsxs)("div", { children: [/* @__PURE__ */ (0, n.jsx)("strong", { children: "Customer-owned records" }), /* @__PURE__ */ (0, n.jsx)("p", { children: "Reports and decisions stay in this deployment." })] })]
17736
- })]
17730
+ }, x.view))] }, p.label)) })
17737
17731
  }), /* @__PURE__ */ (0, n.jsx)("button", {
17738
17732
  className: "nav-scrim",
17739
17733
  type: "button",
@@ -123,9 +123,6 @@ input:focus, select:focus { border-color: #68a380; box-shadow: 0 0 0 3px rgba(34
123
123
  .presence-dot.online { background: #55a271; }
124
124
  .presence-dot.viewing { background: #4d83a0; }
125
125
  .presence-dot.editing { background: #cf8a38; box-shadow: 0 0 0 3px rgba(207,138,56,.13); }
126
- .principle { margin-top: auto; padding: 14px; border-radius: 10px; color: #496054; background: #edf6f0; }
127
- .principle strong { font-size: 10px; }
128
- .principle p { margin: 7px 0 0; font-size: 9px; line-height: 1.5; }
129
126
  .content { min-width: 0; padding: clamp(25px, 4vw, 48px); }
130
127
  .page-head { max-width: 1400px; margin: 0 auto 24px; display: flex; justify-content: space-between; align-items: flex-start; gap: 16px; }
131
128
  .page-head p:last-child { margin: 0; color: var(--muted); font-size: 12px; }
@@ -327,7 +324,6 @@ td small { margin-top: 3px; color: var(--muted); }
327
324
  .workspace { grid-template-columns: 1fr; }
328
325
  .sidebar { position: static; width: auto; height: auto; border-right: 0; border-bottom: 1px solid var(--line); }
329
326
  .sidebar nav { grid-template-columns: 1fr 1fr; }
330
- .principle { display: none; }
331
327
  .metrics { grid-template-columns: 1fr 1fr; }
332
328
  .operation-metrics { grid-template-columns: 1fr 1fr; }
333
329
  .analytics-metrics { grid-template-columns: 1fr 1fr; }
@@ -388,8 +384,6 @@ td small { margin-top: 3px; color: var(--muted); }
388
384
  .sidebar nav a { min-height: 36px; color: #bdd0c8; font-size: 10px; }
389
385
  .sidebar nav a:hover { color: white; background: rgba(255,255,255,.07); }
390
386
  .sidebar nav a.active { color: #123b2b; background: #dafa72; box-shadow: 0 7px 18px rgba(0,0,0,.12); }
391
- .principle { color: #bcd2c8; background: rgba(255,255,255,.06); }
392
- .principle strong { color: white; }
393
387
  .content { min-width: 0; padding: 0; transition: margin-right .2s ease; }
394
388
  .queue-view { min-height: calc(100vh - 66px); }
395
389
  .page-head { max-width: none; min-height: 78px; margin: 0; padding: 15px 22px; align-items: center; border-bottom: 1px solid #dedfd8; background: #fbfaf7; }
@@ -498,7 +492,7 @@ body.report-open .content { margin-right: 408px; }
498
492
  @media (max-width: 900px) {
499
493
  .workspace { width: 100%; min-width: 0; grid-template-columns: minmax(0, 1fr); overflow-x: clip; }
500
494
  .sidebar { position: static; width: 100%; min-width: 0; max-width: 100vw; height: auto; padding: 10px; overflow: hidden; }
501
- .sidebar .workspace-card, .sidebar .principle { display: none; }
495
+ .sidebar .workspace-card { display: none; }
502
496
  .sidebar nav { width: 100%; min-width: 0; max-width: 100%; margin: 0; display: flex; overflow-x: auto; overscroll-behavior-x: contain; }
503
497
  .sidebar nav, .active-analyst-list { scrollbar-width: none; }
504
498
  .sidebar nav::-webkit-scrollbar, .active-analyst-list::-webkit-scrollbar { display: none; }
@@ -862,10 +856,6 @@ body.report-open .content { margin-right: 0; }
862
856
  .sidebar nav a.active { color: #102f24; background: var(--safest-lime-400); box-shadow: none; font-weight: 750; }
863
857
  .sidebar .nav-count { min-width: 22px; margin-left: 0; color: #dceae3; background: rgba(255,255,255,.10); font-size: 9px; }
864
858
  .sidebar nav a.active .nav-count { color: white; background: var(--forest-dark); }
865
- .principle { margin-top: auto; padding: 12px 10px; display: flex; align-items: flex-start; gap: 9px; color: #8eb0a2; background: rgba(255,255,255,.055); }
866
- .principle-dot { width: 8px; height: 8px; margin-top: 3px; flex: 0 0 8px; border-radius: 50%; background: #55b27b; box-shadow: 0 0 0 5px rgba(85,178,123,.12); }
867
- .principle strong { display: block; color: #eef6f1; font-size: 11px; }
868
- .principle p { margin-top: 3px; color: #8eb0a2; font-size: 10px; line-height: 1.45; }
869
859
 
870
860
  .content { background: var(--canvas); }
871
861
  .command-view { max-width: 1184px; margin: 0 auto; padding: 28px; }
@@ -947,9 +937,8 @@ body.report-open .content { margin-right: 0; }
947
937
  .topbar { grid-template-columns: 190px minmax(210px, 1fr) auto; }
948
938
  .top-workspace { display: none; }
949
939
  .sidebar { padding-inline: 10px; }
950
- .sidebar nav > small, .sidebar nav a span, .sidebar nav a b, .principle div { display: none; }
940
+ .sidebar nav > small, .sidebar nav a span, .sidebar nav a b { display: none; }
951
941
  .sidebar nav a { grid-template-columns: 1fr; justify-items: center; padding-inline: 0; }
952
- .principle { justify-content: center; }
953
942
  .command-grid { grid-template-columns: 1fr; }
954
943
  .queue-load-list { grid-template-columns: 1fr 1fr; }
955
944
  }
@@ -969,8 +958,6 @@ body.report-open .content { margin-right: 0; }
969
958
  .sidebar nav a { min-width: 0; display: grid; grid-template-columns: 20px minmax(0, 1fr) auto; justify-items: stretch; padding: 0 10px; }
970
959
  .sidebar nav a span { display: block; white-space: normal; }
971
960
  .sidebar nav a b { display: inline-block; }
972
- .principle { display: flex; }
973
- .principle div { display: block; }
974
961
  .nav-scrim { position: fixed; z-index: 65; inset: var(--product-topbar) 0 0; width: 100%; height: auto; display: block; border: 0; border-radius: 0; background: rgba(5,28,20,.33); backdrop-filter: blur(1px); }
975
962
  .nav-scrim[hidden] { display: none; }
976
963
  .content { padding: 0; }
@@ -13,11 +13,18 @@ function sqlLiteral(value) {
13
13
  return `'${String(value).replaceAll("'", "''")}'`;
14
14
  }
15
15
 
16
+ export function ownerSetupWranglerEnvironment(env = process.env) {
17
+ return { ...env, WRANGLER_LOG: "log", WRANGLER_WRITE_LOGS: "false" };
18
+ }
19
+
16
20
  function runWrangler(args) {
17
21
  return new Promise((resolvePromise, reject) => {
18
22
  const child = spawn(process.execPath, [wrangler, ...args], {
19
23
  cwd: projectRoot,
20
- env: { ...process.env, WRANGLER_LOG: "none", WRANGLER_WRITE_LOGS: "false" },
24
+ // Wrangler 4.127 emits `d1 execute --json` through its logger. Keep log
25
+ // output in the captured pipe while preventing the OAuth-backed response
26
+ // from being persisted to Wrangler's log directory.
27
+ env: ownerSetupWranglerEnvironment(),
21
28
  stdio: ["ignore", "pipe", "pipe"],
22
29
  });
23
30
  let stdout = "";
@@ -82,18 +89,26 @@ RETURNING id, email, purpose, expires_at;
82
89
  `.trim();
83
90
  }
84
91
 
85
- function executionRows(stdout) {
86
- let parsed;
87
- try { parsed = JSON.parse(stdout.trim()); } catch { throw new Error("Wrangler returned unreadable D1 JSON while creating the owner setup token"); }
88
- const results = Array.isArray(parsed) ? parsed : Array.isArray(parsed?.result) ? parsed.result : [parsed];
89
- return results.flatMap((entry) => Array.isArray(entry?.results) ? entry.results : []);
92
+ export function ownerSetupExecutionRows(output) {
93
+ const candidates = typeof output === "string" ? [output] : [output?.stdout, output?.stderr];
94
+ for (const candidate of candidates) {
95
+ if (typeof candidate !== "string" || !candidate.trim()) continue;
96
+ try {
97
+ const parsed = JSON.parse(candidate.trim());
98
+ const results = Array.isArray(parsed) ? parsed : Array.isArray(parsed?.result) ? parsed.result : [parsed];
99
+ return results.flatMap((entry) => Array.isArray(entry?.results) ? entry.results : []);
100
+ } catch {
101
+ // Try the other captured stream before reporting incompatible output.
102
+ }
103
+ }
104
+ throw new Error("Wrangler returned unreadable D1 JSON while creating the owner setup token");
90
105
  }
91
106
 
92
107
  export async function issueOwnerSetup({ config, email, purpose = "bootstrap", runner = runWrangler, now, token, id } = {}) {
93
108
  if (!config?.publicBaseUrl || !config?.owner?.email) throw new Error("owner setup requires a parsed Safest Resolve configuration");
94
109
  const record = createOwnerSetupRecord({ email: email || config.owner.email, purpose, now, token, id });
95
110
  const result = await runner(["d1", "execute", "DB", "--remote", "--command", ownerSetupSql(record), "--json", "--yes"]);
96
- const created = executionRows(result.stdout).find((row) => row?.id === record.id);
111
+ const created = ownerSetupExecutionRows(result).find((row) => row?.id === record.id);
97
112
  if (!created) {
98
113
  throw new Error(purpose === "bootstrap"
99
114
  ? "An active infrastructure owner already exists. Use `npm run owner:recover` for break-glass access."
@@ -178,7 +178,7 @@ export function buildReportsPlan(config) {
178
178
  "Onboard the sender domain to Cloudflare Email Service before deployment.",
179
179
  "Initialize local secrets without committing them.",
180
180
  "Create D1, four private R2 buckets, and the three Queues in the customer account.",
181
- "Apply all report migrations before accepting traffic.",
181
+ "Apply the D1 schema baseline and any later forward-only migrations before accepting traffic.",
182
182
  `Deploy the Worker, assets, Worker Loader, Dynamic Workflow, presence Durable Object, Queue consumers, cron, Workers AI binding${customDomain ? `, and ${customDomain} Custom Domain` : ""}.`,
183
183
  "Open the generated one-time owner setup link, create the owner account, verify required setup features and authenticated /ready, invite an administrator and analyst, then verify report intake, routing, and delivery recovery.",
184
184
  ],
@@ -1,444 +0,0 @@
1
- PRAGMA foreign_keys = ON;
2
-
3
- CREATE TABLE installations (
4
- id TEXT PRIMARY KEY,
5
- display_name TEXT NOT NULL,
6
- default_locale TEXT NOT NULL DEFAULT 'en',
7
- anonymous_reporting_enabled INTEGER NOT NULL DEFAULT 0 CHECK (anonymous_reporting_enabled IN (0, 1)),
8
- created_at TEXT NOT NULL,
9
- updated_at TEXT NOT NULL
10
- );
11
-
12
- CREATE TABLE application_integrations (
13
- id TEXT PRIMARY KEY,
14
- installation_id TEXT NOT NULL REFERENCES installations(id),
15
- name TEXT NOT NULL,
16
- key_id TEXT NOT NULL UNIQUE,
17
- allowed_origins_json TEXT NOT NULL DEFAULT '[]',
18
- enabled INTEGER NOT NULL DEFAULT 1 CHECK (enabled IN (0, 1)),
19
- created_at TEXT NOT NULL,
20
- updated_at TEXT NOT NULL
21
- );
22
-
23
- CREATE TABLE report_form_versions (
24
- id TEXT PRIMARY KEY,
25
- installation_id TEXT NOT NULL REFERENCES installations(id),
26
- form_key TEXT NOT NULL,
27
- version INTEGER NOT NULL CHECK (version > 0),
28
- title TEXT NOT NULL,
29
- description TEXT NOT NULL DEFAULT '',
30
- target_types_json TEXT NOT NULL,
31
- reasons_json TEXT NOT NULL,
32
- fields_json TEXT NOT NULL,
33
- consent_notice TEXT NOT NULL,
34
- published_at TEXT,
35
- retired_at TEXT,
36
- created_by TEXT NOT NULL,
37
- created_at TEXT NOT NULL,
38
- UNIQUE (installation_id, form_key, version)
39
- );
40
-
41
- CREATE TABLE policy_versions (
42
- id TEXT PRIMARY KEY,
43
- installation_id TEXT NOT NULL REFERENCES installations(id),
44
- policy_key TEXT NOT NULL,
45
- version INTEGER NOT NULL CHECK (version > 0),
46
- title TEXT NOT NULL,
47
- rules_json TEXT NOT NULL,
48
- published_at TEXT,
49
- created_by TEXT NOT NULL,
50
- created_at TEXT NOT NULL,
51
- UNIQUE (installation_id, policy_key, version)
52
- );
53
-
54
- CREATE TABLE queue_definitions (
55
- id TEXT PRIMARY KEY,
56
- installation_id TEXT NOT NULL REFERENCES installations(id),
57
- queue_key TEXT NOT NULL,
58
- name TEXT NOT NULL,
59
- queue_kind TEXT NOT NULL CHECK (queue_kind IN ('human', 'ai', 'appeal', 'operations')),
60
- enabled INTEGER NOT NULL DEFAULT 1 CHECK (enabled IN (0, 1)),
61
- created_at TEXT NOT NULL,
62
- updated_at TEXT NOT NULL,
63
- UNIQUE (installation_id, queue_key)
64
- );
65
-
66
- CREATE TABLE queue_versions (
67
- id TEXT PRIMARY KEY,
68
- queue_id TEXT NOT NULL REFERENCES queue_definitions(id),
69
- version INTEGER NOT NULL CHECK (version > 0),
70
- mode TEXT NOT NULL CHECK (mode IN ('off', 'shadow', 'assist', 'autonomous_low_priority')),
71
- priority INTEGER NOT NULL DEFAULT 50 CHECK (priority BETWEEN 0 AND 100),
72
- response_sla_minutes INTEGER,
73
- resolution_sla_minutes INTEGER,
74
- claim_ttl_minutes INTEGER NOT NULL DEFAULT 15 CHECK (claim_ttl_minutes BETWEEN 1 AND 1440),
75
- fallback_queue_id TEXT REFERENCES queue_definitions(id),
76
- configuration_json TEXT NOT NULL DEFAULT '{}',
77
- published_at TEXT,
78
- created_by TEXT NOT NULL,
79
- created_at TEXT NOT NULL,
80
- UNIQUE (queue_id, version)
81
- );
82
-
83
- CREATE TABLE routing_rule_versions (
84
- id TEXT PRIMARY KEY,
85
- installation_id TEXT NOT NULL REFERENCES installations(id),
86
- version INTEGER NOT NULL CHECK (version > 0),
87
- rules_json TEXT NOT NULL,
88
- fixture_results_json TEXT NOT NULL,
89
- published_at TEXT,
90
- created_by TEXT NOT NULL,
91
- created_at TEXT NOT NULL,
92
- UNIQUE (installation_id, version)
93
- );
94
-
95
- CREATE TABLE reports (
96
- id TEXT PRIMARY KEY,
97
- public_reference TEXT NOT NULL UNIQUE,
98
- installation_id TEXT NOT NULL REFERENCES installations(id),
99
- integration_id TEXT NOT NULL REFERENCES application_integrations(id),
100
- idempotency_key TEXT NOT NULL,
101
- source TEXT NOT NULL CHECK (source IN ('signed_widget', 'public_page', 'server_api')),
102
- form_version_id TEXT REFERENCES report_form_versions(id),
103
- policy_version_id TEXT REFERENCES policy_versions(id),
104
- routing_rule_version_id TEXT REFERENCES routing_rule_versions(id),
105
- queue_id TEXT REFERENCES queue_definitions(id),
106
- state TEXT NOT NULL CHECK (state IN (
107
- 'received', 'human_review', 'ai_review', 'awaiting_reporter', 'resolved_by_human',
108
- 'resolved_by_ai', 'human_review_requested', 'appealed', 'closed'
109
- )),
110
- priority INTEGER NOT NULL DEFAULT 50 CHECK (priority BETWEEN 0 AND 100),
111
- reason_code TEXT NOT NULL,
112
- locale TEXT NOT NULL,
113
- consent_notice TEXT NOT NULL,
114
- consented_at TEXT NOT NULL,
115
- submitted_at TEXT NOT NULL,
116
- received_at TEXT NOT NULL,
117
- updated_at TEXT NOT NULL,
118
- due_at TEXT,
119
- resolved_at TEXT,
120
- routing_status TEXT NOT NULL DEFAULT 'pending' CHECK (routing_status IN ('pending', 'enqueued', 'processing', 'complete', 'failed')),
121
- routing_attempts INTEGER NOT NULL DEFAULT 0 CHECK (routing_attempts >= 0),
122
- routing_error_code TEXT,
123
- UNIQUE (integration_id, idempotency_key)
124
- );
125
-
126
- CREATE INDEX reports_queue_state_priority_idx
127
- ON reports(queue_id, state, priority DESC, received_at ASC, id ASC);
128
- CREATE INDEX reports_routing_status_idx
129
- ON reports(routing_status, received_at ASC, id ASC);
130
- CREATE INDEX reports_target_time_idx
131
- ON reports(installation_id, received_at DESC, id DESC);
132
-
133
- CREATE TABLE report_participants (
134
- id TEXT PRIMARY KEY,
135
- report_id TEXT NOT NULL REFERENCES reports(id) ON DELETE CASCADE,
136
- audience TEXT NOT NULL CHECK (audience IN ('reporter', 'affected_user')),
137
- reference TEXT,
138
- contact_mode TEXT NOT NULL CHECK (contact_mode IN ('portal', 'customer_notification', 'none')),
139
- contact_ciphertext TEXT,
140
- created_at TEXT NOT NULL,
141
- UNIQUE (report_id, audience)
142
- );
143
-
144
- CREATE TABLE report_targets (
145
- id TEXT PRIMARY KEY,
146
- report_id TEXT NOT NULL REFERENCES reports(id) ON DELETE CASCADE,
147
- target_type TEXT NOT NULL,
148
- target_reference TEXT NOT NULL,
149
- owner_reference TEXT,
150
- customer_url TEXT,
151
- signed_facts_json TEXT NOT NULL DEFAULT '{}',
152
- created_at TEXT NOT NULL,
153
- UNIQUE (report_id, target_type, target_reference)
154
- );
155
-
156
- CREATE INDEX report_targets_reference_idx
157
- ON report_targets(target_type, target_reference, report_id);
158
-
159
- CREATE TABLE report_answers (
160
- report_id TEXT NOT NULL REFERENCES reports(id) ON DELETE CASCADE,
161
- field_key TEXT NOT NULL,
162
- value_json TEXT NOT NULL,
163
- created_at TEXT NOT NULL,
164
- PRIMARY KEY (report_id, field_key)
165
- );
166
-
167
- CREATE TABLE evidence_references (
168
- id TEXT PRIMARY KEY,
169
- report_id TEXT NOT NULL REFERENCES reports(id) ON DELETE CASCADE,
170
- evidence_kind TEXT NOT NULL CHECK (evidence_kind IN ('allegation', 'signed_fact', 'customer_reference', 'fetched_record', 'snapshot')),
171
- reference TEXT NOT NULL,
172
- availability TEXT NOT NULL DEFAULT 'available' CHECK (availability IN ('available', 'unavailable', 'deleted', 'changed')),
173
- retention_class TEXT NOT NULL DEFAULT 'reference',
174
- created_at TEXT NOT NULL,
175
- UNIQUE (report_id, evidence_kind, reference)
176
- );
177
-
178
- CREATE TABLE report_state_history (
179
- id TEXT PRIMARY KEY,
180
- report_id TEXT NOT NULL REFERENCES reports(id) ON DELETE CASCADE,
181
- background_job_id TEXT UNIQUE REFERENCES background_jobs(id),
182
- from_state TEXT,
183
- to_state TEXT NOT NULL,
184
- reason_code TEXT NOT NULL,
185
- actor_type TEXT NOT NULL,
186
- actor_id TEXT NOT NULL,
187
- created_at TEXT NOT NULL
188
- );
189
-
190
- CREATE INDEX report_state_history_report_idx
191
- ON report_state_history(report_id, created_at ASC, id ASC);
192
-
193
- CREATE TABLE portal_sessions (
194
- id TEXT PRIMARY KEY,
195
- report_id TEXT NOT NULL REFERENCES reports(id) ON DELETE CASCADE,
196
- audience TEXT NOT NULL CHECK (audience IN ('reporter', 'affected_user')),
197
- token_hash TEXT NOT NULL UNIQUE,
198
- expires_at TEXT NOT NULL,
199
- revoked_at TEXT,
200
- created_at TEXT NOT NULL,
201
- last_used_at TEXT
202
- );
203
-
204
- CREATE INDEX portal_sessions_report_idx ON portal_sessions(report_id, audience, expires_at);
205
-
206
- CREATE TABLE context_token_uses (
207
- jti TEXT PRIMARY KEY,
208
- integration_id TEXT NOT NULL REFERENCES application_integrations(id),
209
- report_id TEXT NOT NULL UNIQUE REFERENCES reports(id) ON DELETE CASCADE,
210
- used_at TEXT NOT NULL
211
- );
212
-
213
- CREATE TABLE case_messages (
214
- id TEXT PRIMARY KEY,
215
- report_id TEXT NOT NULL REFERENCES reports(id) ON DELETE CASCADE,
216
- audience TEXT NOT NULL CHECK (audience IN ('internal', 'reporter', 'affected_user')),
217
- direction TEXT NOT NULL CHECK (direction IN ('inbound', 'outbound', 'internal')),
218
- sender_type TEXT NOT NULL CHECK (sender_type IN ('reporter', 'affected_user', 'human', 'ai', 'system')),
219
- sender_id TEXT NOT NULL,
220
- body TEXT NOT NULL,
221
- template_version_id TEXT,
222
- automated INTEGER NOT NULL DEFAULT 0 CHECK (automated IN (0, 1)),
223
- delivery_state TEXT NOT NULL DEFAULT 'not_required' CHECK (delivery_state IN ('not_required', 'queued', 'delivered', 'failed', 'suppressed')),
224
- created_at TEXT NOT NULL
225
- );
226
-
227
- CREATE INDEX case_messages_report_audience_idx
228
- ON case_messages(report_id, audience, created_at ASC, id ASC);
229
-
230
- CREATE TABLE review_claims (
231
- report_id TEXT PRIMARY KEY REFERENCES reports(id) ON DELETE CASCADE,
232
- reviewer_id TEXT NOT NULL,
233
- claimed_at TEXT NOT NULL,
234
- expires_at TEXT NOT NULL,
235
- version INTEGER NOT NULL DEFAULT 1 CHECK (version > 0)
236
- );
237
-
238
- CREATE TABLE decisions (
239
- id TEXT PRIMARY KEY,
240
- report_id TEXT NOT NULL REFERENCES reports(id),
241
- decision_code TEXT NOT NULL,
242
- policy_version_id TEXT NOT NULL REFERENCES policy_versions(id),
243
- policy_code TEXT NOT NULL,
244
- rationale TEXT NOT NULL,
245
- evidence_references_json TEXT NOT NULL,
246
- maker_type TEXT NOT NULL CHECK (maker_type IN ('human', 'ai')),
247
- maker_id TEXT NOT NULL,
248
- queue_version_id TEXT REFERENCES queue_versions(id),
249
- ai_run_id TEXT,
250
- proposed_action_code TEXT,
251
- supersedes_decision_id TEXT REFERENCES decisions(id),
252
- created_at TEXT NOT NULL
253
- );
254
-
255
- CREATE INDEX decisions_report_idx ON decisions(report_id, created_at DESC, id DESC);
256
-
257
- CREATE TABLE notices (
258
- id TEXT PRIMARY KEY,
259
- report_id TEXT NOT NULL REFERENCES reports(id),
260
- decision_id TEXT REFERENCES decisions(id),
261
- audience TEXT NOT NULL CHECK (audience IN ('reporter', 'affected_user')),
262
- template_version_id TEXT NOT NULL,
263
- rendered_body TEXT NOT NULL,
264
- delivery_state TEXT NOT NULL CHECK (delivery_state IN ('queued', 'delivered', 'failed', 'suppressed')),
265
- created_at TEXT NOT NULL,
266
- updated_at TEXT NOT NULL
267
- );
268
-
269
- CREATE TABLE action_outbox (
270
- id TEXT PRIMARY KEY,
271
- report_id TEXT NOT NULL REFERENCES reports(id),
272
- decision_id TEXT NOT NULL REFERENCES decisions(id),
273
- idempotency_key TEXT NOT NULL UNIQUE,
274
- action_code TEXT NOT NULL,
275
- target_reference TEXT NOT NULL,
276
- payload_json TEXT NOT NULL,
277
- status TEXT NOT NULL CHECK (status IN ('pending', 'delivering', 'accepted', 'rejected', 'already_applied', 'failed')),
278
- attempt_count INTEGER NOT NULL DEFAULT 0 CHECK (attempt_count >= 0),
279
- next_attempt_at TEXT NOT NULL,
280
- last_error_code TEXT,
281
- created_at TEXT NOT NULL,
282
- updated_at TEXT NOT NULL
283
- );
284
-
285
- CREATE INDEX action_outbox_due_idx ON action_outbox(status, next_attempt_at, id);
286
-
287
- CREATE TABLE action_attempts (
288
- id TEXT PRIMARY KEY,
289
- action_id TEXT NOT NULL REFERENCES action_outbox(id) ON DELETE CASCADE,
290
- attempt_number INTEGER NOT NULL CHECK (attempt_number > 0),
291
- response_class TEXT NOT NULL,
292
- response_status INTEGER,
293
- error_code TEXT,
294
- started_at TEXT NOT NULL,
295
- finished_at TEXT NOT NULL,
296
- UNIQUE (action_id, attempt_number)
297
- );
298
-
299
- CREATE TABLE notification_outbox (
300
- id TEXT PRIMARY KEY,
301
- notice_id TEXT NOT NULL REFERENCES notices(id),
302
- idempotency_key TEXT NOT NULL UNIQUE,
303
- channel TEXT NOT NULL CHECK (channel IN ('customer_webhook', 'email')),
304
- payload_json TEXT NOT NULL,
305
- status TEXT NOT NULL CHECK (status IN ('pending', 'delivering', 'delivered', 'failed', 'suppressed')),
306
- attempt_count INTEGER NOT NULL DEFAULT 0 CHECK (attempt_count >= 0),
307
- next_attempt_at TEXT NOT NULL,
308
- last_error_code TEXT,
309
- created_at TEXT NOT NULL,
310
- updated_at TEXT NOT NULL
311
- );
312
-
313
- CREATE INDEX notification_outbox_due_idx ON notification_outbox(status, next_attempt_at, id);
314
-
315
- CREATE TABLE notification_attempts (
316
- id TEXT PRIMARY KEY,
317
- notification_id TEXT NOT NULL REFERENCES notification_outbox(id) ON DELETE CASCADE,
318
- attempt_number INTEGER NOT NULL CHECK (attempt_number > 0),
319
- response_class TEXT NOT NULL,
320
- response_status INTEGER,
321
- error_code TEXT,
322
- started_at TEXT NOT NULL,
323
- finished_at TEXT NOT NULL,
324
- UNIQUE (notification_id, attempt_number)
325
- );
326
-
327
- CREATE TABLE appeals (
328
- id TEXT PRIMARY KEY,
329
- report_id TEXT NOT NULL REFERENCES reports(id),
330
- decision_id TEXT NOT NULL REFERENCES decisions(id),
331
- appellant_audience TEXT NOT NULL CHECK (appellant_audience IN ('reporter', 'affected_user')),
332
- reason TEXT NOT NULL,
333
- state TEXT NOT NULL CHECK (state IN ('submitted', 'human_review', 'upheld', 'modified', 'reversed')),
334
- queue_id TEXT NOT NULL REFERENCES queue_definitions(id),
335
- submitted_at TEXT NOT NULL,
336
- resolved_at TEXT,
337
- UNIQUE (decision_id, appellant_audience)
338
- );
339
-
340
- CREATE INDEX appeals_queue_state_idx ON appeals(queue_id, state, submitted_at ASC, id ASC);
341
-
342
- CREATE TABLE appeal_decisions (
343
- id TEXT PRIMARY KEY,
344
- appeal_id TEXT NOT NULL UNIQUE REFERENCES appeals(id),
345
- outcome TEXT NOT NULL CHECK (outcome IN ('upheld', 'modified', 'reversed')),
346
- rationale TEXT NOT NULL,
347
- reviewer_id TEXT NOT NULL,
348
- corrective_action_id TEXT REFERENCES action_outbox(id),
349
- created_at TEXT NOT NULL
350
- );
351
-
352
- CREATE TABLE background_jobs (
353
- id TEXT PRIMARY KEY,
354
- report_id TEXT REFERENCES reports(id),
355
- job_type TEXT NOT NULL CHECK (job_type IN ('route_report', 'deliver_action', 'deliver_notification', 'resume_ai', 'retention')),
356
- idempotency_key TEXT NOT NULL UNIQUE,
357
- state TEXT NOT NULL CHECK (state IN ('pending', 'enqueued', 'processing', 'complete', 'failed', 'dead_letter')),
358
- payload_json TEXT NOT NULL,
359
- attempt_count INTEGER NOT NULL DEFAULT 0 CHECK (attempt_count >= 0),
360
- available_at TEXT NOT NULL,
361
- last_error_code TEXT,
362
- created_at TEXT NOT NULL,
363
- updated_at TEXT NOT NULL
364
- );
365
-
366
- CREATE INDEX background_jobs_reconcile_idx ON background_jobs(state, available_at, id);
367
-
368
- CREATE TABLE audit_entries (
369
- sequence INTEGER PRIMARY KEY AUTOINCREMENT,
370
- id TEXT NOT NULL UNIQUE,
371
- action TEXT NOT NULL,
372
- actor_type TEXT NOT NULL,
373
- actor_id TEXT NOT NULL,
374
- target_type TEXT NOT NULL,
375
- target_id TEXT NOT NULL,
376
- details_json TEXT NOT NULL,
377
- previous_hash TEXT NOT NULL UNIQUE,
378
- entry_hash TEXT NOT NULL UNIQUE,
379
- created_at TEXT NOT NULL
380
- );
381
-
382
- CREATE INDEX audit_entries_target_idx
383
- ON audit_entries(target_type, target_id, sequence DESC);
384
-
385
- CREATE TABLE daily_aggregates (
386
- aggregate_date TEXT NOT NULL,
387
- metric_key TEXT NOT NULL,
388
- dimension_key TEXT NOT NULL,
389
- dimension_value TEXT NOT NULL,
390
- value INTEGER NOT NULL DEFAULT 0,
391
- updated_at TEXT NOT NULL,
392
- PRIMARY KEY (aggregate_date, metric_key, dimension_key, dimension_value)
393
- );
394
-
395
- INSERT INTO installations (
396
- id, display_name, default_locale, anonymous_reporting_enabled, created_at, updated_at
397
- ) VALUES (
398
- 'default', 'Safest Resolve', 'en', 0, strftime('%Y-%m-%dT%H:%M:%fZ', 'now'), strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
399
- );
400
-
401
- INSERT INTO application_integrations (
402
- id, installation_id, name, key_id, allowed_origins_json, enabled, created_at, updated_at
403
- ) VALUES (
404
- 'default', 'default', 'Default integration', 'default', '["http://localhost:8787","http://localhost:8788"]', 1,
405
- strftime('%Y-%m-%dT%H:%M:%fZ', 'now'), strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
406
- );
407
-
408
- INSERT INTO report_form_versions (
409
- id, installation_id, form_key, version, title, description, target_types_json, reasons_json,
410
- fields_json, consent_notice, published_at, created_by, created_at
411
- ) VALUES (
412
- 'general-v1', 'default', 'general', 1, 'Report a safety concern',
413
- 'Tell the service owner about abuse or a safety concern.',
414
- '["message","profile","listing","account","file","transaction","service"]',
415
- '[{"code":"harassment","label":"Harassment"},{"code":"spam","label":"Spam or scam"},{"code":"threat","label":"Threat or immediate danger"},{"code":"other","label":"Something else"}]',
416
- '[{"key":"details","label":"What happened?","type":"long_text","required":true,"max_length":4000}]',
417
- 'Your report will be shared with authorized safety reviewers and handled according to the service privacy policy.',
418
- strftime('%Y-%m-%dT%H:%M:%fZ', 'now'), 'system', strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
419
- );
420
-
421
- INSERT INTO policy_versions (
422
- id, installation_id, policy_key, version, title, rules_json, published_at, created_by, created_at
423
- ) VALUES (
424
- 'general-policy-v1', 'default', 'general', 1, 'General safety policy',
425
- '[{"code":"GENERAL-1","title":"Customer review required"}]',
426
- strftime('%Y-%m-%dT%H:%M:%fZ', 'now'), 'system', strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
427
- );
428
-
429
- INSERT INTO queue_definitions (id, installation_id, queue_key, name, queue_kind, created_at, updated_at) VALUES
430
- ('urgent-human', 'default', 'urgent-human', 'Urgent human', 'human', strftime('%Y-%m-%dT%H:%M:%fZ', 'now'), strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
431
- ('standard-human', 'default', 'standard-human', 'Standard human', 'human', strftime('%Y-%m-%dT%H:%M:%fZ', 'now'), strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
432
- ('low-priority-ai', 'default', 'low-priority-ai', 'Low-priority AI', 'ai', strftime('%Y-%m-%dT%H:%M:%fZ', 'now'), strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
433
- ('appeals', 'default', 'appeals', 'Appeals', 'appeal', strftime('%Y-%m-%dT%H:%M:%fZ', 'now'), strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
434
- ('operations', 'default', 'operations', 'Operations', 'operations', strftime('%Y-%m-%dT%H:%M:%fZ', 'now'), strftime('%Y-%m-%dT%H:%M:%fZ', 'now'));
435
-
436
- INSERT INTO queue_versions (
437
- id, queue_id, version, mode, priority, response_sla_minutes, resolution_sla_minutes,
438
- claim_ttl_minutes, fallback_queue_id, configuration_json, published_at, created_by, created_at
439
- ) VALUES
440
- ('urgent-human-v1', 'urgent-human', 1, 'off', 100, 15, 240, 15, NULL, '{}', strftime('%Y-%m-%dT%H:%M:%fZ', 'now'), 'system', strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
441
- ('standard-human-v1', 'standard-human', 1, 'off', 50, 1440, 10080, 15, NULL, '{}', strftime('%Y-%m-%dT%H:%M:%fZ', 'now'), 'system', strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
442
- ('low-priority-ai-v1', 'low-priority-ai', 1, 'shadow', 20, 1440, 10080, 15, 'standard-human', '{"allowed_outcomes":[],"protected_categories":[],"daily_volume_budget":0}', strftime('%Y-%m-%dT%H:%M:%fZ', 'now'), 'system', strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
443
- ('appeals-v1', 'appeals', 1, 'off', 60, 1440, 10080, 30, NULL, '{}', strftime('%Y-%m-%dT%H:%M:%fZ', 'now'), 'system', strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
444
- ('operations-v1', 'operations', 1, 'off', 80, 60, 1440, 30, NULL, '{}', strftime('%Y-%m-%dT%H:%M:%fZ', 'now'), 'system', strftime('%Y-%m-%dT%H:%M:%fZ', 'now'));