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,753 @@
1
+ import { canonicalJson } from "./audit";
2
+ import { ApiError } from "./report-http";
3
+ import type { OperatorSession } from "./report-types";
4
+ import { validateJsonSchemaValue } from "./workflow-expressions";
5
+ import type { JsonObject, JsonValue } from "./workflow-platform-types";
6
+ import type {
7
+ ComponentCreateInput,
8
+ ComponentVersionInput,
9
+ ConnectionCreateInput,
10
+ ConnectionVersionInput,
11
+ } from "./platform-registry-validation";
12
+
13
+ interface MutationRow {
14
+ action: string;
15
+ actor_id: string;
16
+ response_json: string;
17
+ }
18
+
19
+ function now(): string {
20
+ return new Date().toISOString();
21
+ }
22
+
23
+ function parsedJson<T>(value: string | null, fallback: T): T {
24
+ if (!value) return fallback;
25
+ try {
26
+ return JSON.parse(value) as T;
27
+ } catch {
28
+ return fallback;
29
+ }
30
+ }
31
+
32
+ async function replay(db: D1Database, key: string, action: string, actorId: string): Promise<Record<string, unknown> | null> {
33
+ const row = await db.prepare(`SELECT action, actor_id, response_json FROM configuration_mutations WHERE idempotency_key = ?1 LIMIT 1`)
34
+ .bind(key).first<MutationRow>();
35
+ if (!row) return null;
36
+ if (row.action !== action || row.actor_id !== actorId) throw new ApiError(409, "idempotency_key_reused", "This Idempotency-Key was already used for another registry mutation.");
37
+ return parsedJson(row.response_json, {});
38
+ }
39
+
40
+ function audit(
41
+ db: D1Database,
42
+ session: OperatorSession,
43
+ key: string,
44
+ action: string,
45
+ targetType: string,
46
+ targetId: string,
47
+ details: unknown,
48
+ createdAt: string,
49
+ ): D1PreparedStatement {
50
+ return db.prepare(`
51
+ INSERT OR IGNORE INTO audit_events (
52
+ id, idempotency_key, action, actor_type, actor_id, target_type, target_id, details_json, created_at
53
+ ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)
54
+ `).bind(crypto.randomUUID(), key, action, session.actor.type, session.actor.id, targetType, targetId, canonicalJson(details), createdAt);
55
+ }
56
+
57
+ function connectionVersionInsert(
58
+ db: D1Database,
59
+ definitionId: string,
60
+ versionId: string,
61
+ version: number,
62
+ input: ConnectionVersionInput,
63
+ actorId: string,
64
+ createdAt: string,
65
+ ): D1PreparedStatement {
66
+ return db.prepare(`
67
+ INSERT INTO connection_versions (
68
+ id, connection_id, version, base_url, service_binding_name,
69
+ allowed_hosts_json, allowed_methods_json, allowed_path_prefixes_json,
70
+ redirect_policy_json, request_limit_bytes, response_limit_bytes,
71
+ response_content_types_json, timeout_ms, concurrency_limit,
72
+ requests_per_window, window_seconds, credential_strategy, secret_reference,
73
+ signing_strategy_json, oauth_configuration_json, published_at, created_by, created_at
74
+ ) VALUES (
75
+ ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, '{"follow":false,"maximum":0}',
76
+ ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20, ?21, ?20
77
+ )
78
+ `).bind(
79
+ versionId, definitionId, version, input.baseUrl, input.serviceBindingName,
80
+ canonicalJson(input.allowedHosts), canonicalJson(input.allowedMethods),
81
+ canonicalJson(input.allowedPathPrefixes), input.requestLimitBytes, input.responseLimitBytes,
82
+ canonicalJson(input.responseContentTypes), input.timeoutMs, input.concurrencyLimit,
83
+ input.requestsPerWindow, input.windowSeconds, input.credentialStrategy, input.secretReference,
84
+ canonicalJson(input.signingStrategy), canonicalJson(input.oauthConfiguration), createdAt, actorId,
85
+ );
86
+ }
87
+
88
+ export async function listConnections(db: D1Database, includeArchived = false): Promise<Record<string, unknown>[]> {
89
+ const rows = await db.prepare(`
90
+ SELECT d.id, d.connection_key AS key, d.name, d.description, d.kind, d.status,
91
+ d.active_version_id AS activeVersionId, d.created_by AS createdBy,
92
+ d.created_at AS createdAt, d.updated_at AS updatedAt,
93
+ v.version, v.base_url AS baseUrl, v.service_binding_name AS serviceBindingName,
94
+ v.credential_strategy AS credentialStrategy, v.published_at AS publishedAt,
95
+ h.state AS healthState, h.last_success_at AS lastSuccessAt,
96
+ h.last_error_code AS lastErrorCode,
97
+ (SELECT COUNT(*) FROM component_versions cv WHERE cv.connection_version_id = v.id) AS dependencyCount
98
+ FROM connection_definitions d
99
+ LEFT JOIN connection_versions v ON v.id = d.active_version_id
100
+ LEFT JOIN connection_health h ON h.connection_version_id = v.id
101
+ WHERE (?1 = 1 OR d.status != 'archived')
102
+ ORDER BY d.name, d.id
103
+ `).bind(includeArchived ? 1 : 0).all<Record<string, unknown>>();
104
+ return rows.results;
105
+ }
106
+
107
+ export async function loadConnection(db: D1Database, id: string): Promise<Record<string, unknown> | null> {
108
+ const definition = await db.prepare(`SELECT * FROM connection_definitions WHERE id = ?1 LIMIT 1`).bind(id).first<Record<string, unknown>>();
109
+ if (!definition) return null;
110
+ const versions = await db.prepare(`
111
+ SELECT v.id, v.version, v.base_url AS baseUrl, v.service_binding_name AS serviceBindingName,
112
+ v.allowed_hosts_json AS allowedHostsJson, v.allowed_methods_json AS allowedMethodsJson,
113
+ v.allowed_path_prefixes_json AS allowedPathPrefixesJson,
114
+ v.request_limit_bytes AS requestLimitBytes, v.response_limit_bytes AS responseLimitBytes,
115
+ v.response_content_types_json AS responseContentTypesJson, v.timeout_ms AS timeoutMs,
116
+ v.concurrency_limit AS concurrencyLimit, v.requests_per_window AS requestsPerWindow,
117
+ v.window_seconds AS windowSeconds, v.credential_strategy AS credentialStrategy,
118
+ v.secret_reference AS secretReference, v.signing_strategy_json AS signingStrategyJson,
119
+ v.oauth_configuration_json AS oauthConfigurationJson,
120
+ v.published_at AS publishedAt, v.retired_at AS retiredAt,
121
+ v.created_by AS createdBy, h.state AS healthState,
122
+ h.consecutive_failures AS consecutiveFailures, h.last_success_at AS lastSuccessAt,
123
+ h.last_error_code AS lastErrorCode
124
+ FROM connection_versions v LEFT JOIN connection_health h ON h.connection_version_id = v.id
125
+ WHERE v.connection_id = ?1 ORDER BY v.version DESC, v.id DESC
126
+ `).bind(id).all<Record<string, unknown>>();
127
+ return {
128
+ definition,
129
+ versions: versions.results.map((version) => ({
130
+ ...version,
131
+ allowedHosts: parsedJson(String(version.allowedHostsJson ?? ""), []),
132
+ allowedMethods: parsedJson(String(version.allowedMethodsJson ?? ""), []),
133
+ allowedPathPrefixes: parsedJson(String(version.allowedPathPrefixesJson ?? ""), []),
134
+ responseContentTypes: parsedJson(String(version.responseContentTypesJson ?? ""), []),
135
+ signingStrategy: parsedJson(String(version.signingStrategyJson ?? ""), {}),
136
+ oauthConfiguration: parsedJson(String(version.oauthConfigurationJson ?? ""), {}),
137
+ secretReference: version.secretReference ? "configured" : null,
138
+ allowedHostsJson: undefined,
139
+ allowedMethodsJson: undefined,
140
+ allowedPathPrefixesJson: undefined,
141
+ responseContentTypesJson: undefined,
142
+ signingStrategyJson: undefined,
143
+ oauthConfigurationJson: undefined,
144
+ })),
145
+ };
146
+ }
147
+
148
+ function metadataText(value: unknown, field: string, minimum: number, maximum: number): string {
149
+ if (typeof value !== "string" || value.trim().length < minimum || value.trim().length > maximum || /[\u0000-\u001f\u007f]/u.test(value)) {
150
+ throw new ApiError(400, "registry_metadata_invalid", `${field} must contain ${minimum} to ${maximum} safe characters.`);
151
+ }
152
+ return value.trim();
153
+ }
154
+
155
+ export async function updateConnectionDefinition(
156
+ db: D1Database,
157
+ connectionId: string,
158
+ input: Record<string, unknown>,
159
+ session: OperatorSession,
160
+ idempotencyKey: string,
161
+ ): Promise<{ connectionId: string; name: string; description: string; status: string; idempotentReplay: boolean }> {
162
+ const existing = await replay(db, idempotencyKey, "connection.update", session.actor.id);
163
+ if (existing) return {
164
+ connectionId: String(existing.connectionId), name: String(existing.name), description: String(existing.description),
165
+ status: String(existing.status), idempotentReplay: true,
166
+ };
167
+ const unsupported = Object.keys(input).find((key) => !["name", "description", "status"].includes(key));
168
+ if (unsupported) throw new ApiError(400, "registry_metadata_invalid", `Unsupported connection field: ${unsupported}.`);
169
+ const current = await db.prepare(`SELECT name, description, status, active_version_id AS activeVersionId FROM connection_definitions WHERE id = ?1 LIMIT 1`)
170
+ .bind(connectionId).first<{ name: string; description: string; status: string; activeVersionId: string | null }>();
171
+ if (!current) throw new ApiError(404, "connection_not_found", "The connection does not exist.");
172
+ if (current.status === "archived") throw new ApiError(409, "connection_archived", "Archived connections are immutable.");
173
+ const name = input.name === undefined ? current.name : metadataText(input.name, "name", 2, 160);
174
+ const description = input.description === undefined ? current.description : metadataText(input.description, "description", 0, 2_000);
175
+ const status = input.status === undefined ? current.status : String(input.status);
176
+ if (!["paused", "archived"].includes(status) && status !== current.status) {
177
+ throw new ApiError(409, "connection_lifecycle_invalid", "Use the health-checked resume endpoint to activate a connection.");
178
+ }
179
+ if (status === "archived") {
180
+ const dependency = await db.prepare(`
181
+ SELECT COUNT(*) AS count FROM component_versions cv JOIN connection_versions v ON v.id = cv.connection_version_id
182
+ WHERE v.connection_id = ?1 AND cv.retired_at IS NULL
183
+ `).bind(connectionId).first<{ count: number }>();
184
+ if (Number(dependency?.count ?? 0) > 0) throw new ApiError(409, "connection_has_dependencies", "Retire dependent component versions before archiving this connection.");
185
+ }
186
+ const timestamp = now();
187
+ const response = { connectionId, name, description, status };
188
+ const statements = [
189
+ db.prepare(`UPDATE connection_definitions SET name = ?2, description = ?3, status = ?4, updated_at = ?5 WHERE id = ?1`).bind(connectionId, name, description, status, timestamp),
190
+ db.prepare(`INSERT INTO configuration_mutations (idempotency_key, action, actor_id, target_id, response_json, created_at) VALUES (?1, 'connection.update', ?2, ?3, ?4, ?5)`)
191
+ .bind(idempotencyKey, session.actor.id, connectionId, canonicalJson(response), timestamp),
192
+ audit(db, session, `connection-update:${idempotencyKey}`, "connection.updated", "connection", connectionId, { name, status }, timestamp),
193
+ ];
194
+ if (current.activeVersionId && status === "paused") statements.push(
195
+ db.prepare(`UPDATE connection_health SET state = 'paused', last_error_code = 'manually_paused', revision = revision + 1, updated_at = ?2 WHERE connection_version_id = ?1`)
196
+ .bind(current.activeVersionId, timestamp),
197
+ );
198
+ await db.batch(statements);
199
+ return { ...response, idempotentReplay: false };
200
+ }
201
+
202
+ export async function createConnection(
203
+ db: D1Database,
204
+ input: ConnectionCreateInput,
205
+ session: OperatorSession,
206
+ idempotencyKey: string,
207
+ ): Promise<{ connectionId: string; versionId: string; version: number; idempotentReplay: boolean }> {
208
+ const existingMutation = await replay(db, idempotencyKey, "connection.create", session.actor.id);
209
+ if (existingMutation) return {
210
+ connectionId: String(existingMutation.connectionId), versionId: String(existingMutation.versionId),
211
+ version: Number(existingMutation.version), idempotentReplay: true,
212
+ };
213
+ if (await db.prepare(`SELECT id FROM connection_definitions WHERE installation_id = 'default' AND connection_key = ?1`).bind(input.key).first()) {
214
+ throw new ApiError(409, "connection_key_exists", "A connection with this key already exists.");
215
+ }
216
+ const connectionId = crypto.randomUUID();
217
+ const versionId = crypto.randomUUID();
218
+ const createdAt = now();
219
+ const response = { connectionId, versionId, version: 1 };
220
+ await db.batch([
221
+ db.prepare(`
222
+ INSERT INTO connection_definitions (
223
+ id, installation_id, connection_key, name, description, kind, status,
224
+ active_version_id, created_by, created_at, updated_at
225
+ ) VALUES (?1, 'default', ?2, ?3, ?4, ?5, 'active', ?6, ?7, ?8, ?8)
226
+ `).bind(connectionId, input.key, input.name, input.description, input.kind, versionId, session.actor.id, createdAt),
227
+ connectionVersionInsert(db, connectionId, versionId, 1, input.version, session.actor.id, createdAt),
228
+ db.prepare(`
229
+ INSERT INTO connection_health (connection_version_id, state, consecutive_failures, revision, updated_at)
230
+ VALUES (?1, 'unknown', 0, 1, ?2)
231
+ `).bind(versionId, createdAt),
232
+ db.prepare(`
233
+ INSERT INTO configuration_mutations (idempotency_key, action, actor_id, target_id, response_json, created_at)
234
+ VALUES (?1, 'connection.create', ?2, ?3, ?4, ?5)
235
+ `).bind(idempotencyKey, session.actor.id, connectionId, canonicalJson(response), createdAt),
236
+ audit(db, session, `connection-create:${idempotencyKey}`, "connection.created", "connection", connectionId, {
237
+ key: input.key, kind: input.kind, versionId, credentialStrategy: input.version.credentialStrategy,
238
+ }, createdAt),
239
+ ]);
240
+ return { ...response, idempotentReplay: false };
241
+ }
242
+
243
+ export async function publishConnectionVersion(
244
+ db: D1Database,
245
+ connectionId: string,
246
+ input: ConnectionVersionInput,
247
+ session: OperatorSession,
248
+ idempotencyKey: string,
249
+ ): Promise<{ connectionId: string; versionId: string; version: number; idempotentReplay: boolean }> {
250
+ const existingMutation = await replay(db, idempotencyKey, "connection.version.publish", session.actor.id);
251
+ if (existingMutation) return {
252
+ connectionId: String(existingMutation.connectionId), versionId: String(existingMutation.versionId),
253
+ version: Number(existingMutation.version), idempotentReplay: true,
254
+ };
255
+ const definition = await db.prepare(`SELECT status FROM connection_definitions WHERE id = ?1`).bind(connectionId).first<{ status: string }>();
256
+ if (!definition) throw new ApiError(404, "connection_not_found", "The connection does not exist.");
257
+ if (definition.status === "archived") throw new ApiError(409, "connection_archived", "Archived connections cannot receive versions.");
258
+ const latest = await db.prepare(`SELECT COALESCE(MAX(version), 0) AS version FROM connection_versions WHERE connection_id = ?1`)
259
+ .bind(connectionId).first<{ version: number }>();
260
+ const version = Number(latest?.version ?? 0) + 1;
261
+ const versionId = crypto.randomUUID();
262
+ const createdAt = now();
263
+ const response = { connectionId, versionId, version };
264
+ await db.batch([
265
+ connectionVersionInsert(db, connectionId, versionId, version, input, session.actor.id, createdAt),
266
+ db.prepare(`INSERT INTO connection_health (connection_version_id, state, consecutive_failures, revision, updated_at) VALUES (?1, 'unknown', 0, 1, ?2)`).bind(versionId, createdAt),
267
+ db.prepare(`UPDATE connection_definitions SET active_version_id = ?2, status = 'active', updated_at = ?3 WHERE id = ?1`).bind(connectionId, versionId, createdAt),
268
+ db.prepare(`
269
+ INSERT INTO configuration_mutations (idempotency_key, action, actor_id, target_id, response_json, created_at)
270
+ VALUES (?1, 'connection.version.publish', ?2, ?3, ?4, ?5)
271
+ `).bind(idempotencyKey, session.actor.id, versionId, canonicalJson(response), createdAt),
272
+ audit(db, session, `connection-publish:${idempotencyKey}`, "connection.version_published", "connection", connectionId, {
273
+ versionId, version, credentialStrategy: input.credentialStrategy,
274
+ }, createdAt),
275
+ ]);
276
+ return { ...response, idempotentReplay: false };
277
+ }
278
+
279
+ export async function setConnectionStatus(
280
+ db: D1Database,
281
+ connectionId: string,
282
+ status: "active" | "paused",
283
+ session: OperatorSession,
284
+ idempotencyKey: string,
285
+ ): Promise<{ connectionId: string; status: string; idempotentReplay: boolean }> {
286
+ const action = `connection.${status}`;
287
+ const existing = await replay(db, idempotencyKey, action, session.actor.id);
288
+ if (existing) return { connectionId: String(existing.connectionId), status: String(existing.status), idempotentReplay: true };
289
+ const definition = await db.prepare(`SELECT active_version_id AS activeVersionId, status FROM connection_definitions WHERE id = ?1 LIMIT 1`)
290
+ .bind(connectionId).first<{ activeVersionId: string | null; status: string }>();
291
+ if (!definition) throw new ApiError(404, "connection_not_found", "The connection does not exist.");
292
+ if (definition.status === "archived") throw new ApiError(409, "connection_archived", "Archived connections cannot change status.");
293
+ if (!definition.activeVersionId) throw new ApiError(409, "connection_version_missing", "The connection has no active version.");
294
+ const timestamp = now();
295
+ const response = { connectionId, status };
296
+ await db.batch([
297
+ db.prepare(`UPDATE connection_definitions SET status = ?2, updated_at = ?3 WHERE id = ?1`).bind(connectionId, status, timestamp),
298
+ db.prepare(`UPDATE connection_health SET state = ?2, consecutive_failures = CASE WHEN ?2 = 'paused' THEN consecutive_failures ELSE 0 END, opened_at = NULL, next_probe_at = NULL, last_error_code = CASE WHEN ?2 = 'paused' THEN 'manually_paused' ELSE NULL END, revision = revision + 1, updated_at = ?3 WHERE connection_version_id = ?1`)
299
+ .bind(definition.activeVersionId, status === "paused" ? "paused" : "unknown", timestamp),
300
+ db.prepare(`INSERT INTO configuration_mutations (idempotency_key, action, actor_id, target_id, response_json, created_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6)`)
301
+ .bind(idempotencyKey, action, session.actor.id, connectionId, canonicalJson(response), timestamp),
302
+ audit(db, session, `${action}:${idempotencyKey}`, `connection.${status}`, "connection", connectionId, { activeVersionId: definition.activeVersionId }, timestamp),
303
+ ]);
304
+ return { ...response, idempotentReplay: false };
305
+ }
306
+
307
+ export async function connectionDependencies(db: D1Database, connectionId: string): Promise<Record<string, unknown>> {
308
+ const definition = await db.prepare(`SELECT id FROM connection_definitions WHERE id = ?1 LIMIT 1`).bind(connectionId).first();
309
+ if (!definition) throw new ApiError(404, "connection_not_found", "The connection does not exist.");
310
+ const [components, workflows, modelAliases] = await Promise.all([
311
+ db.prepare(`
312
+ SELECT d.id AS componentId, d.name, v.id AS componentVersionId, v.version
313
+ FROM component_versions v JOIN component_definitions d ON d.id = v.definition_id
314
+ JOIN connection_versions cv ON cv.id = v.connection_version_id
315
+ WHERE cv.connection_id = ?1 AND v.retired_at IS NULL ORDER BY d.name, v.version DESC
316
+ `).bind(connectionId).all<Record<string, unknown>>(),
317
+ db.prepare(`
318
+ SELECT DISTINCT d.id AS workflowId, d.name, wv.id AS workflowVersionId, wv.version
319
+ FROM workflow_dependencies dep
320
+ JOIN workflow_versions wv ON wv.id = dep.workflow_version_id
321
+ JOIN workflow_definitions d ON d.id = wv.workflow_id
322
+ JOIN connection_versions cv ON cv.id = dep.dependency_version_id
323
+ WHERE dep.dependency_type = 'connection' AND cv.connection_id = ?1
324
+ ORDER BY d.name, wv.version DESC
325
+ `).bind(connectionId).all<Record<string, unknown>>(),
326
+ db.prepare(`
327
+ SELECT a.id AS aliasId, a.name, v.id AS aliasVersionId, v.version
328
+ FROM model_alias_versions v JOIN model_aliases a ON a.id = v.alias_id
329
+ JOIN connection_versions cv ON cv.id = v.connection_version_id
330
+ WHERE cv.connection_id = ?1 AND v.retired_at IS NULL ORDER BY a.name, v.version DESC
331
+ `).bind(connectionId).all<Record<string, unknown>>(),
332
+ ]);
333
+ return { components: components.results, workflows: workflows.results, modelAliases: modelAliases.results };
334
+ }
335
+
336
+ export async function validateConnectionDefinition(db: D1Database, connectionId: string): Promise<Record<string, unknown>> {
337
+ const row = await db.prepare(`
338
+ SELECT d.status, d.active_version_id AS activeVersionId, v.base_url AS baseUrl,
339
+ v.service_binding_name AS serviceBindingName, v.allowed_hosts_json AS allowedHostsJson,
340
+ v.allowed_methods_json AS allowedMethodsJson, v.allowed_path_prefixes_json AS allowedPathsJson,
341
+ v.credential_strategy AS credentialStrategy, v.secret_reference AS secretReference,
342
+ h.state AS healthState
343
+ FROM connection_definitions d LEFT JOIN connection_versions v ON v.id = d.active_version_id
344
+ LEFT JOIN connection_health h ON h.connection_version_id = v.id
345
+ WHERE d.id = ?1 LIMIT 1
346
+ `).bind(connectionId).first<Record<string, unknown>>();
347
+ if (!row) throw new ApiError(404, "connection_not_found", "The connection does not exist.");
348
+ const errors: string[] = [];
349
+ if (!row.activeVersionId) errors.push("active_version_missing");
350
+ if (Boolean(row.baseUrl) === Boolean(row.serviceBindingName)) errors.push("destination_invalid");
351
+ if (!parsedJson(String(row.allowedMethodsJson ?? ""), [] as string[]).length) errors.push("allowed_methods_empty");
352
+ if (!parsedJson(String(row.allowedPathsJson ?? ""), [] as string[]).length) errors.push("allowed_paths_empty");
353
+ if (!["none", "oauth2_authorization_code", "service_binding"].includes(String(row.credentialStrategy)) && !row.secretReference) errors.push("secret_reference_missing");
354
+ return { valid: errors.length === 0, errors, activeVersionId: row.activeVersionId, status: row.status, healthState: row.healthState };
355
+ }
356
+
357
+ function fixtureEvidence(input: ComponentVersionInput): boolean {
358
+ const evidence = input.compatibility.fixture_evidence;
359
+ return Boolean(evidence && typeof evidence === "object" && !Array.isArray(evidence) && evidence.passed === true);
360
+ }
361
+
362
+ function componentFixtureStatements(
363
+ db: D1Database,
364
+ versionId: string,
365
+ input: ComponentVersionInput,
366
+ actorId: string,
367
+ createdAt: string,
368
+ ): D1PreparedStatement[] {
369
+ const evidence = input.compatibility.fixture_evidence;
370
+ const fixtures = evidence && typeof evidence === "object" && !Array.isArray(evidence) && Array.isArray(evidence.fixtures)
371
+ ? evidence.fixtures : [];
372
+ return fixtures.flatMap((entry) => {
373
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) return [];
374
+ const fixtureId = crypto.randomUUID();
375
+ const testRunId = crypto.randomUUID();
376
+ const name = typeof entry.name === "string" ? entry.name : "fixture";
377
+ const fixtureInput = entry.input && typeof entry.input === "object" && !Array.isArray(entry.input) ? entry.input : {};
378
+ const expected = entry.expected_output && typeof entry.expected_output === "object" && !Array.isArray(entry.expected_output) ? entry.expected_output : {};
379
+ const expectedStatus = typeof entry.expected_status === "string" ? entry.expected_status : "succeeded";
380
+ return [
381
+ db.prepare(`
382
+ INSERT INTO component_fixtures (
383
+ id, component_version_id, name, input_json, expected_json,
384
+ expected_status, created_by, created_at
385
+ ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)
386
+ `).bind(fixtureId, versionId, name, canonicalJson(fixtureInput), canonicalJson(expected), expectedStatus, actorId, createdAt),
387
+ db.prepare(`
388
+ INSERT INTO component_test_runs (
389
+ id, component_version_id, fixture_id, status, output_json,
390
+ started_by, started_at, finished_at
391
+ ) VALUES (?1, ?2, ?3, 'passed', ?4, ?5, ?6, ?6)
392
+ `).bind(testRunId, versionId, fixtureId, canonicalJson({
393
+ validation: "schema_only",
394
+ expectedStatus,
395
+ mode: typeof entry.mode === "string" ? entry.mode : "simulated",
396
+ }), actorId, createdAt),
397
+ ];
398
+ });
399
+ }
400
+
401
+ async function requireComponentConnection(db: D1Database, input: ComponentVersionInput): Promise<void> {
402
+ if (!input.connectionVersionId) return;
403
+ const connection = await db.prepare(`
404
+ SELECT v.id FROM connection_versions v JOIN connection_definitions d ON d.id = v.connection_id
405
+ WHERE v.id = ?1 AND v.retired_at IS NULL AND d.status = 'active' LIMIT 1
406
+ `).bind(input.connectionVersionId).first();
407
+ if (!connection) throw new ApiError(409, "component_connection_unavailable", "The pinned connection version is unavailable.");
408
+ }
409
+
410
+ function componentVersionInsert(
411
+ db: D1Database,
412
+ definitionId: string,
413
+ versionId: string,
414
+ version: number,
415
+ input: ComponentVersionInput,
416
+ actorId: string,
417
+ createdAt: string,
418
+ ): D1PreparedStatement {
419
+ return db.prepare(`
420
+ INSERT INTO component_versions (
421
+ id, definition_id, version, implementation_kind, implementation_reference,
422
+ input_schema_json, output_schema_json, allowed_input_fields_json,
423
+ data_classifications_json, connection_version_id, risk_class,
424
+ timeout_policy_json, retry_policy_json, cache_policy_json,
425
+ concurrency_policy_json, budget_policy_json, approval_policy_json,
426
+ tool_manifest_json, compatibility_json, published_at, created_by, created_at
427
+ ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20, ?21, ?20)
428
+ `).bind(
429
+ versionId, definitionId, version, input.implementationKind, input.implementationReference,
430
+ canonicalJson(input.inputSchema), canonicalJson(input.outputSchema), canonicalJson(input.allowedInputFields),
431
+ canonicalJson(input.dataClassifications), input.connectionVersionId, input.riskClass,
432
+ canonicalJson(input.timeoutPolicy), canonicalJson(input.retryPolicy), canonicalJson(input.cachePolicy),
433
+ canonicalJson(input.concurrencyPolicy), canonicalJson(input.budgetPolicy), canonicalJson(input.approvalPolicy),
434
+ canonicalJson(input.toolManifest), canonicalJson(input.compatibility), createdAt, actorId,
435
+ );
436
+ }
437
+
438
+ export async function listComponents(db: D1Database, includeArchived = false): Promise<Record<string, unknown>[]> {
439
+ const rows = await db.prepare(`
440
+ SELECT d.id, d.component_key AS key, d.name, d.description,
441
+ d.component_kind AS kind, d.effect_class AS effectClass,
442
+ d.owner_kind AS ownerKind, d.status, d.active_version_id AS activeVersionId,
443
+ d.created_by AS createdBy, d.created_at AS createdAt, d.updated_at AS updatedAt,
444
+ v.version, v.implementation_kind AS implementationKind,
445
+ v.risk_class AS riskClass, v.connection_version_id AS connectionVersionId,
446
+ v.published_at AS publishedAt,
447
+ (SELECT COUNT(*) FROM workflow_dependencies wd
448
+ WHERE wd.dependency_type = 'component' AND wd.dependency_version_id = v.id) AS dependencyCount
449
+ FROM component_definitions d LEFT JOIN component_versions v ON v.id = d.active_version_id
450
+ WHERE (?1 = 1 OR d.status != 'archived')
451
+ ORDER BY d.name, d.id
452
+ `).bind(includeArchived ? 1 : 0).all<Record<string, unknown>>();
453
+ return rows.results;
454
+ }
455
+
456
+ export async function loadComponent(db: D1Database, id: string): Promise<Record<string, unknown> | null> {
457
+ const definition = await db.prepare(`SELECT * FROM component_definitions WHERE id = ?1 LIMIT 1`).bind(id).first<Record<string, unknown>>();
458
+ if (!definition) return null;
459
+ const [versions, fixtures, testRuns] = await Promise.all([db.prepare(`
460
+ SELECT id, version, implementation_kind AS implementationKind,
461
+ implementation_reference AS implementationReference,
462
+ input_schema_json AS inputSchemaJson, output_schema_json AS outputSchemaJson,
463
+ allowed_input_fields_json AS allowedInputFieldsJson,
464
+ data_classifications_json AS dataClassificationsJson,
465
+ connection_version_id AS connectionVersionId, risk_class AS riskClass,
466
+ timeout_policy_json AS timeoutPolicyJson, retry_policy_json AS retryPolicyJson,
467
+ cache_policy_json AS cachePolicyJson, concurrency_policy_json AS concurrencyPolicyJson,
468
+ budget_policy_json AS budgetPolicyJson, approval_policy_json AS approvalPolicyJson,
469
+ tool_manifest_json AS toolManifestJson, compatibility_json AS compatibilityJson,
470
+ published_at AS publishedAt, retired_at AS retiredAt, created_by AS createdBy
471
+ FROM component_versions WHERE definition_id = ?1 ORDER BY version DESC, id DESC
472
+ `).bind(id).all<Record<string, unknown>>(),
473
+ db.prepare(`
474
+ SELECT f.id, f.component_version_id AS componentVersionId, f.name,
475
+ f.input_json AS inputJson, f.expected_json AS expectedJson,
476
+ f.expected_status AS expectedStatus, f.created_by AS createdBy,
477
+ f.created_at AS createdAt
478
+ FROM component_fixtures f JOIN component_versions v ON v.id = f.component_version_id
479
+ WHERE v.definition_id = ?1 ORDER BY f.created_at DESC, f.id DESC
480
+ `).bind(id).all<Record<string, unknown>>(),
481
+ db.prepare(`
482
+ SELECT t.id, t.component_version_id AS componentVersionId, t.fixture_id AS fixtureId,
483
+ t.status, t.output_json AS outputJson, t.error_code AS errorCode,
484
+ t.started_by AS startedBy, t.started_at AS startedAt, t.finished_at AS finishedAt
485
+ FROM component_test_runs t JOIN component_versions v ON v.id = t.component_version_id
486
+ WHERE v.definition_id = ?1 ORDER BY t.started_at DESC, t.id DESC LIMIT 200
487
+ `).bind(id).all<Record<string, unknown>>()]);
488
+ const jsonFields = ["inputSchema", "outputSchema", "allowedInputFields", "dataClassifications", "timeoutPolicy", "retryPolicy", "cachePolicy", "concurrencyPolicy", "budgetPolicy", "approvalPolicy", "toolManifest", "compatibility"];
489
+ return {
490
+ definition,
491
+ versions: versions.results.map((version) => {
492
+ const output: Record<string, unknown> = { ...version };
493
+ for (const field of jsonFields) {
494
+ const source = `${field}Json`;
495
+ output[field] = parsedJson(String(output[source] ?? ""), field.endsWith("Fields") || field === "toolManifest" ? [] : {});
496
+ delete output[source];
497
+ }
498
+ return output;
499
+ }),
500
+ fixtures: fixtures.results.map((fixture) => ({
501
+ ...fixture,
502
+ input: parsedJson(String(fixture.inputJson ?? ""), {}),
503
+ expected: parsedJson(String(fixture.expectedJson ?? ""), {}),
504
+ inputJson: undefined,
505
+ expectedJson: undefined,
506
+ })),
507
+ testRuns: testRuns.results.map((testRun) => ({
508
+ ...testRun,
509
+ output: testRun.outputJson ? parsedJson(String(testRun.outputJson), {}) : null,
510
+ outputJson: undefined,
511
+ })),
512
+ };
513
+ }
514
+
515
+ export async function updateComponentDefinition(
516
+ db: D1Database,
517
+ componentId: string,
518
+ input: Record<string, unknown>,
519
+ session: OperatorSession,
520
+ idempotencyKey: string,
521
+ ): Promise<{ componentId: string; name: string; description: string; status: string; idempotentReplay: boolean }> {
522
+ const existing = await replay(db, idempotencyKey, "component.update", session.actor.id);
523
+ if (existing) return {
524
+ componentId: String(existing.componentId), name: String(existing.name), description: String(existing.description),
525
+ status: String(existing.status), idempotentReplay: true,
526
+ };
527
+ const unsupported = Object.keys(input).find((key) => !["name", "description", "status"].includes(key));
528
+ if (unsupported) throw new ApiError(400, "registry_metadata_invalid", `Unsupported component field: ${unsupported}.`);
529
+ const current = await db.prepare(`SELECT name, description, status FROM component_definitions WHERE id = ?1 LIMIT 1`)
530
+ .bind(componentId).first<{ name: string; description: string; status: string }>();
531
+ if (!current) throw new ApiError(404, "component_not_found", "The component does not exist.");
532
+ if (current.status === "archived") throw new ApiError(409, "component_archived", "Archived components are immutable.");
533
+ const name = input.name === undefined ? current.name : metadataText(input.name, "name", 2, 160);
534
+ const description = input.description === undefined ? current.description : metadataText(input.description, "description", 0, 2_000);
535
+ const status = input.status === undefined ? current.status : String(input.status);
536
+ if (![current.status, "paused", "deprecated", "archived"].includes(status)) {
537
+ throw new ApiError(409, "component_lifecycle_invalid", "Use the readiness-checked resume endpoint to activate a component.");
538
+ }
539
+ if (status === "archived") {
540
+ const dependency = await db.prepare(`
541
+ SELECT COUNT(*) AS count FROM workflow_dependencies dep JOIN component_versions v ON v.id = dep.dependency_version_id
542
+ WHERE dep.dependency_type = 'component' AND v.definition_id = ?1
543
+ `).bind(componentId).first<{ count: number }>();
544
+ if (Number(dependency?.count ?? 0) > 0) throw new ApiError(409, "component_has_dependencies", "Retire dependent workflow versions before archiving this component.");
545
+ }
546
+ const timestamp = now();
547
+ const response = { componentId, name, description, status };
548
+ await db.batch([
549
+ db.prepare(`UPDATE component_definitions SET name = ?2, description = ?3, status = ?4, updated_at = ?5 WHERE id = ?1`).bind(componentId, name, description, status, timestamp),
550
+ db.prepare(`INSERT INTO configuration_mutations (idempotency_key, action, actor_id, target_id, response_json, created_at) VALUES (?1, 'component.update', ?2, ?3, ?4, ?5)`)
551
+ .bind(idempotencyKey, session.actor.id, componentId, canonicalJson(response), timestamp),
552
+ audit(db, session, `component-update:${idempotencyKey}`, "component.updated", "component", componentId, { name, status }, timestamp),
553
+ ]);
554
+ return { ...response, idempotentReplay: false };
555
+ }
556
+
557
+ export async function createComponent(
558
+ db: D1Database,
559
+ input: ComponentCreateInput,
560
+ session: OperatorSession,
561
+ idempotencyKey: string,
562
+ ): Promise<{ componentId: string; versionId: string; version: number; idempotentReplay: boolean }> {
563
+ const existingMutation = await replay(db, idempotencyKey, "component.create", session.actor.id);
564
+ if (existingMutation) return {
565
+ componentId: String(existingMutation.componentId), versionId: String(existingMutation.versionId),
566
+ version: Number(existingMutation.version), idempotentReplay: true,
567
+ };
568
+ if (!fixtureEvidence(input.version)) throw new ApiError(409, "component_fixture_gate_not_met", "Passing fixture evidence is required before component publication.");
569
+ await requireComponentConnection(db, input.version);
570
+ if (await db.prepare(`SELECT id FROM component_definitions WHERE installation_id = 'default' AND component_key = ?1`).bind(input.key).first()) {
571
+ throw new ApiError(409, "component_key_exists", "A component with this key already exists.");
572
+ }
573
+ const componentId = crypto.randomUUID();
574
+ const versionId = crypto.randomUUID();
575
+ const createdAt = now();
576
+ const response = { componentId, versionId, version: 1 };
577
+ await db.batch([
578
+ db.prepare(`
579
+ INSERT INTO component_definitions (
580
+ id, installation_id, component_key, name, description, component_kind,
581
+ effect_class, owner_kind, status, active_version_id, created_by, created_at, updated_at
582
+ ) VALUES (?1, 'default', ?2, ?3, ?4, ?5, ?6, ?7, 'active', ?8, ?9, ?10, ?10)
583
+ `).bind(
584
+ componentId, input.key, input.name, input.description, input.kind,
585
+ input.effectClass, input.ownerKind, versionId, session.actor.id, createdAt,
586
+ ),
587
+ componentVersionInsert(db, componentId, versionId, 1, input.version, session.actor.id, createdAt),
588
+ db.prepare(`
589
+ INSERT INTO configuration_mutations (idempotency_key, action, actor_id, target_id, response_json, created_at)
590
+ VALUES (?1, 'component.create', ?2, ?3, ?4, ?5)
591
+ `).bind(idempotencyKey, session.actor.id, componentId, canonicalJson(response), createdAt),
592
+ audit(db, session, `component-create:${idempotencyKey}`, "component.created", "component", componentId, {
593
+ key: input.key, kind: input.kind, effectClass: input.effectClass, versionId,
594
+ }, createdAt),
595
+ ...componentFixtureStatements(db, versionId, input.version, session.actor.id, createdAt),
596
+ ]);
597
+ return { ...response, idempotentReplay: false };
598
+ }
599
+
600
+ export async function publishComponentVersion(
601
+ db: D1Database,
602
+ componentId: string,
603
+ input: ComponentVersionInput,
604
+ session: OperatorSession,
605
+ idempotencyKey: string,
606
+ ): Promise<{ componentId: string; versionId: string; version: number; idempotentReplay: boolean }> {
607
+ const existingMutation = await replay(db, idempotencyKey, "component.version.publish", session.actor.id);
608
+ if (existingMutation) return {
609
+ componentId: String(existingMutation.componentId), versionId: String(existingMutation.versionId),
610
+ version: Number(existingMutation.version), idempotentReplay: true,
611
+ };
612
+ const definition = await db.prepare(`SELECT status, effect_class FROM component_definitions WHERE id = ?1`)
613
+ .bind(componentId).first<{ status: string; effect_class: string }>();
614
+ if (!definition) throw new ApiError(404, "component_not_found", "The component does not exist.");
615
+ if (definition.status === "archived") throw new ApiError(409, "component_archived", "Archived components cannot receive versions.");
616
+ if (!fixtureEvidence(input)) throw new ApiError(409, "component_fixture_gate_not_met", "Passing fixture evidence is required before component publication.");
617
+ if (definition.effect_class === "side_effecting" && !["human", "bounded_auto"].includes(String(input.approvalPolicy.mode))) throw new ApiError(409, "action_approval_policy_required", "A side-effecting version must preserve its approval policy.");
618
+ await requireComponentConnection(db, input);
619
+ const latest = await db.prepare(`SELECT COALESCE(MAX(version), 0) AS version FROM component_versions WHERE definition_id = ?1`)
620
+ .bind(componentId).first<{ version: number }>();
621
+ const version = Number(latest?.version ?? 0) + 1;
622
+ const versionId = crypto.randomUUID();
623
+ const createdAt = now();
624
+ const response = { componentId, versionId, version };
625
+ await db.batch([
626
+ componentVersionInsert(db, componentId, versionId, version, input, session.actor.id, createdAt),
627
+ db.prepare(`UPDATE component_definitions SET active_version_id = ?2, status = 'active', updated_at = ?3 WHERE id = ?1`).bind(componentId, versionId, createdAt),
628
+ db.prepare(`
629
+ INSERT INTO configuration_mutations (idempotency_key, action, actor_id, target_id, response_json, created_at)
630
+ VALUES (?1, 'component.version.publish', ?2, ?3, ?4, ?5)
631
+ `).bind(idempotencyKey, session.actor.id, versionId, canonicalJson(response), createdAt),
632
+ audit(db, session, `component-publish:${idempotencyKey}`, "component.version_published", "component", componentId, {
633
+ versionId, version, implementationKind: input.implementationKind,
634
+ }, createdAt),
635
+ ...componentFixtureStatements(db, versionId, input, session.actor.id, createdAt),
636
+ ]);
637
+ return { ...response, idempotentReplay: false };
638
+ }
639
+
640
+ export async function setComponentStatus(
641
+ db: D1Database,
642
+ componentId: string,
643
+ status: "active" | "paused",
644
+ session: OperatorSession,
645
+ idempotencyKey: string,
646
+ ): Promise<{ componentId: string; status: string; idempotentReplay: boolean }> {
647
+ const action = `component.${status}`;
648
+ const existing = await replay(db, idempotencyKey, action, session.actor.id);
649
+ if (existing) return { componentId: String(existing.componentId), status: String(existing.status), idempotentReplay: true };
650
+ const definition = await db.prepare(`SELECT status, active_version_id AS activeVersionId FROM component_definitions WHERE id = ?1 LIMIT 1`)
651
+ .bind(componentId).first<{ status: string; activeVersionId: string | null }>();
652
+ if (!definition) throw new ApiError(404, "component_not_found", "The component does not exist.");
653
+ if (["archived", "deprecated"].includes(definition.status)) throw new ApiError(409, "component_lifecycle_invalid", "Archived or deprecated components cannot change status.");
654
+ if (!definition.activeVersionId) throw new ApiError(409, "component_version_missing", "The component has no active version.");
655
+ const timestamp = now();
656
+ const response = { componentId, status };
657
+ await db.batch([
658
+ db.prepare(`UPDATE component_definitions SET status = ?2, updated_at = ?3 WHERE id = ?1`).bind(componentId, status, timestamp),
659
+ db.prepare(`INSERT INTO configuration_mutations (idempotency_key, action, actor_id, target_id, response_json, created_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6)`)
660
+ .bind(idempotencyKey, action, session.actor.id, componentId, canonicalJson(response), timestamp),
661
+ audit(db, session, `${action}:${idempotencyKey}`, `component.${status}`, "component", componentId, { activeVersionId: definition.activeVersionId }, timestamp),
662
+ ]);
663
+ return { ...response, idempotentReplay: false };
664
+ }
665
+
666
+ export async function componentDependencies(db: D1Database, componentId: string): Promise<Record<string, unknown>> {
667
+ const definition = await db.prepare(`SELECT id FROM component_definitions WHERE id = ?1 LIMIT 1`).bind(componentId).first();
668
+ if (!definition) throw new ApiError(404, "component_not_found", "The component does not exist.");
669
+ const workflows = await db.prepare(`
670
+ SELECT DISTINCT d.id AS workflowId, d.name, wv.id AS workflowVersionId, wv.version,
671
+ wv.authority_mode AS authorityMode, wv.published_at AS publishedAt,
672
+ wv.retired_at AS retiredAt
673
+ FROM workflow_dependencies dep
674
+ JOIN workflow_versions wv ON wv.id = dep.workflow_version_id
675
+ JOIN workflow_definitions d ON d.id = wv.workflow_id
676
+ JOIN component_versions cv ON cv.id = dep.dependency_version_id
677
+ WHERE dep.dependency_type = 'component' AND cv.definition_id = ?1
678
+ ORDER BY d.name, wv.version DESC
679
+ `).bind(componentId).all<Record<string, unknown>>();
680
+ return { workflows: workflows.results };
681
+ }
682
+
683
+ export async function validateComponentDefinition(db: D1Database, componentId: string): Promise<Record<string, unknown>> {
684
+ const row = await db.prepare(`
685
+ SELECT d.status, d.effect_class AS effectClass, d.active_version_id AS activeVersionId,
686
+ v.implementation_kind AS implementationKind, v.implementation_reference AS implementationReference,
687
+ v.input_schema_json AS inputSchemaJson, v.output_schema_json AS outputSchemaJson,
688
+ v.connection_version_id AS connectionVersionId,
689
+ (SELECT COUNT(*) FROM component_fixtures f WHERE f.component_version_id = v.id) AS fixtureCount,
690
+ (SELECT COUNT(*) FROM component_test_runs t WHERE t.component_version_id = v.id AND t.status = 'passed') AS passingTestCount
691
+ FROM component_definitions d LEFT JOIN component_versions v ON v.id = d.active_version_id
692
+ WHERE d.id = ?1 LIMIT 1
693
+ `).bind(componentId).first<Record<string, unknown>>();
694
+ if (!row) throw new ApiError(404, "component_not_found", "The component does not exist.");
695
+ const errors: string[] = [];
696
+ if (!row.activeVersionId) errors.push("active_version_missing");
697
+ if (!parsedJson(String(row.inputSchemaJson ?? ""), null)) errors.push("input_schema_invalid");
698
+ if (!parsedJson(String(row.outputSchemaJson ?? ""), null)) errors.push("output_schema_invalid");
699
+ if (["webhook", "external_api"].includes(String(row.implementationKind)) && !row.connectionVersionId) errors.push("connection_version_missing");
700
+ if (Number(row.fixtureCount ?? 0) < 1) errors.push("fixture_missing");
701
+ if (Number(row.passingTestCount ?? 0) < 1) errors.push("passing_test_missing");
702
+ return { valid: errors.length === 0, errors, ...row };
703
+ }
704
+
705
+ export async function testComponentDefinition(
706
+ db: D1Database,
707
+ componentId: string,
708
+ session: OperatorSession,
709
+ ): Promise<{ passed: boolean; componentVersionId: string; results: Record<string, unknown>[] }> {
710
+ const version = await db.prepare(`
711
+ SELECT d.active_version_id AS componentVersionId, v.input_schema_json AS inputSchemaJson,
712
+ v.output_schema_json AS outputSchemaJson
713
+ FROM component_definitions d JOIN component_versions v ON v.id = d.active_version_id
714
+ WHERE d.id = ?1 AND d.status != 'archived' LIMIT 1
715
+ `).bind(componentId).first<{ componentVersionId: string; inputSchemaJson: string; outputSchemaJson: string }>();
716
+ if (!version) throw new ApiError(404, "component_not_found", "The component or active version does not exist.");
717
+ const fixtures = await db.prepare(`
718
+ SELECT id, name, input_json AS inputJson, expected_json AS expectedJson,
719
+ expected_status AS expectedStatus FROM component_fixtures
720
+ WHERE component_version_id = ?1 ORDER BY name, id
721
+ `).bind(version.componentVersionId).all<{ id: string; name: string; inputJson: string; expectedJson: string; expectedStatus: string }>();
722
+ if (!fixtures.results.length) throw new ApiError(409, "component_fixture_missing", "The active component version has no fixtures.");
723
+ const inputSchema = parsedJson<JsonObject>(version.inputSchemaJson, {});
724
+ const outputSchema = parsedJson<JsonObject>(version.outputSchemaJson, {});
725
+ const timestamp = now();
726
+ const results: Record<string, unknown>[] = [];
727
+ const statements: D1PreparedStatement[] = [];
728
+ for (const fixture of fixtures.results) {
729
+ let inputValue: JsonValue = null;
730
+ let expectedValue: JsonValue = null;
731
+ const errors: string[] = [];
732
+ try { inputValue = JSON.parse(fixture.inputJson) as JsonValue; } catch { errors.push("fixture_input_json_invalid"); }
733
+ try { expectedValue = JSON.parse(fixture.expectedJson) as JsonValue; } catch { errors.push("fixture_expected_json_invalid"); }
734
+ errors.push(...validateJsonSchemaValue(inputValue, inputSchema).map((entry) => `input:${entry}`));
735
+ if (fixture.expectedStatus === "succeeded") errors.push(...validateJsonSchemaValue(expectedValue, outputSchema).map((entry) => `output:${entry}`));
736
+ const passed = errors.length === 0;
737
+ const testRunId = crypto.randomUUID();
738
+ results.push({ fixtureId: fixture.id, name: fixture.name, passed, errors });
739
+ statements.push(db.prepare(`
740
+ INSERT INTO component_test_runs (
741
+ id, component_version_id, fixture_id, status, output_json,
742
+ error_code, started_by, started_at, finished_at
743
+ ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?8)
744
+ `).bind(
745
+ testRunId, version.componentVersionId, fixture.id, passed ? "passed" : "failed",
746
+ canonicalJson({ fixture_validation: true, errors }), passed ? null : "fixture_schema_mismatch",
747
+ session.actor.id, timestamp,
748
+ ));
749
+ }
750
+ statements.push(audit(db, session, `component-test:${componentId}:${timestamp}:${session.actor.id}`, "component.tested", "component", componentId, { componentVersionId: version.componentVersionId, results }, timestamp));
751
+ await db.batch(statements);
752
+ return { passed: results.every((result) => result.passed === true), componentVersionId: version.componentVersionId, results };
753
+ }