create-safest-tools 0.5.3 → 0.6.1

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 (50) hide show
  1. package/README.md +2 -2
  2. package/package.json +1 -1
  3. package/src/update.mjs +10 -1
  4. package/template/README.md +1 -1
  5. package/template/console/README.md +2 -2
  6. package/template/console/command/CommandCentre.tsx +94 -63
  7. package/template/console/command/controller.ts +71 -65
  8. package/template/console/command/events.ts +1 -2
  9. package/template/console/command/presentation.ts +34 -0
  10. package/template/console/command/store.ts +3 -4
  11. package/template/console/command/types.ts +25 -21
  12. package/template/console/registry/RegistryDetailsDialog.tsx +249 -0
  13. package/template/console/registry/RegistryDialog.tsx +120 -67
  14. package/template/console/registry/RegistryWorkspace.tsx +75 -55
  15. package/template/console/registry/types.ts +23 -1
  16. package/template/console/reports/EnrichmentFindings.tsx +148 -0
  17. package/template/console/reports/ReportDrawer.tsx +20 -16
  18. package/template/console/reports/ReportWorkspace.tsx +104 -14
  19. package/template/console/reports/controller.ts +31 -13
  20. package/template/console/reports/presence-presentation.ts +52 -0
  21. package/template/console/reports/store.ts +1 -1
  22. package/template/console/reports/types.ts +9 -0
  23. package/template/console/shell/controller.ts +1 -8
  24. package/template/console/shell/navigation.ts +1 -1
  25. package/template/migrations/0001_safest_resolve_baseline.sql +0 -1
  26. package/template/package.json +8 -7
  27. package/template/public/console/auth-shell.js +9166 -8180
  28. package/template/public/styles.css +472 -81
  29. package/template/src/component-executor.ts +6 -3
  30. package/template/src/connection-oauth.ts +12 -3
  31. package/template/src/database-readiness.ts +9 -0
  32. package/template/src/index.ts +13 -11
  33. package/template/src/platform-registry-validation.ts +25 -2
  34. package/template/src/platform-registry.ts +29 -16
  35. package/template/src/report-better-auth.ts +18 -6
  36. package/template/src/report-delivery.ts +20 -26
  37. package/template/src/report-email-inbound.ts +43 -25
  38. package/template/src/report-email.ts +7 -30
  39. package/template/src/report-http.ts +37 -2
  40. package/template/src/report-operations.ts +15 -5
  41. package/template/src/report-operator-accounts.ts +7 -6
  42. package/template/src/report-presence.ts +15 -9
  43. package/template/src/report-repository.ts +6 -1
  44. package/template/src/report-turnstile.ts +7 -3
  45. package/template/src/report-workflow.ts +34 -7
  46. package/template/src/workflow-dynamic-runtime.ts +2 -2
  47. package/template/src/workflow-effects.ts +22 -21
  48. package/template/src/workflow-expressions.ts +30 -0
  49. package/template/src/workflow-loader-code.ts +21 -0
  50. package/template/src/workflow-runs.ts +52 -9
@@ -1,49 +1,54 @@
1
1
  import type { WorkspaceView } from "../shell/types";
2
2
 
3
- export type CommandStatusTone = "neutral" | "warning" | "danger";
3
+ export type CommandStatusTone = "neutral" | "positive" | "warning" | "danger";
4
4
 
5
5
  export type CommandAction =
6
6
  | { type: "report"; reportId: string }
7
7
  | { type: "view"; view: WorkspaceView };
8
8
 
9
9
  export interface CommandMetricSnapshot {
10
- openReports: number;
11
- needsHuman: number;
12
- activeRuns: number;
13
- unresolvedIncidents: number;
10
+ awaitingReview: number;
11
+ urgent: number;
12
+ unassigned: number;
13
+ onlineReviewers: number;
14
+ oldestWaitMinutes: number;
14
15
  }
15
16
 
16
17
  export interface CommandAttentionItem {
17
18
  id: string;
18
- icon: string;
19
19
  title: string;
20
- detail: string;
21
- meta: string;
22
- urgent: boolean;
20
+ reference: string;
21
+ queueName: string;
22
+ age: string;
23
+ ownership: string;
24
+ priorityLabel: string;
25
+ slaLabel: string;
26
+ tone: CommandStatusTone;
23
27
  action: CommandAction;
24
28
  }
25
29
 
26
30
  export interface CommandQueueLoad {
27
31
  id: string;
28
32
  name: string;
29
- targetMinutes: number;
33
+ targetMinutes: number | null;
30
34
  openCount: number;
35
+ unassignedCount: number;
36
+ oldestMinutes: number;
37
+ breachedCount: number;
31
38
  pressure: number;
39
+ tone: CommandStatusTone;
32
40
  }
33
41
 
34
- export interface CommandRunSummary {
35
- id: string;
36
- title: string;
37
- detail: string;
38
- status: string;
39
- tone: CommandStatusTone;
40
- age: string;
41
- action: CommandAction;
42
+ export interface CommandSystemHealth {
43
+ issueCount: number;
44
+ incidentCount: number;
45
+ workflowIssueCount: number;
46
+ deliveryFailureCount: number;
47
+ activeRunCount: number;
42
48
  }
43
49
 
44
50
  export interface CommandPermissions {
45
51
  useAssistant: boolean;
46
- manageWorkflows: boolean;
47
52
  viewAudit: boolean;
48
53
  manageInstallation: boolean;
49
54
  }
@@ -51,11 +56,10 @@ export interface CommandPermissions {
51
56
  export interface CommandSnapshot {
52
57
  loading: boolean;
53
58
  generatedAt: string;
54
- firstName: string;
55
59
  metrics: CommandMetricSnapshot;
56
60
  attention: CommandAttentionItem[];
57
61
  queueLoads: CommandQueueLoad[];
58
- recentRuns: CommandRunSummary[];
62
+ systemHealth: CommandSystemHealth;
59
63
  incompleteSetupCount: number;
60
64
  permissions: CommandPermissions;
61
65
  }
@@ -0,0 +1,249 @@
1
+ import { useEffect, useMemo, useRef, useState } from "react";
2
+ import { readable } from "../lib/format";
3
+ import { ApiRequestError, errorMessage, mutationHeaders, requestJson } from "../lib/http";
4
+ import type {
5
+ ComponentSummary,
6
+ ConnectionSummary,
7
+ RegistryDependenciesResponse,
8
+ RegistryDetailResponse,
9
+ RegistryKind,
10
+ RegistryValidation,
11
+ RegistryValidationIssue,
12
+ RegistryValidationResponse,
13
+ } from "./types";
14
+
15
+ type RegistryItem = ComponentSummary | ConnectionSummary;
16
+
17
+ interface RegistryDetailsDialogProps {
18
+ item: RegistryItem | null;
19
+ kind: RegistryKind;
20
+ canManage: boolean;
21
+ onClose: () => void;
22
+ onChanged: (message: string) => void;
23
+ }
24
+
25
+ const validationLabels: Record<string, string> = {
26
+ active_version_missing: "Publish an active version.",
27
+ allowed_methods_empty: "Allow at least one HTTP method.",
28
+ allowed_paths_empty: "Allow at least one request path.",
29
+ connection_version_missing: "Choose a published connection for this tool.",
30
+ connection_version_unavailable: "Choose an active, published connection version.",
31
+ destination_invalid: "Configure exactly one HTTPS endpoint or Worker service binding.",
32
+ fixture_missing: "Add at least one contract example.",
33
+ input_schema_invalid: "Fix the input schema.",
34
+ output_schema_invalid: "Fix the output schema.",
35
+ passing_test_missing: "Run and pass at least one contract test.",
36
+ secret_reference_missing: "Configure a Worker secret binding for the selected authentication method.",
37
+ };
38
+
39
+ function issueCode(issue: RegistryValidationIssue | string): string {
40
+ return typeof issue === "string" ? issue : issue.code || issue.message;
41
+ }
42
+
43
+ function issueMessage(issue: RegistryValidationIssue | string): string {
44
+ if (typeof issue !== "string") return issue.message;
45
+ return validationLabels[issue] || readable(issue);
46
+ }
47
+
48
+ function text(value: unknown, fallback = "—"): string {
49
+ return typeof value === "string" && value ? value : fallback;
50
+ }
51
+
52
+ function number(value: unknown): number {
53
+ return Number.isFinite(Number(value)) ? Number(value) : 0;
54
+ }
55
+
56
+ function activeVersion(detail: RegistryDetailResponse | null, item: RegistryItem | null, kind: RegistryKind): Record<string, unknown> | null {
57
+ const versions = kind === "component" ? detail?.component?.versions : detail?.connection?.versions;
58
+ if (!versions?.length) return null;
59
+ return versions.find((version) => version.id === item?.activeVersionId) || versions[0];
60
+ }
61
+
62
+ function dependencyRows(dependencies: RegistryDependenciesResponse["dependencies"] | null): Array<{ type: string; name: string; version: string }> {
63
+ if (!dependencies) return [];
64
+ return [
65
+ ...(dependencies.workflows || []).map((entry) => ({ type: "Workflow", name: text(entry.name, "Unnamed workflow"), version: `v${number(entry.version) || 1}` })),
66
+ ...(dependencies.components || []).map((entry) => ({ type: "Tool", name: text(entry.name, "Unnamed tool"), version: `v${number(entry.version) || 1}` })),
67
+ ...(dependencies.modelAliases || []).map((entry) => ({ type: "AI model", name: text(entry.name, "Unnamed model"), version: `v${number(entry.version) || 1}` })),
68
+ ];
69
+ }
70
+
71
+ function VerificationPanel({ validation, kind }: { validation: RegistryValidation; kind: RegistryKind }) {
72
+ const errors = validation.errors || [];
73
+ const failed = new Set(errors.map(issueCode));
74
+ const componentChecks = [
75
+ ["active_version_missing", "Active version is published"],
76
+ ["input_schema_invalid", "Input contract is valid"],
77
+ ["output_schema_invalid", "Output contract is valid"],
78
+ ["connection_version_missing", "Required connection is selected"],
79
+ ["connection_version_unavailable", "Selected connection is available"],
80
+ ["fixture_missing", "Contract example is present"],
81
+ ["passing_test_missing", "A contract test has passed"],
82
+ ];
83
+ const connectionChecks = [
84
+ ["active_version_missing", "Active version is published"],
85
+ ["destination_invalid", "One destination is configured"],
86
+ ["allowed_methods_empty", "HTTP methods are allowlisted"],
87
+ ["allowed_paths_empty", "Request paths are allowlisted"],
88
+ ["secret_reference_missing", "Credential binding name is recorded"],
89
+ ];
90
+ const checks = kind === "component" ? componentChecks : connectionChecks;
91
+ return <section className={`registry-verification ${validation.valid ? "verified" : "attention"}`} aria-live="polite">
92
+ <div><strong>{validation.valid ? "Setup verified" : "Setup needs attention"}</strong><span>{validation.valid ? "The stored configuration and contracts passed." : `${errors.length} ${errors.length === 1 ? "item needs" : "items need"} attention.`}</span></div>
93
+ <ul>{checks.map(([code, label]) => <li className={failed.has(code) ? "failed" : "passed"} key={code}><span aria-hidden="true">{failed.has(code) ? "!" : "✓"}</span>{label}</li>)}</ul>
94
+ {errors.length ? <div className="registry-verification-errors">{errors.map((issue) => <p key={issueCode(issue)}>{issueMessage(issue)}</p>)}</div> : null}
95
+ </section>;
96
+ }
97
+
98
+ export function RegistryDetailsDialog({ item, kind, canManage, onClose, onChanged }: RegistryDetailsDialogProps) {
99
+ const dialogRef = useRef<HTMLDialogElement>(null);
100
+ const [detail, setDetail] = useState<RegistryDetailResponse | null>(null);
101
+ const [dependencies, setDependencies] = useState<RegistryDependenciesResponse["dependencies"] | null>(null);
102
+ const [validation, setValidation] = useState<RegistryValidation | null>(null);
103
+ const [loading, setLoading] = useState(false);
104
+ const [pending, setPending] = useState("");
105
+ const [error, setError] = useState("");
106
+ const [confirmArchive, setConfirmArchive] = useState(false);
107
+ const [method, setMethod] = useState("POST");
108
+ const [path, setPath] = useState("/");
109
+ const [requestBody, setRequestBody] = useState("{}");
110
+
111
+ const route = kind === "component" ? "components" : "connections";
112
+ const version = useMemo(() => activeVersion(detail, item, kind), [detail, item, kind]);
113
+ const dependencyList = useMemo(() => dependencyRows(dependencies), [dependencies]);
114
+ const archived = item?.status === "archived";
115
+ const paused = item?.status === "paused";
116
+
117
+ useEffect(() => {
118
+ const dialog = dialogRef.current;
119
+ if (!dialog) return;
120
+ if (item && !dialog.open) dialog.showModal();
121
+ if (!item && dialog.open) dialog.close();
122
+ }, [item]);
123
+
124
+ useEffect(() => {
125
+ if (!item) return;
126
+ let active = true;
127
+ setLoading(true); setPending(""); setError(""); setDetail(null); setDependencies(null); setValidation(null); setConfirmArchive(false);
128
+ void Promise.all([
129
+ requestJson<RegistryDetailResponse>(`/v1/admin/${route}/${encodeURIComponent(item.id)}`),
130
+ requestJson<RegistryDependenciesResponse>(`/v1/admin/${route}/${encodeURIComponent(item.id)}/dependencies`),
131
+ ]).then(([nextDetail, nextDependencies]) => {
132
+ if (!active) return;
133
+ setDetail(nextDetail); setDependencies(nextDependencies.dependencies || {});
134
+ const nextVersion = activeVersion(nextDetail, item, kind);
135
+ if (kind === "connection" && nextVersion) {
136
+ const methods = Array.isArray(nextVersion.allowedMethods) ? nextVersion.allowedMethods : [];
137
+ const paths = Array.isArray(nextVersion.allowedPathPrefixes) ? nextVersion.allowedPathPrefixes : [];
138
+ setMethod(typeof methods[0] === "string" ? methods[0] : "POST");
139
+ setPath(typeof paths[0] === "string" ? paths[0] : "/");
140
+ }
141
+ }).catch((cause) => { if (active) setError(errorMessage(cause, `The ${route.slice(0, -1)} details could not be loaded.`)); })
142
+ .finally(() => { if (active) setLoading(false); });
143
+ return () => { active = false; };
144
+ }, [item?.id, kind, route]);
145
+
146
+ const verify = async (): Promise<void> => {
147
+ if (!item) return;
148
+ setPending("verify"); setError(""); setValidation(null);
149
+ try {
150
+ const response = await requestJson<RegistryValidationResponse>(`/v1/admin/${route}/${encodeURIComponent(item.id)}/validate`, { method: "POST" });
151
+ setValidation(response.validation);
152
+ } catch (cause) {
153
+ if (cause instanceof ApiRequestError && cause.status === 422 && cause.body && typeof cause.body === "object") {
154
+ const response = cause.body as RegistryValidationResponse;
155
+ if (response.validation) setValidation(response.validation);
156
+ else setError(errorMessage(cause, "Setup verification failed."));
157
+ } else setError(errorMessage(cause, "Setup verification failed."));
158
+ } finally { setPending(""); }
159
+ };
160
+
161
+ const runTest = async (resume = false): Promise<void> => {
162
+ if (!item) return;
163
+ const action = resume ? "resume" : "test";
164
+ let body: Record<string, unknown> = {};
165
+ try {
166
+ const parsed: unknown = JSON.parse(requestBody);
167
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("invalid");
168
+ body = parsed as Record<string, unknown>;
169
+ } catch {
170
+ setError("Request body must be a JSON object.");
171
+ return;
172
+ }
173
+ if (kind === "connection" && !resume && !window.confirm(`Send a live ${method} request to ${path}? The destination may process this request.`)) return;
174
+ setPending(action); setError("");
175
+ try {
176
+ if (kind === "connection") {
177
+ await requestJson(`/v1/admin/connections/${encodeURIComponent(item.id)}/${action}`, {
178
+ method: "POST", headers: mutationHeaders(), body: JSON.stringify({ method, path, body }),
179
+ });
180
+ onChanged(resume ? `${item.name} was tested and resumed.` : `${item.name} accepted the live test request.`);
181
+ } else {
182
+ await requestJson(`/v1/admin/components/${encodeURIComponent(item.id)}/test`, { method: "POST" });
183
+ onChanged(`${item.name} passed its stored contract examples.`);
184
+ }
185
+ await verify();
186
+ } catch (cause) { setError(errorMessage(cause, `${item.name} could not be ${resume ? "resumed" : "tested"}.`)); setPending(""); }
187
+ };
188
+
189
+ const setLifecycle = async (action: "pause" | "resume" | "archive"): Promise<void> => {
190
+ if (!item) return;
191
+ if (action === "resume" && kind === "connection") { await runTest(true); return; }
192
+ setPending(action); setError("");
193
+ try {
194
+ const endpoint = action === "archive" ? `/v1/admin/${route}/${encodeURIComponent(item.id)}` : `/v1/admin/${route}/${encodeURIComponent(item.id)}/${action}`;
195
+ await requestJson(endpoint, action === "archive"
196
+ ? { method: "PATCH", headers: mutationHeaders(), body: JSON.stringify({ status: "archived" }) }
197
+ : { method: "POST", headers: mutationHeaders(false) });
198
+ onChanged(action === "archive" ? `${item.name} was archived. Its versions remain in audit history.` : `${item.name} was ${action === "pause" ? "paused" : "resumed"}.`);
199
+ onClose();
200
+ } catch (cause) { setError(errorMessage(cause, `${item.name} could not be ${action}d.`)); setPending(""); }
201
+ };
202
+
203
+ const destination = kind === "connection" ? text(version?.baseUrl || version?.serviceBindingName, "No destination") : null;
204
+ const implementation = kind === "component" ? readable(version?.implementationKind || (item as ComponentSummary | null)?.implementationKind || "Not configured") : null;
205
+ const authority = kind === "component" && (item as ComponentSummary | null)?.effectClass === "side_effecting" ? "Can change customer data" : "Read-only";
206
+
207
+ return <dialog ref={dialogRef} className="detail-dialog registry-detail-dialog" aria-labelledby="registry-detail-title" onCancel={(event) => { event.preventDefault(); if (!pending) onClose(); }}>
208
+ {item ? <>
209
+ <div className="detail-head registry-detail-head"><div><span className="eyebrow">{kind === "component" ? "Tool" : "Connection"}</span><h2 id="registry-detail-title">{item.name}</h2><p>{item.description || "No description yet."}</p></div><button className="icon-button" type="button" aria-label="Close" disabled={Boolean(pending)} onClick={onClose}>×</button></div>
210
+ <div className="registry-detail-body">
211
+ {loading ? <div className="skeleton-block" /> : <>
212
+ <section className="registry-detail-summary" aria-label="Summary">
213
+ <div><span>Status</span><strong>{archived ? "Archived" : paused ? "Paused" : "Available"}</strong></div>
214
+ <div><span>Version</span><strong>{item.version ? `v${item.version}` : "Not published"}</strong></div>
215
+ <div><span>{kind === "component" ? "Authority" : "Authentication"}</span><strong>{kind === "component" ? authority : readable(version?.credentialStrategy || "none")}</strong></div>
216
+ <div><span>{kind === "component" ? "Implementation" : "Destination"}</span><strong title={destination || undefined}>{implementation || destination}</strong></div>
217
+ </section>
218
+
219
+ <section className="registry-detail-section">
220
+ <div className="registry-section-heading"><div><h3>Setup verification</h3><p>Checks stored configuration, schemas, credential binding names, and contract evidence. It does not call the tool or an external service.</p></div><button className="secondary" type="button" disabled={Boolean(pending)} onClick={() => void verify()}>{pending === "verify" ? "Verifying…" : "Verify setup"}</button></div>
221
+ {validation ? <VerificationPanel validation={validation} kind={kind} /> : <p className="registry-placeholder">Not verified in this session. Verification never sends customer data.</p>}
222
+ </section>
223
+
224
+ <section className="registry-detail-section">
225
+ <div className="registry-section-heading"><div><h3>{kind === "component" ? "Contract test" : "Live connection test"}</h3><p>{kind === "component" ? "Validates stored example inputs and outputs against this version’s schemas. It does not execute the implementation." : "Sends a real allowlisted request to the configured destination. The destination may process it."}</p></div></div>
226
+ {kind === "connection" ? <div className="registry-probe-form"><label>Method<select value={method} disabled={Boolean(pending) || archived} onChange={(event) => setMethod(event.currentTarget.value)}>{(Array.isArray(version?.allowedMethods) && version.allowedMethods.length ? version.allowedMethods : ["POST"]).map((value) => <option value={String(value)} key={String(value)}>{String(value)}</option>)}</select></label><label>Path<input value={path} disabled={Boolean(pending) || archived} onChange={(event) => setPath(event.currentTarget.value)} /></label><label className="wide-field">JSON request body<textarea className="code-input registry-probe-body" spellCheck={false} value={requestBody} disabled={Boolean(pending) || archived} onChange={(event) => setRequestBody(event.currentTarget.value)} /></label></div> : null}
227
+ <button className="secondary" type="button" disabled={Boolean(pending) || archived || (kind === "connection" && !path.startsWith("/"))} onClick={() => void runTest()}>{pending === "test" ? "Testing…" : kind === "component" ? "Run contract test" : "Send test request"}</button>
228
+ </section>
229
+
230
+ <section className="registry-detail-section">
231
+ <div className="registry-section-heading"><div><h3>Used by</h3><p>Active dependencies must be removed before this {kind === "component" ? "tool" : "connection"} can be archived.</p></div><span className="registry-count">{dependencyList.length}</span></div>
232
+ {dependencyList.length ? <ul className="registry-dependency-list">{dependencyList.map((entry, index) => <li key={`${entry.type}:${entry.name}:${entry.version}:${index}`}><span>{entry.type}</span><strong>{entry.name}</strong><small>{entry.version}</small></li>)}</ul> : <p className="registry-placeholder">Nothing currently depends on this {kind === "component" ? "tool" : "connection"}.</p>}
233
+ </section>
234
+
235
+ {canManage && !archived ? <section className="registry-detail-section registry-lifecycle-section">
236
+ <div className="registry-section-heading"><div><h3>Lifecycle</h3><p>Pause temporarily blocks new use. Archive permanently removes the item from selection while retaining its audit history.</p></div></div>
237
+ <div className="registry-lifecycle-actions">
238
+ <button className="secondary" type="button" disabled={Boolean(pending)} onClick={() => void setLifecycle(paused ? "resume" : "pause")}>{pending === "pause" ? "Pausing…" : pending === "resume" ? kind === "connection" ? "Testing…" : "Resuming…" : paused ? kind === "connection" ? "Test and resume" : "Resume tool" : `Pause ${kind === "component" ? "tool" : "connection"}`}</button>
239
+ <button className="danger-link" type="button" disabled={Boolean(pending) || dependencyList.length > 0} onClick={() => setConfirmArchive(true)}>Archive {kind === "component" ? "tool" : "connection"}</button>
240
+ </div>
241
+ {dependencyList.length ? <p className="registry-blocked-copy">Archive is unavailable until the dependencies above are retired or moved to another version.</p> : null}
242
+ {confirmArchive ? <div className="registry-archive-confirm" role="alert"><div><strong>Archive {item.name}?</strong><p>It will disappear from normal lists and cannot be restored. Version and audit records will remain.</p></div><div><button className="secondary" type="button" disabled={Boolean(pending)} onClick={() => setConfirmArchive(false)}>Cancel</button><button className="danger" type="button" disabled={Boolean(pending)} onClick={() => void setLifecycle("archive")}>{pending === "archive" ? "Archiving…" : "Archive permanently"}</button></div></div> : null}
243
+ </section> : null}
244
+ </>}
245
+ <p className="error" role="alert">{error}</p>
246
+ </div>
247
+ </> : null}
248
+ </dialog>;
249
+ }