create-safest-tools 0.2.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 (240) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +71 -0
  3. package/bin/create-safest-tools.mjs +12 -0
  4. package/package.json +34 -0
  5. package/src/cli.mjs +128 -0
  6. package/src/config.mjs +151 -0
  7. package/src/scaffold.mjs +30 -0
  8. package/template/LICENSE +201 -0
  9. package/template/README.md +35 -0
  10. package/template/console/README.md +28 -0
  11. package/template/console/analytics/AnalyticsWorkspace.tsx +55 -0
  12. package/template/console/analytics/controller.ts +133 -0
  13. package/template/console/analytics/events.ts +33 -0
  14. package/template/console/analytics/store.ts +33 -0
  15. package/template/console/analytics/types.ts +38 -0
  16. package/template/console/appeals/AppealWorkspace.tsx +73 -0
  17. package/template/console/appeals/controller.ts +125 -0
  18. package/template/console/appeals/events.ts +33 -0
  19. package/template/console/appeals/store.ts +35 -0
  20. package/template/console/appeals/types.ts +37 -0
  21. package/template/console/assistant/AssistantWorkspace.tsx +133 -0
  22. package/template/console/assistant/types.ts +34 -0
  23. package/template/console/auth/AccountJourney.tsx +69 -0
  24. package/template/console/auth/InvitationAcceptance.tsx +145 -0
  25. package/template/console/auth/OAuthButtons.tsx +78 -0
  26. package/template/console/auth/OperatorLogin.tsx +110 -0
  27. package/template/console/auth/PasswordRecovery.tsx +132 -0
  28. package/template/console/auth/account-route.ts +15 -0
  29. package/template/console/auth/browser-navigation.ts +7 -0
  30. package/template/console/auth/events.ts +15 -0
  31. package/template/console/command/CommandCentre.tsx +119 -0
  32. package/template/console/command/controller.ts +267 -0
  33. package/template/console/command/events.ts +19 -0
  34. package/template/console/command/store.ts +36 -0
  35. package/template/console/command/types.ts +61 -0
  36. package/template/console/components/AnalystIdentity.tsx +25 -0
  37. package/template/console/components/PageHeader.tsx +27 -0
  38. package/template/console/configuration/ConfigurationDialog.tsx +200 -0
  39. package/template/console/configuration/ConfigurationWorkspace.tsx +66 -0
  40. package/template/console/configuration/FormBuilder.tsx +143 -0
  41. package/template/console/configuration/api.ts +40 -0
  42. package/template/console/configuration/events.ts +14 -0
  43. package/template/console/configuration/types.ts +79 -0
  44. package/template/console/lib/format.ts +49 -0
  45. package/template/console/lib/http.ts +96 -0
  46. package/template/console/main.tsx +225 -0
  47. package/template/console/operations/OperationsWorkspace.tsx +126 -0
  48. package/template/console/operations/controller.ts +237 -0
  49. package/template/console/operations/events.ts +37 -0
  50. package/template/console/operations/store.ts +32 -0
  51. package/template/console/operations/types.ts +107 -0
  52. package/template/console/people/PeopleWorkspace.tsx +81 -0
  53. package/template/console/people/types.ts +25 -0
  54. package/template/console/profile/ProfileWorkspace.tsx +136 -0
  55. package/template/console/profile/events.ts +9 -0
  56. package/template/console/profile/types.ts +6 -0
  57. package/template/console/quality/QualityWorkspace.tsx +87 -0
  58. package/template/console/quality/controller.ts +154 -0
  59. package/template/console/quality/events.ts +34 -0
  60. package/template/console/quality/store.ts +36 -0
  61. package/template/console/quality/types.ts +70 -0
  62. package/template/console/queues/QueueEditor.tsx +173 -0
  63. package/template/console/queues/QueueWorkspace.tsx +109 -0
  64. package/template/console/queues/controller.ts +194 -0
  65. package/template/console/queues/events.ts +34 -0
  66. package/template/console/queues/store.ts +37 -0
  67. package/template/console/queues/types.ts +102 -0
  68. package/template/console/registry/RegistryDialog.tsx +198 -0
  69. package/template/console/registry/RegistryWorkspace.tsx +108 -0
  70. package/template/console/registry/events.ts +14 -0
  71. package/template/console/registry/types.ts +52 -0
  72. package/template/console/reports/ReportDrawer.tsx +159 -0
  73. package/template/console/reports/ReportWorkspace.tsx +90 -0
  74. package/template/console/reports/controller.ts +517 -0
  75. package/template/console/reports/events.ts +42 -0
  76. package/template/console/reports/store.ts +39 -0
  77. package/template/console/reports/types.ts +230 -0
  78. package/template/console/settings/BrandEditor.tsx +126 -0
  79. package/template/console/settings/ChannelDialog.tsx +241 -0
  80. package/template/console/settings/SettingsWorkspace.tsx +155 -0
  81. package/template/console/settings/events.ts +19 -0
  82. package/template/console/settings/handoff.ts +22 -0
  83. package/template/console/settings/types.ts +93 -0
  84. package/template/console/shell/WorkspaceShell.tsx +160 -0
  85. package/template/console/shell/controller.ts +182 -0
  86. package/template/console/shell/events.ts +25 -0
  87. package/template/console/shell/navigation.ts +61 -0
  88. package/template/console/shell/store.ts +56 -0
  89. package/template/console/shell/types.ts +86 -0
  90. package/template/console/workflows/CreateWorkflowDialog.tsx +87 -0
  91. package/template/console/workflows/WorkflowCanvas.tsx +42 -0
  92. package/template/console/workflows/WorkflowDialogs.tsx +6 -0
  93. package/template/console/workflows/WorkflowStudio.tsx +331 -0
  94. package/template/console/workflows/WorkflowWorkspace.tsx +82 -0
  95. package/template/console/workflows/events.ts +17 -0
  96. package/template/console/workflows/graph.ts +156 -0
  97. package/template/console/workflows/templates.ts +70 -0
  98. package/template/console/workflows/types.ts +201 -0
  99. package/template/gitignore.template +18 -0
  100. package/template/migrations/0001_reports_foundation.sql +444 -0
  101. package/template/migrations/0002_human_report_loop.sql +65 -0
  102. package/template/migrations/0003_delivery_reliability.sql +18 -0
  103. package/template/migrations/0004_public_intake.sql +14 -0
  104. package/template/migrations/0005_operations_visibility.sql +24 -0
  105. package/template/migrations/0006_ai_governance.sql +198 -0
  106. package/template/migrations/0007_ai_release_gates.sql +6 -0
  107. package/template/migrations/0008_retention_analytics_exports.sql +52 -0
  108. package/template/migrations/0009_retention_derived_copies.sql +14 -0
  109. package/template/migrations/0010_ai_quality_controls.sql +26 -0
  110. package/template/migrations/0011_analyst_presence.sql +28 -0
  111. package/template/migrations/0012_queue_policies.sql +87 -0
  112. package/template/migrations/0013_routing_agents.sql +63 -0
  113. package/template/migrations/0014_webhook_enrichments.sql +122 -0
  114. package/template/migrations/0015_queue_owned_ai.sql +55 -0
  115. package/template/migrations/0016_operator_accounts.sql +67 -0
  116. package/template/migrations/0017_operator_profiles_and_recovery.sql +33 -0
  117. package/template/migrations/0018_platform_configuration.sql +382 -0
  118. package/template/migrations/0019_workflow_authoring_runtime.sql +372 -0
  119. package/template/migrations/0020_tasks_findings_assistant_budgets.sql +427 -0
  120. package/template/migrations/0021_abuse_evidence_operations.sql +324 -0
  121. package/template/migrations/0022_workflow_dispatch_operations.sql +42 -0
  122. package/template/migrations/0023_component_connection_execution.sql +75 -0
  123. package/template/migrations/0024_access_runtime_integrity.sql +72 -0
  124. package/template/migrations/0025_installation_timezone.sql +11 -0
  125. package/template/migrations/0026_ai_and_egress_execution_controls.sql +49 -0
  126. package/template/migrations/0027_prompt_and_ai_registry.sql +37 -0
  127. package/template/migrations/0028_step_attempt_ai_provenance.sql +13 -0
  128. package/template/migrations/0029_evidence_fetch_transport.sql +5 -0
  129. package/template/migrations/0030_evidence_dlq_incidents.sql +45 -0
  130. package/template/migrations/0031_shadow_quality_integrity.sql +7 -0
  131. package/template/migrations/0032_action_delivery_outbox.sql +55 -0
  132. package/template/migrations/0033_configuration_and_assistant_drafts.sql +43 -0
  133. package/template/migrations/0034_installation_integrations.sql +31 -0
  134. package/template/migrations/0035_workspace_governance.sql +21 -0
  135. package/template/migrations/0036_published_routing_baseline.sql +15 -0
  136. package/template/migrations/0037_builtin_phishing_specialist.sql +71 -0
  137. package/template/migrations/0038_remove_deprecated_enrichment_runtime.sql +228 -0
  138. package/template/migrations/0039_secure_reporting_channels.sql +45 -0
  139. package/template/migrations/0040_notification_only_reporting.sql +24 -0
  140. package/template/migrations/0041_better_auth_credentials.sql +17 -0
  141. package/template/package.json +47 -0
  142. package/template/public/_headers +27 -0
  143. package/template/public/app-icon-192.png +0 -0
  144. package/template/public/app-icon-512.png +0 -0
  145. package/template/public/brand-icon.svg +7 -0
  146. package/template/public/brand-tokens.css +80 -0
  147. package/template/public/console/auth-shell.js +19340 -0
  148. package/template/public/customer-brand.js +58 -0
  149. package/template/public/embed/embed.css +139 -0
  150. package/template/public/embed/embed.js +408 -0
  151. package/template/public/embed/index.html +91 -0
  152. package/template/public/favicon.svg +7 -0
  153. package/template/public/fonts/Manrope-Variable.ttf +0 -0
  154. package/template/public/fonts/Newsreader-Italic-Variable.ttf +0 -0
  155. package/template/public/fonts/Newsreader-Variable.ttf +0 -0
  156. package/template/public/index.html +123 -0
  157. package/template/public/logo-primary.svg +7 -0
  158. package/template/public/logo-reversed.svg +7 -0
  159. package/template/public/manifest.webmanifest +21 -0
  160. package/template/public/public-report.js +184 -0
  161. package/template/public/report/index.html +39 -0
  162. package/template/public/report/public-report.css +20 -0
  163. package/template/public/social-card.png +0 -0
  164. package/template/public/styles.css +1521 -0
  165. package/template/public/widget.css +80 -0
  166. package/template/public/widget.js +220 -0
  167. package/template/reports.config.example.json +38 -0
  168. package/template/reports.schema.json +89 -0
  169. package/template/scripts/reports-auth-onboarding.mjs +180 -0
  170. package/template/scripts/reports-backup.mjs +276 -0
  171. package/template/scripts/reports-cloudflare-preflight.mjs +152 -0
  172. package/template/scripts/reports-deploy.mjs +173 -0
  173. package/template/scripts/reports-plan.mjs +226 -0
  174. package/template/scripts/reports-restore.mjs +180 -0
  175. package/template/scripts/reports-secrets.mjs +73 -0
  176. package/template/scripts/reports-uninstall-plan.mjs +32 -0
  177. package/template/src/agent-executor.ts +330 -0
  178. package/template/src/ai-observability.ts +163 -0
  179. package/template/src/ai-registry-validation.ts +244 -0
  180. package/template/src/ai-registry.ts +292 -0
  181. package/template/src/api-cursor.ts +84 -0
  182. package/template/src/assistant.ts +554 -0
  183. package/template/src/audit.ts +39 -0
  184. package/template/src/backup-service.ts +269 -0
  185. package/template/src/budget-control.ts +127 -0
  186. package/template/src/component-executor.ts +845 -0
  187. package/template/src/configuration-registry.ts +550 -0
  188. package/template/src/connection-egress.ts +430 -0
  189. package/template/src/connection-oauth.ts +368 -0
  190. package/template/src/evidence-service.ts +335 -0
  191. package/template/src/human-tasks.ts +316 -0
  192. package/template/src/index.ts +4114 -0
  193. package/template/src/installation-admin.ts +301 -0
  194. package/template/src/intake-abuse.ts +156 -0
  195. package/template/src/platform-registry-validation.ts +367 -0
  196. package/template/src/platform-registry.ts +753 -0
  197. package/template/src/report-admin.ts +342 -0
  198. package/template/src/report-ai-quality.ts +390 -0
  199. package/template/src/report-ai-validation.ts +80 -0
  200. package/template/src/report-ai.ts +857 -0
  201. package/template/src/report-auth.ts +292 -0
  202. package/template/src/report-better-auth.ts +583 -0
  203. package/template/src/report-context-schema.ts +132 -0
  204. package/template/src/report-context.ts +126 -0
  205. package/template/src/report-crypto.ts +142 -0
  206. package/template/src/report-delivery.ts +579 -0
  207. package/template/src/report-form-validation.ts +51 -0
  208. package/template/src/report-governance.ts +510 -0
  209. package/template/src/report-http.ts +90 -0
  210. package/template/src/report-operations.ts +685 -0
  211. package/template/src/report-operator-accounts.ts +810 -0
  212. package/template/src/report-presence.ts +398 -0
  213. package/template/src/report-queue-validation.ts +309 -0
  214. package/template/src/report-queues.ts +478 -0
  215. package/template/src/report-repository.ts +972 -0
  216. package/template/src/report-router-agent.ts +328 -0
  217. package/template/src/report-router-validation.ts +81 -0
  218. package/template/src/report-routing.ts +178 -0
  219. package/template/src/report-turnstile.ts +142 -0
  220. package/template/src/report-types.ts +224 -0
  221. package/template/src/report-validation.ts +279 -0
  222. package/template/src/report-workflow-validation.ts +119 -0
  223. package/template/src/report-workflow.ts +886 -0
  224. package/template/src/shadow-quality.ts +278 -0
  225. package/template/src/workflow-actions.ts +782 -0
  226. package/template/src/workflow-compiler.ts +230 -0
  227. package/template/src/workflow-dynamic-runtime.ts +613 -0
  228. package/template/src/workflow-effects.ts +316 -0
  229. package/template/src/workflow-expressions.ts +191 -0
  230. package/template/src/workflow-platform-types.ts +121 -0
  231. package/template/src/workflow-platform-validation.ts +540 -0
  232. package/template/src/workflow-repository.ts +916 -0
  233. package/template/src/workflow-runs.ts +686 -0
  234. package/template/src/workflow-simulator.ts +211 -0
  235. package/template/src/workspace-branding.ts +128 -0
  236. package/template/src/workspace-governance.ts +279 -0
  237. package/template/tsconfig.console.json +23 -0
  238. package/template/tsconfig.json +22 -0
  239. package/template/vite.console.config.ts +20 -0
  240. package/template/worker-configuration.d.ts +65 -0
@@ -0,0 +1,34 @@
1
+ import type { QueuePublishInput, QueueWorkspaceSnapshot, RoutingAgentInput } from "./types";
2
+
3
+ export const queueEvents = {
4
+ state: "safest:queues:state",
5
+ create: "safest:queues:create",
6
+ edit: "safest:queues:edit",
7
+ closeEditor: "safest:queues:close-editor",
8
+ publishQueue: "safest:queues:publish-queue",
9
+ publishRouting: "safest:queues:publish-routing",
10
+ } as const;
11
+
12
+ function dispatch<TDetail>(type: string, detail?: TDetail): void {
13
+ window.dispatchEvent(detail === undefined ? new Event(type) : new CustomEvent(type, { detail }));
14
+ }
15
+
16
+ export function dispatchCreateQueue(): void { dispatch(queueEvents.create); }
17
+ export function dispatchEditQueue(queueId: string): void { dispatch(queueEvents.edit, { queueId }); }
18
+ export function dispatchCloseQueueEditor(): void { dispatch(queueEvents.closeEditor); }
19
+ export function dispatchPublishQueue(input: QueuePublishInput): void { dispatch(queueEvents.publishQueue, input); }
20
+ export function dispatchPublishRouting(input: RoutingAgentInput): void { dispatch(queueEvents.publishRouting, input); }
21
+
22
+ export function isQueueWorkspaceSnapshot(value: unknown): value is QueueWorkspaceSnapshot {
23
+ if (!value || typeof value !== "object") return false;
24
+ const candidate = value as Partial<QueueWorkspaceSnapshot>;
25
+ return typeof candidate.loading === "boolean"
26
+ && typeof candidate.error === "string"
27
+ && typeof candidate.routingLoading === "boolean"
28
+ && typeof candidate.routingError === "string"
29
+ && typeof candidate.routingPending === "boolean"
30
+ && typeof candidate.queuePending === "boolean"
31
+ && Array.isArray(candidate.queues)
32
+ && typeof candidate.canManage === "boolean"
33
+ && typeof candidate.editor === "object";
34
+ }
@@ -0,0 +1,37 @@
1
+ import { isQueueWorkspaceSnapshot, queueEvents } from "./events";
2
+ import type { QueueWorkspaceSnapshot } from "./types";
3
+
4
+ const subscribers = new Set<() => void>();
5
+
6
+ let snapshot: QueueWorkspaceSnapshot = {
7
+ loading: true,
8
+ error: "",
9
+ routingLoading: true,
10
+ routingError: "",
11
+ routingPending: false,
12
+ queuePending: false,
13
+ queues: [],
14
+ routingAgent: null,
15
+ canManage: false,
16
+ editor: { open: false, queueId: null, error: "" },
17
+ };
18
+
19
+ window.addEventListener(queueEvents.state, (event) => {
20
+ if (!(event instanceof CustomEvent) || !isQueueWorkspaceSnapshot(event.detail)) return;
21
+ snapshot = event.detail;
22
+ for (const subscriber of subscribers) subscriber();
23
+ });
24
+
25
+ export function getQueueWorkspaceSnapshot(): QueueWorkspaceSnapshot {
26
+ return snapshot;
27
+ }
28
+
29
+ export function setQueueWorkspaceSnapshot(next: QueueWorkspaceSnapshot | ((current: QueueWorkspaceSnapshot) => QueueWorkspaceSnapshot)): void {
30
+ snapshot = typeof next === "function" ? next(snapshot) : next;
31
+ for (const subscriber of subscribers) subscriber();
32
+ }
33
+
34
+ export function subscribeToQueueWorkspace(callback: () => void): () => void {
35
+ subscribers.add(callback);
36
+ return () => subscribers.delete(callback);
37
+ }
@@ -0,0 +1,102 @@
1
+ export type QueueManagementMode = "human" | "ai" | "hybrid";
2
+ export type QueueRolloutMode = "off" | "shadow" | "assist" | "autonomous";
3
+ export type RoutingRolloutMode = "off" | "shadow" | "active";
4
+
5
+ export interface QueueAiBounds {
6
+ allowedOutcomes: string[];
7
+ confidenceThreshold: number;
8
+ maxFollowUpTurns: number;
9
+ model: string;
10
+ policyVersionId: string;
11
+ allowedFields: string[];
12
+ allowedTemplateIds: string[];
13
+ dailyReportBudget: number;
14
+ dailyCostBudgetMicrousd: number;
15
+ inputCostMicrousdPerMillion: number;
16
+ outputCostMicrousdPerMillion: number;
17
+ qualitySampleRate: number;
18
+ maxWaitHours: number;
19
+ minimumShadowRuns: number;
20
+ minimumQualityReviews: number;
21
+ maximumFailureRate: number;
22
+ maximumDisagreementRate: number;
23
+ }
24
+
25
+ export interface QueuePolicy {
26
+ version: number;
27
+ managementMode: QueueManagementMode;
28
+ rolloutMode: QueueRolloutMode;
29
+ priority: number;
30
+ responseSlaMinutes: number | null;
31
+ resolutionSlaMinutes: number | null;
32
+ claimTtlMinutes: number;
33
+ fallbackQueueId: string | null;
34
+ reviewerInstructions: string;
35
+ aiInstructions: string | null;
36
+ aiBounds: QueueAiBounds | null;
37
+ }
38
+
39
+ export interface QueueRecord {
40
+ id: string;
41
+ key: string;
42
+ name: string;
43
+ description: string;
44
+ status: string;
45
+ position: number;
46
+ counts: { openReports: number };
47
+ activePolicy: QueuePolicy | null;
48
+ }
49
+
50
+ export interface RoutingAgentConfiguration {
51
+ version: number;
52
+ rolloutMode: RoutingRolloutMode;
53
+ model: string;
54
+ confidenceThreshold: number;
55
+ safeguardQueueId: string;
56
+ defaultQueueId: string;
57
+ instructions: string;
58
+ candidateQueueIds: string[];
59
+ allowedFields: string[];
60
+ }
61
+
62
+ export interface QueueEditorSnapshot {
63
+ open: boolean;
64
+ queueId: string | null;
65
+ error: string;
66
+ }
67
+
68
+ export interface QueueWorkspaceSnapshot {
69
+ loading: boolean;
70
+ error: string;
71
+ routingLoading: boolean;
72
+ routingError: string;
73
+ routingPending: boolean;
74
+ queuePending: boolean;
75
+ queues: QueueRecord[];
76
+ routingAgent: RoutingAgentConfiguration | null;
77
+ canManage: boolean;
78
+ editor: QueueEditorSnapshot;
79
+ }
80
+
81
+ export interface RoutingAgentInput {
82
+ rolloutMode: RoutingRolloutMode;
83
+ model: string;
84
+ confidenceThreshold: number;
85
+ safeguardQueueId: string;
86
+ defaultQueueId: string;
87
+ instructions: string;
88
+ candidateQueueIds: string[];
89
+ allowedFields: string[];
90
+ }
91
+
92
+ export interface QueuePolicyInput extends Omit<QueuePolicy, "version" | "aiBounds"> {
93
+ aiBounds: QueueAiBounds;
94
+ }
95
+
96
+ export interface QueuePublishInput {
97
+ queueId: string | null;
98
+ key: string;
99
+ name: string;
100
+ description: string;
101
+ policy: QueuePolicyInput;
102
+ }
@@ -0,0 +1,198 @@
1
+ import { useEffect, useMemo, useRef, useState, useSyncExternalStore, type FormEvent } from "react";
2
+ import { errorMessage, mutationHeaders, requestJson } from "../lib/http";
3
+ import { getShellSnapshot, subscribeToShell } from "../shell/store";
4
+ import { announceRegistryChanged, registryEvents } from "./events";
5
+ import type { ConnectionListResponse, ConnectionSummary, RegistryKind } from "./types";
6
+
7
+ type ComponentKind = "built_in" | "webhook_enrichment" | "api_enrichment" | "workers_ai" | "specialist_agent" | "action" | "message";
8
+ type ImplementationKind = "built_in" | "webhook" | "external_api" | "workers_ai" | "specialist_agent" | "message_template";
9
+ type ConnectionKind = "webhook" | "external_api" | "customer_worker" | "ai_provider" | "notification" | "email";
10
+ type CredentialStrategy = "none" | "static_header" | "bearer" | "hmac" | "service_binding";
11
+
12
+ const inputSchemaDefault = JSON.stringify({ type: "object", properties: { report: { type: "object" } } }, null, 2);
13
+ const outputSchemaDefault = JSON.stringify({ type: "object", properties: { result: { type: "string" } } }, null, 2);
14
+
15
+ function slug(value: string): string {
16
+ return value.toLowerCase().trim().replace(/[^a-z0-9]+/gu, "-").replace(/^-|-$/gu, "").slice(0, 100);
17
+ }
18
+
19
+ function commaValues(value: string): string[] {
20
+ return value.split(",").map((entry) => entry.trim()).filter(Boolean);
21
+ }
22
+
23
+ function jsonObject(value: string, label: string): Record<string, unknown> {
24
+ const parsed: unknown = JSON.parse(value);
25
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(`${label} must be a JSON object.`);
26
+ return parsed as Record<string, unknown>;
27
+ }
28
+
29
+ function defaultsForComponentKind(kind: ComponentKind): { implementation: ImplementationKind; reference: string } {
30
+ if (kind === "action") return { implementation: "webhook", reference: "POST /" };
31
+ if (kind === "webhook_enrichment") return { implementation: "webhook", reference: "POST /" };
32
+ if (kind === "api_enrichment") return { implementation: "external_api", reference: "POST /" };
33
+ if (kind === "workers_ai") return { implementation: "workers_ai", reference: "@cf/meta/llama-3.1-8b-instruct" };
34
+ if (kind === "specialist_agent") return { implementation: "specialist_agent", reference: "custom-specialist-v1" };
35
+ if (kind === "message") return { implementation: "message_template", reference: "report-update-v1" };
36
+ return { implementation: "built_in", reference: "custom-enrichment-v1" };
37
+ }
38
+
39
+ export function RegistryDialog() {
40
+ const shell = useSyncExternalStore(subscribeToShell, getShellSnapshot, getShellSnapshot);
41
+ const dialogRef = useRef<HTMLDialogElement>(null);
42
+ const nameRef = useRef<HTMLInputElement>(null);
43
+ const [kind, setKind] = useState<RegistryKind | null>(null);
44
+ const [pending, setPending] = useState(false);
45
+ const [error, setError] = useState("");
46
+ const [name, setName] = useState("");
47
+ const [key, setKey] = useState("");
48
+ const [keyEdited, setKeyEdited] = useState(false);
49
+ const [description, setDescription] = useState("");
50
+ const [componentKind, setComponentKind] = useState<ComponentKind>("built_in");
51
+ const [owner, setOwner] = useState<"customer" | "safest">("customer");
52
+ const [implementation, setImplementation] = useState<ImplementationKind>("built_in");
53
+ const [reference, setReference] = useState("custom-enrichment-v1");
54
+ const [connectionVersionId, setConnectionVersionId] = useState("");
55
+ const [risk, setRisk] = useState("low");
56
+ const [inputFields, setInputFields] = useState("report.id,report.reason_code");
57
+ const [classifications, setClassifications] = useState("report_operational");
58
+ const [inputSchema, setInputSchema] = useState(inputSchemaDefault);
59
+ const [outputSchema, setOutputSchema] = useState(outputSchemaDefault);
60
+ const [connections, setConnections] = useState<ConnectionSummary[]>([]);
61
+ const [connectionKind, setConnectionKind] = useState<ConnectionKind>("webhook");
62
+ const [targetMode, setTargetMode] = useState<"url" | "service">("url");
63
+ const [baseUrl, setBaseUrl] = useState("");
64
+ const [serviceBinding, setServiceBinding] = useState("");
65
+ const [methods, setMethods] = useState("POST");
66
+ const [paths, setPaths] = useState("/");
67
+ const [credential, setCredential] = useState<CredentialStrategy>("none");
68
+ const [secretReference, setSecretReference] = useState("");
69
+
70
+ const reset = (): void => {
71
+ setName(""); setKey(""); setKeyEdited(false); setDescription(""); setComponentKind("built_in"); setOwner("customer");
72
+ setImplementation("built_in"); setReference("custom-enrichment-v1"); setConnectionVersionId(""); setRisk("low");
73
+ setInputFields("report.id,report.reason_code"); setClassifications("report_operational"); setInputSchema(inputSchemaDefault); setOutputSchema(outputSchemaDefault);
74
+ setConnectionKind("webhook"); setTargetMode("url"); setBaseUrl(""); setServiceBinding(""); setMethods("POST"); setPaths("/"); setCredential("none"); setSecretReference(""); setError("");
75
+ };
76
+ const close = (): void => { if (!pending) { setKind(null); reset(); } };
77
+
78
+ useEffect(() => {
79
+ const open = (event: Event): void => {
80
+ if (!(event instanceof CustomEvent) || (event.detail?.kind !== "component" && event.detail?.kind !== "connection")) return;
81
+ const requested = event.detail.kind as RegistryKind;
82
+ const allowed = requested === "component" ? shell.session?.permissions.publishComponents === true : shell.session?.permissions.publishConnections === true;
83
+ if (!allowed) return;
84
+ reset(); setKind(requested);
85
+ };
86
+ window.addEventListener(registryEvents.openEditor, open);
87
+ return () => window.removeEventListener(registryEvents.openEditor, open);
88
+ }, [shell.session?.permissions.publishComponents, shell.session?.permissions.publishConnections]);
89
+ useEffect(() => {
90
+ const dialog = dialogRef.current;
91
+ if (!dialog) return;
92
+ if (kind && !dialog.open) { dialog.showModal(); requestAnimationFrame(() => nameRef.current?.focus()); }
93
+ if (!kind && dialog.open) dialog.close();
94
+ }, [kind]);
95
+ useEffect(() => {
96
+ if (kind !== "component" || shell.session?.permissions.manageConnections !== true) return;
97
+ let active = true;
98
+ void requestJson<ConnectionListResponse>("/v1/admin/connections").then((response) => {
99
+ if (active) setConnections((response.connections || []).filter((connection) => connection.status === "active" && Boolean(connection.activeVersionId)));
100
+ }).catch(() => { if (active) setConnections([]); });
101
+ return () => { active = false; };
102
+ }, [kind, shell.session?.permissions.manageConnections]);
103
+
104
+ const isAction = componentKind === "action";
105
+ const needsConnection = implementation === "webhook" || implementation === "external_api";
106
+ const needsSecret = credential !== "none" && credential !== "service_binding";
107
+ const title = kind === "connection" ? "Create connection" : "Create component";
108
+ const subtitle = kind === "connection" ? "Allow one destination and refer to credentials by Worker secret binding." : "Define its authority and immutable runtime contract.";
109
+ const authority = isAction ? "Side-effecting action" : "Read-only enrichment";
110
+ const authorityDetail = isAction ? "This component can change customer data. Workflows must provide explicit approval." : "This component may add findings but cannot change customer data.";
111
+ const activeConnections = useMemo(() => connections.filter((connection) => connection.activeVersionId), [connections]);
112
+
113
+ const changeComponentKind = (next: ComponentKind): void => {
114
+ setComponentKind(next);
115
+ const defaults = defaultsForComponentKind(next);
116
+ setImplementation(defaults.implementation); setReference(defaults.reference); setRisk(next === "action" ? "high" : "low");
117
+ };
118
+ const changeTargetMode = (next: "url" | "service"): void => {
119
+ setTargetMode(next);
120
+ if (next === "service") { setBaseUrl(""); setCredential("service_binding"); setSecretReference(""); }
121
+ else { setServiceBinding(""); if (credential === "service_binding") setCredential("none"); }
122
+ };
123
+
124
+ const submit = async (event: FormEvent<HTMLFormElement>): Promise<void> => {
125
+ event.preventDefault();
126
+ if (!kind) return;
127
+ setPending(true); setError("");
128
+ try {
129
+ if (kind === "connection") {
130
+ await requestJson("/v1/admin/connections", {
131
+ method: "POST", headers: mutationHeaders(), body: JSON.stringify({
132
+ key, name, description, kind: connectionKind,
133
+ version: {
134
+ base_url: targetMode === "url" ? baseUrl : null,
135
+ service_binding_name: targetMode === "service" ? serviceBinding : null,
136
+ allowed_methods: commaValues(methods), allowed_path_prefixes: commaValues(paths),
137
+ credential_strategy: credential, secret_reference: needsSecret ? secretReference : null,
138
+ },
139
+ }),
140
+ });
141
+ } else {
142
+ const parsedInput = jsonObject(inputSchema, "Input schema");
143
+ const parsedOutput = jsonObject(outputSchema, "Output schema");
144
+ await requestJson("/v1/admin/components", {
145
+ method: "POST", headers: mutationHeaders(), body: JSON.stringify({
146
+ key, name, description, kind: componentKind, effect_class: isAction ? "side_effecting" : "read_only", owner_kind: owner,
147
+ version: {
148
+ implementation_kind: implementation, implementation_reference: reference,
149
+ input_schema: parsedInput, output_schema: parsedOutput,
150
+ allowed_input_fields: commaValues(inputFields), data_classifications: commaValues(classifications),
151
+ connection_version_id: needsConnection ? connectionVersionId : null, risk_class: risk,
152
+ timeout_policy: { timeout_ms: 5000 }, retry_policy: { maximum_attempts: isAction ? 5 : 3, backoff: "exponential" },
153
+ cache_policy: { enabled: !isAction }, concurrency_policy: { maximum_concurrency: 8 }, budget_policy: {},
154
+ approval_policy: isAction ? { mode: "human" } : {}, tool_manifest: [],
155
+ compatibility: { fixture_evidence: { fixtures: [{ name: "Contract example", input: { report: {} }, expected_output: { result: "ok" }, expected_status: "succeeded", mode: "simulated" }] } },
156
+ },
157
+ }),
158
+ });
159
+ }
160
+ setPending(false); setKind(null); reset(); announceRegistryChanged();
161
+ } catch (cause) { setError(errorMessage(cause, `The ${kind} could not be created.`)); setPending(false); }
162
+ };
163
+
164
+ return <dialog ref={dialogRef} className="editor-dialog registry-dialog" aria-labelledby="registry-dialog-title" onCancel={(event) => { event.preventDefault(); close(); }} data-react-slice="registry-dialog">
165
+ <div className="detail-head"><div><h2 id="registry-dialog-title">{title}</h2><p>{subtitle}</p></div><button className="icon-button" type="button" aria-label="Close" disabled={pending} onClick={close}>×</button></div>
166
+ <form className="editor-form registry-form" onSubmit={(event) => void submit(event)}>
167
+ <div className="form-grid">
168
+ <label>Name<input ref={nameRef} maxLength={160} required disabled={pending} value={name} onChange={(event) => { const value = event.currentTarget.value; setName(value); if (!keyEdited) setKey(slug(value)); }} /></label>
169
+ <label>Key<input maxLength={100} pattern="[a-z0-9]+(?:-[a-z0-9]+)*" required disabled={pending} value={key} onChange={(event) => { setKeyEdited(true); setKey(event.currentTarget.value); }} /></label>
170
+ {kind === "component" ? <><label>Type<select disabled={pending} value={componentKind} onChange={(event) => changeComponentKind(event.currentTarget.value as ComponentKind)}><option value="built_in">Built-in enrichment</option><option value="webhook_enrichment">Webhook enrichment</option><option value="api_enrichment">API enrichment</option><option value="workers_ai">Workers AI</option><option value="specialist_agent">Specialist agent</option><option value="action">Action</option><option value="message">Message</option></select></label><label>Owner<select disabled={pending} value={owner} onChange={(event) => setOwner(event.currentTarget.value as "customer" | "safest")}><option value="customer">Customer</option><option value="safest">Safest built-in</option></select></label></> : <><label>Type<select disabled={pending} value={connectionKind} onChange={(event) => setConnectionKind(event.currentTarget.value as ConnectionKind)}><option value="webhook">Webhook</option><option value="external_api">External API</option><option value="customer_worker">Customer Worker</option><option value="ai_provider">AI provider</option><option value="notification">Notification</option><option value="email">Email</option></select></label><label>Destination<select disabled={pending} value={targetMode} onChange={(event) => changeTargetMode(event.currentTarget.value as "url" | "service")}><option value="url">HTTPS endpoint</option><option value="service">Worker service binding</option></select></label></>}
171
+ <label className="wide-field">Description<textarea maxLength={2000} disabled={pending} value={description} onChange={(event) => setDescription(event.currentTarget.value)} /></label>
172
+ </div>
173
+ {kind === "component" ? <>
174
+ <div className="safety-boundary"><strong>{authority}</strong><span>{authorityDetail}</span></div>
175
+ <div className="form-grid">
176
+ <label>Implementation<select disabled={pending} value={implementation} onChange={(event) => setImplementation(event.currentTarget.value as ImplementationKind)}><option value="built_in">Built-in</option><option value="workers_ai">Workers AI</option><option value="specialist_agent">Specialist agent</option><option value="webhook">Webhook</option><option value="external_api">External API</option><option value="message_template">Message template</option></select></label>
177
+ <label>Implementation reference<input required disabled={pending} value={reference} onChange={(event) => setReference(event.currentTarget.value)} /></label>
178
+ {needsConnection ? <label>Connection<select required disabled={pending} value={connectionVersionId} onChange={(event) => setConnectionVersionId(event.currentTarget.value)}><option value="">Select a published connection</option>{activeConnections.map((connection) => <option value={connection.activeVersionId || ""} key={connection.id}>{connection.name} · v{connection.version || 1}</option>)}</select></label> : null}
179
+ <label>Risk class<select disabled={pending} value={risk} onChange={(event) => setRisk(event.currentTarget.value)}><option value="low">Low</option><option value="medium">Medium</option><option value="high">High</option><option value="critical">Critical</option></select></label>
180
+ <label>Allowed input fields<input required disabled={pending} value={inputFields} onChange={(event) => setInputFields(event.currentTarget.value)} /></label>
181
+ <label>Data classifications<input required disabled={pending} value={classifications} onChange={(event) => setClassifications(event.currentTarget.value)} /></label>
182
+ <label className="wide-field">Input schema<textarea className="code-input" spellCheck={false} required disabled={pending} value={inputSchema} onChange={(event) => setInputSchema(event.currentTarget.value)} /></label>
183
+ <label className="wide-field">Output schema<textarea className="code-input" spellCheck={false} required disabled={pending} value={outputSchema} onChange={(event) => setOutputSchema(event.currentTarget.value)} /></label>
184
+ </div>
185
+ </> : kind === "connection" ? <>
186
+ <div className="safety-boundary"><strong>Outbound access stays allowlisted.</strong><span>Enter a public HTTPS origin or a Worker service binding. Store credential values only in Worker secrets.</span></div>
187
+ <div className="form-grid">
188
+ {targetMode === "url" ? <label>HTTPS origin<input type="url" placeholder="https://api.example.com" required disabled={pending} value={baseUrl} onChange={(event) => setBaseUrl(event.currentTarget.value)} /></label> : <label>Service binding<input placeholder="CUSTOMER_API" required disabled={pending} value={serviceBinding} onChange={(event) => setServiceBinding(event.currentTarget.value)} /></label>}
189
+ <label>Allowed methods<input required disabled={pending} value={methods} onChange={(event) => setMethods(event.currentTarget.value)} /></label>
190
+ <label>Allowed paths<input required disabled={pending} value={paths} onChange={(event) => setPaths(event.currentTarget.value)} /></label>
191
+ <label>Credential strategy<select disabled={pending || targetMode === "service"} value={credential} onChange={(event) => { setCredential(event.currentTarget.value as CredentialStrategy); setSecretReference(""); }}><option value="none">None</option><option value="bearer">Bearer token</option><option value="hmac">HMAC signing</option><option value="static_header">Static header</option><option value="service_binding">Service binding</option></select></label>
192
+ {needsSecret ? <label className="wide-field">Worker secret binding <small>Enter the binding name, never the credential value.</small><input pattern="(?:CONNECTION|OAUTH|ACTION|NOTIFICATION|EMAIL|AI)_[A-Z0-9_]{2,96}" placeholder="CONNECTION_CUSTOMER_API" required disabled={pending} value={secretReference} onChange={(event) => setSecretReference(event.currentTarget.value)} /></label> : null}
193
+ </div>
194
+ </> : null}
195
+ <div className="editor-footer"><p className="error" role="alert">{error}</p><div><button className="secondary" type="button" disabled={pending} onClick={close}>Cancel</button><button className="primary" type="submit" disabled={pending}>{pending ? "Creating…" : `Create ${kind || "item"}`}</button></div></div>
196
+ </form>
197
+ </dialog>;
198
+ }
@@ -0,0 +1,108 @@
1
+ import { useCallback, useEffect, useMemo, useState, useSyncExternalStore } from "react";
2
+ import { PageHeader } from "../components/PageHeader";
3
+ import { readable } from "../lib/format";
4
+ import { ApiRequestError, errorMessage, requestJson } from "../lib/http";
5
+ import { getShellSnapshot, subscribeToShell } from "../shell/store";
6
+ import { openRegistryEditor, registryEvents } from "./events";
7
+ import type { ComponentListResponse, ComponentSummary, ConnectionListResponse, ConnectionSummary, RegistryKind, RegistryValidationResponse } from "./types";
8
+
9
+ function validationMessage(cause: unknown, name: string): string {
10
+ if (cause instanceof ApiRequestError && cause.status === 422 && cause.body && typeof cause.body === "object") {
11
+ const response = cause.body as RegistryValidationResponse;
12
+ const issues = response.validation?.errors?.map((issue) => issue.message).filter(Boolean) || [];
13
+ if (issues.length) return `${name} needs changes: ${issues.join(" ")}`;
14
+ }
15
+ return errorMessage(cause, `${name} could not be checked.`);
16
+ }
17
+
18
+ function RegistryCard({ item, kind, checking, onCheck }: {
19
+ item: ComponentSummary | ConnectionSummary;
20
+ kind: RegistryKind;
21
+ checking: boolean;
22
+ onCheck: () => void;
23
+ }) {
24
+ const connection = kind === "connection" ? item as ConnectionSummary : null;
25
+ const component = kind === "component" ? item as ComponentSummary : null;
26
+ const iconClass = connection ? "connection" : component?.effectClass === "side_effecting" ? "action" : ["workers_ai", "specialist_agent"].includes(component?.kind || "") ? "agent" : "";
27
+ const icon = connection ? "↗" : component?.effectClass === "side_effecting" ? "⚡" : component?.kind === "specialist_agent" ? "✦" : "⬡";
28
+ const meta = connection
29
+ ? [["Type", readable(connection.kind)], ["Version", connection.version ? `v${connection.version}` : "—"], ["Health", readable(connection.healthState || "unknown")]]
30
+ : [["Capability", component?.effectClass === "side_effecting" ? "Action" : "Enrichment"], ["Version", component?.version ? `v${component.version}` : "—"], ["Used by", `${component?.dependencyCount || 0} workflows`]];
31
+ const footer = connection ? connection.baseUrl || connection.serviceBindingName || "No endpoint" : readable(component?.implementationKind || component?.kind || "Not configured");
32
+ return <article className="object-card registry-card">
33
+ <div className="object-card-head"><span className={`object-icon ${iconClass}`} aria-hidden="true">{icon}</span><span className={`badge${item.status !== "active" ? " urgent" : ""}`}>{readable(item.status || "active")}</span></div>
34
+ <h3>{item.name}</h3><p>{item.description || "No description yet."}</p>
35
+ <div className="object-card-meta">{meta.map(([label, value]) => <div key={label}><span>{label}</span><strong>{value}</strong></div>)}</div>
36
+ <div className="object-card-footer"><small>{footer}</small><button className="secondary" type="button" disabled={checking} onClick={onCheck}>{checking ? "Checking…" : "Check"}</button></div>
37
+ </article>;
38
+ }
39
+
40
+ export function RegistryWorkspace() {
41
+ const shell = useSyncExternalStore(subscribeToShell, getShellSnapshot, getShellSnapshot);
42
+ const canManage = shell.session?.permissions.manageComponents === true;
43
+ const canManageConnections = shell.session?.permissions.manageConnections === true;
44
+ const canPublishComponents = shell.session?.permissions.publishComponents === true;
45
+ const canPublishConnections = shell.session?.permissions.publishConnections === true;
46
+ const [components, setComponents] = useState<ComponentSummary[]>([]);
47
+ const [connections, setConnections] = useState<ConnectionSummary[]>([]);
48
+ const [tab, setTab] = useState<"library" | "connections">("library");
49
+ const [search, setSearch] = useState("");
50
+ const [effect, setEffect] = useState("");
51
+ const [loading, setLoading] = useState(false);
52
+ const [refreshing, setRefreshing] = useState(false);
53
+ const [checking, setChecking] = useState("");
54
+ const [error, setError] = useState("");
55
+ const [notice, setNotice] = useState("");
56
+
57
+ const load = useCallback(async (refresh = false): Promise<void> => {
58
+ if (!canManage) { setComponents([]); setConnections([]); return; }
59
+ if (refresh) setRefreshing(true); else setLoading(true);
60
+ setError("");
61
+ try {
62
+ const [componentResult, connectionResult] = await Promise.all([
63
+ requestJson<ComponentListResponse>("/v1/admin/components"),
64
+ canManageConnections ? requestJson<ConnectionListResponse>("/v1/admin/connections") : Promise.resolve({ connections: [] }),
65
+ ]);
66
+ setComponents(componentResult.components || []);
67
+ setConnections(connectionResult.connections || []);
68
+ } catch (cause) { setError(errorMessage(cause, "The component library could not be loaded.")); }
69
+ finally { setLoading(false); setRefreshing(false); }
70
+ }, [canManage, canManageConnections]);
71
+
72
+ useEffect(() => { if (shell.currentView === "components") void load(); }, [shell.currentView, shell.session?.actor.id, load]);
73
+ useEffect(() => {
74
+ const changed = (): void => { if (shell.currentView === "components") void load(true); };
75
+ window.addEventListener(registryEvents.changed, changed);
76
+ return () => window.removeEventListener(registryEvents.changed, changed);
77
+ }, [shell.currentView, load]);
78
+
79
+ const filtered = useMemo(() => {
80
+ const term = search.trim().toLowerCase();
81
+ return components.filter((component) => (!effect || component.effectClass === effect) && (!term || `${component.name} ${component.description} ${component.kind}`.toLowerCase().includes(term)));
82
+ }, [components, effect, search]);
83
+
84
+ const check = async (item: ComponentSummary | ConnectionSummary, kind: RegistryKind): Promise<void> => {
85
+ setChecking(`${kind}:${item.id}`); setError(""); setNotice("");
86
+ try {
87
+ const result = await requestJson<RegistryValidationResponse>(`/v1/admin/${kind === "connection" ? "connections" : "components"}/${encodeURIComponent(item.id)}/validate`, { method: "POST" });
88
+ setNotice(result.validation.valid ? `${item.name} is valid and ready.` : `${item.name} needs changes.`);
89
+ } catch (cause) { setError(validationMessage(cause, item.name)); }
90
+ finally { setChecking(""); }
91
+ };
92
+
93
+ const items = tab === "library" ? filtered : connections;
94
+ const canCreate = tab === "library" ? canPublishComponents : canPublishConnections;
95
+ return <div className="page-stack" data-react-slice="registry-workspace">
96
+ <PageHeader titleId="components-title" title="Components" description="Create reusable checks, messages, and actions for workflows.">
97
+ <div className="surface-buttons">
98
+ <button className="secondary" type="button" disabled={loading || refreshing} onClick={() => void load(true)}>{refreshing ? "Refreshing…" : "Refresh"}</button>
99
+ {canPublishConnections ? <button className="secondary" type="button" onClick={() => openRegistryEditor("connection")}>New connection</button> : null}
100
+ {canPublishComponents ? <button className="primary" type="button" onClick={() => openRegistryEditor("component")}>New component</button> : null}
101
+ </div>
102
+ </PageHeader>
103
+ <div className="segmented-tabs" role="tablist" aria-label="Component library views"><button className={tab === "library" ? "active" : ""} type="button" role="tab" aria-selected={tab === "library"} onClick={() => setTab("library")}>Library</button><button className={tab === "connections" ? "active" : ""} type="button" role="tab" aria-selected={tab === "connections"} onClick={() => setTab("connections")}>Connections</button></div>
104
+ {tab === "library" ? <div className="component-filters"><label className="search-field"><span aria-hidden="true">⌕</span><input type="search" placeholder="Search components" aria-label="Search components" value={search} onChange={(event) => setSearch(event.currentTarget.value)} /></label><label>Capability<select value={effect} onChange={(event) => setEffect(event.currentTarget.value)}><option value="">All capabilities</option><option value="read_only">Enrichments</option><option value="side_effecting">Actions</option></select></label></div> : null}
105
+ <div className="object-grid registry-list" aria-busy={loading || refreshing}>{loading ? <><div className="skeleton-card" /><div className="skeleton-card" /></> : items.length ? items.map((item) => <RegistryCard item={item} kind={tab === "library" ? "component" : "connection"} checking={checking === `${tab === "library" ? "component" : "connection"}:${item.id}`} onCheck={() => void check(item, tab === "library" ? "component" : "connection")} key={item.id} />) : error ? null : <div className="empty-product"><span aria-hidden="true">{tab === "library" ? "⬡" : "↗"}</span><h2>{tab === "library" ? components.length ? "No components match" : "No components yet" : "No connections yet"}</h2><p>{tab === "library" ? components.length ? "Change the search or capability filter." : "Create a reusable check, message, or action." : "Add an endpoint when a component needs an external service."}</p>{canCreate ? <button className="primary" type="button" onClick={() => openRegistryEditor(tab === "library" ? "component" : "connection")}>{tab === "library" ? "New component" : "New connection"}</button> : null}</div>}</div>
106
+ <p className="form-message" role="status">{notice}</p><p className="error surface-error" role="alert">{error}</p>
107
+ </div>;
108
+ }
@@ -0,0 +1,14 @@
1
+ import type { RegistryKind } from "./types";
2
+
3
+ export const registryEvents = {
4
+ openEditor: "safest:registry:editor-open",
5
+ changed: "safest:registry:changed",
6
+ } as const;
7
+
8
+ export function openRegistryEditor(kind: RegistryKind): void {
9
+ window.dispatchEvent(new CustomEvent(registryEvents.openEditor, { detail: { kind } }));
10
+ }
11
+
12
+ export function announceRegistryChanged(): void {
13
+ window.dispatchEvent(new Event(registryEvents.changed));
14
+ }
@@ -0,0 +1,52 @@
1
+ export type RegistryKind = "component" | "connection";
2
+ export type ComponentEffect = "read_only" | "side_effecting";
3
+
4
+ export interface ComponentSummary {
5
+ id: string;
6
+ key: string;
7
+ name: string;
8
+ description: string;
9
+ kind: string;
10
+ effectClass: ComponentEffect;
11
+ ownerKind: "safest" | "customer";
12
+ status: string;
13
+ activeVersionId: string | null;
14
+ version: number | null;
15
+ implementationKind: string | null;
16
+ riskClass: string | null;
17
+ connectionVersionId: string | null;
18
+ dependencyCount: number;
19
+ }
20
+
21
+ export interface ConnectionSummary {
22
+ id: string;
23
+ key: string;
24
+ name: string;
25
+ description: string;
26
+ kind: string;
27
+ status: string;
28
+ activeVersionId: string | null;
29
+ version: number | null;
30
+ baseUrl: string | null;
31
+ serviceBindingName: string | null;
32
+ credentialStrategy: string | null;
33
+ healthState: string | null;
34
+ dependencyCount: number;
35
+ }
36
+
37
+ export interface ComponentListResponse { components: ComponentSummary[] }
38
+ export interface ConnectionListResponse { connections: ConnectionSummary[] }
39
+
40
+ export interface RegistryValidationIssue {
41
+ code?: string;
42
+ message: string;
43
+ path?: string;
44
+ }
45
+
46
+ export interface RegistryValidation {
47
+ valid: boolean;
48
+ errors?: RegistryValidationIssue[];
49
+ warnings?: RegistryValidationIssue[];
50
+ }
51
+
52
+ export interface RegistryValidationResponse { validation: RegistryValidation }