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,367 @@
1
+ import { ApiError } from "./report-http";
2
+ import type { JsonObject, JsonValue } from "./workflow-platform-types";
3
+ import { validateJsonSchemaValue } from "./workflow-expressions";
4
+
5
+ export type ConnectionKind = "webhook" | "external_api" | "customer_worker" | "ai_provider" | "notification" | "email";
6
+ export type CredentialStrategy = "none" | "static_header" | "bearer" | "hmac" | "oauth2_client_credentials" | "oauth2_authorization_code" | "mutual_tls" | "service_binding";
7
+ export type ComponentKind = "built_in" | "webhook_enrichment" | "api_enrichment" | "workers_ai" | "specialist_agent" | "action" | "message";
8
+
9
+ export interface ConnectionVersionInput {
10
+ baseUrl: string | null;
11
+ serviceBindingName: string | null;
12
+ allowedHosts: string[];
13
+ allowedMethods: string[];
14
+ allowedPathPrefixes: string[];
15
+ requestLimitBytes: number;
16
+ responseLimitBytes: number;
17
+ responseContentTypes: string[];
18
+ timeoutMs: number;
19
+ concurrencyLimit: number;
20
+ requestsPerWindow: number;
21
+ windowSeconds: number;
22
+ credentialStrategy: CredentialStrategy;
23
+ secretReference: string | null;
24
+ signingStrategy: JsonObject;
25
+ oauthConfiguration: JsonObject;
26
+ }
27
+
28
+ export interface ConnectionCreateInput {
29
+ key: string;
30
+ name: string;
31
+ description: string;
32
+ kind: ConnectionKind;
33
+ version: ConnectionVersionInput;
34
+ }
35
+
36
+ export interface ComponentVersionInput {
37
+ implementationKind: "built_in" | "webhook" | "external_api" | "workers_ai" | "specialist_agent" | "message_template";
38
+ implementationReference: string;
39
+ inputSchema: JsonObject;
40
+ outputSchema: JsonObject;
41
+ allowedInputFields: string[];
42
+ dataClassifications: string[];
43
+ connectionVersionId: string | null;
44
+ riskClass: "low" | "medium" | "high" | "critical";
45
+ timeoutPolicy: JsonObject;
46
+ retryPolicy: JsonObject;
47
+ cachePolicy: JsonObject;
48
+ concurrencyPolicy: JsonObject;
49
+ budgetPolicy: JsonObject;
50
+ approvalPolicy: JsonObject;
51
+ toolManifest: JsonValue[];
52
+ compatibility: JsonObject;
53
+ }
54
+
55
+ export interface ComponentCreateInput {
56
+ key: string;
57
+ name: string;
58
+ description: string;
59
+ kind: ComponentKind;
60
+ effectClass: "read_only" | "side_effecting";
61
+ ownerKind: "safest" | "customer";
62
+ version: ComponentVersionInput;
63
+ }
64
+
65
+ const keyPattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
66
+ const secretReferencePattern = /^(?:CONNECTION|OAUTH|ACTION|NOTIFICATION|EMAIL|AI)_[A-Z0-9_]{2,96}$/u;
67
+ const hostPattern = /^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)*[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/u;
68
+ const methodSet = new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"]);
69
+ const connectionKinds = new Set<ConnectionKind>(["webhook", "external_api", "customer_worker", "ai_provider", "notification", "email"]);
70
+ const credentialStrategies = new Set<CredentialStrategy>(["none", "static_header", "bearer", "hmac", "oauth2_client_credentials", "oauth2_authorization_code", "mutual_tls", "service_binding"]);
71
+ const componentKinds = new Set<ComponentKind>(["built_in", "webhook_enrichment", "api_enrichment", "workers_ai", "specialist_agent", "action", "message"]);
72
+ const implementationKinds = new Set<ComponentVersionInput["implementationKind"]>(["built_in", "webhook", "external_api", "workers_ai", "specialist_agent", "message_template"]);
73
+
74
+ function object(value: unknown, name: string): Record<string, unknown> {
75
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new ApiError(400, "invalid_registry_configuration", `${name} must be an object.`);
76
+ return value as Record<string, unknown>;
77
+ }
78
+
79
+ function known(input: Record<string, unknown>, fields: readonly string[], name: string): void {
80
+ const unsupported = Object.keys(input).find((key) => !fields.includes(key));
81
+ if (unsupported) throw new ApiError(400, "invalid_registry_configuration", `${name} contains unsupported field ${unsupported}.`);
82
+ }
83
+
84
+ function text(value: unknown, name: string, minimum: number, maximum: number): string {
85
+ if (typeof value !== "string" || value.trim().length < minimum || value.trim().length > maximum || /[\u0000-\u001f\u007f]/u.test(value)) {
86
+ throw new ApiError(400, "invalid_registry_configuration", `${name} must contain ${minimum} to ${maximum} safe characters.`);
87
+ }
88
+ return value.trim();
89
+ }
90
+
91
+ function integer(value: unknown, name: string, minimum: number, maximum: number): number {
92
+ if (!Number.isInteger(value) || Number(value) < minimum || Number(value) > maximum) throw new ApiError(400, "invalid_registry_configuration", `${name} must be an integer from ${minimum} to ${maximum}.`);
93
+ return Number(value);
94
+ }
95
+
96
+ function json(value: unknown, name: string, depth = 0): JsonValue {
97
+ if (depth > 20) throw new ApiError(400, "invalid_registry_configuration", `${name} is nested too deeply.`);
98
+ if (value === null || typeof value === "string" || typeof value === "boolean") return value;
99
+ if (typeof value === "number" && Number.isFinite(value)) return value;
100
+ if (Array.isArray(value)) {
101
+ if (value.length > 500) throw new ApiError(400, "invalid_registry_configuration", `${name} contains too many entries.`);
102
+ return value.map((entry, index) => json(entry, `${name}[${index}]`, depth + 1));
103
+ }
104
+ if (value && typeof value === "object") {
105
+ const result: JsonObject = {};
106
+ const entries = Object.entries(value as Record<string, unknown>);
107
+ if (entries.length > 500) throw new ApiError(400, "invalid_registry_configuration", `${name} contains too many fields.`);
108
+ for (const [key, entry] of entries) {
109
+ if (["__proto__", "prototype", "constructor"].includes(key)) throw new ApiError(400, "invalid_registry_configuration", `${name} contains a prohibited field.`);
110
+ result[key] = json(entry, `${name}.${key}`, depth + 1);
111
+ }
112
+ return result;
113
+ }
114
+ throw new ApiError(400, "invalid_registry_configuration", `${name} must contain JSON data.`);
115
+ }
116
+
117
+ function jsonObject(value: unknown, name: string): JsonObject {
118
+ const parsed = json(value ?? {}, name);
119
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new ApiError(400, "invalid_registry_configuration", `${name} must be an object.`);
120
+ return parsed;
121
+ }
122
+
123
+ function stringList(value: unknown, name: string, maximum: number): string[] {
124
+ if (!Array.isArray(value) || value.length < 1 || value.length > maximum) throw new ApiError(400, "invalid_registry_configuration", `${name} must contain 1 to ${maximum} entries.`);
125
+ const result = value.map((entry, index) => text(entry, `${name}[${index}]`, 1, 300));
126
+ if (new Set(result).size !== result.length) throw new ApiError(400, "invalid_registry_configuration", `${name} contains duplicates.`);
127
+ return result;
128
+ }
129
+
130
+ function safeHostname(hostname: string): boolean {
131
+ const normalized = hostname.toLowerCase().replace(/^\[|\]$/gu, "").replace(/\.$/u, "");
132
+ if (!hostPattern.test(normalized) && !normalized.includes(":")) return false;
133
+ if (normalized === "localhost" || normalized.endsWith(".localhost") || normalized.endsWith(".local") || normalized.endsWith(".internal")) return false;
134
+ if (["metadata.google.internal", "metadata.aws.internal", "100.100.100.200", "169.254.169.254", "::1", "::"].includes(normalized)) return false;
135
+ const octets = normalized.split(".").map(Number);
136
+ if (octets.length === 4 && octets.every((octet) => Number.isInteger(octet) && octet >= 0 && octet <= 255)) {
137
+ const [a, b] = octets as [number, number, number, number];
138
+ if (a === 0 || a === 10 || a === 127 || (a === 169 && b === 254) || (a === 172 && b >= 16 && b <= 31) || (a === 192 && b === 168) || (a === 100 && b >= 64 && b <= 127) || a >= 224) return false;
139
+ }
140
+ if (normalized.includes(":") && (/^(?:fc|fd|fe8|fe9|fea|feb)/u.test(normalized) || normalized.startsWith("::ffff:"))) return false;
141
+ return true;
142
+ }
143
+
144
+ function safeOauthUrl(value: unknown, field: string): string {
145
+ try {
146
+ const url = new URL(text(value, field, 1, 2_048));
147
+ if (url.protocol !== "https:" || url.username || url.password || url.hash || !safeHostname(url.hostname)) throw new Error("unsafe");
148
+ return url.href;
149
+ } catch {
150
+ throw new ApiError(400, "unsafe_oauth_url", `${field} must be a public HTTPS URL without credentials or a fragment.`);
151
+ }
152
+ }
153
+
154
+ function oauthConfiguration(value: unknown, strategy: CredentialStrategy): JsonObject {
155
+ const configuration = jsonObject(value ?? {}, "oauth_configuration");
156
+ if (strategy !== "oauth2_authorization_code") {
157
+ if (Object.keys(configuration).length) throw new ApiError(400, "invalid_oauth_configuration", "oauth_configuration is only supported for OAuth credential strategies.");
158
+ return configuration;
159
+ }
160
+ known(configuration, ["authorization_url", "token_url", "revoke_url", "client_id", "client_secret_reference", "scopes", "authorization_parameters"], "oauth_configuration");
161
+ const clientId = text(configuration.client_id, "oauth_configuration.client_id", 1, 500);
162
+ const clientSecretReference = configuration.client_secret_reference === undefined || configuration.client_secret_reference === null
163
+ ? null : text(configuration.client_secret_reference, "oauth_configuration.client_secret_reference", 3, 100);
164
+ if (clientSecretReference && !/^OAUTH_[A-Z0-9_]{2,96}$/u.test(clientSecretReference)) {
165
+ throw new ApiError(400, "invalid_secret_reference", "OAuth client secrets must use an OAUTH_ Worker secret binding.");
166
+ }
167
+ const scopes = configuration.scopes === undefined || (Array.isArray(configuration.scopes) && configuration.scopes.length === 0)
168
+ ? [] : stringList(configuration.scopes, "oauth_configuration.scopes", 100);
169
+ const parameters = jsonObject(configuration.authorization_parameters ?? {}, "oauth_configuration.authorization_parameters");
170
+ for (const [key, entry] of Object.entries(parameters)) {
171
+ if (!/^[A-Za-z0-9_.~-]{1,100}$/u.test(key) || typeof entry !== "string" || entry.length > 1_000
172
+ || ["state", "redirect_uri", "client_id", "code_challenge", "response_type"].includes(key)) {
173
+ throw new ApiError(400, "invalid_oauth_configuration", "authorization_parameters contains a reserved or unsafe value.");
174
+ }
175
+ }
176
+ return {
177
+ authorization_url: safeOauthUrl(configuration.authorization_url, "oauth_configuration.authorization_url"),
178
+ token_url: safeOauthUrl(configuration.token_url, "oauth_configuration.token_url"),
179
+ ...(configuration.revoke_url ? { revoke_url: safeOauthUrl(configuration.revoke_url, "oauth_configuration.revoke_url") } : {}),
180
+ client_id: clientId,
181
+ ...(clientSecretReference ? { client_secret_reference: clientSecretReference } : {}),
182
+ scopes,
183
+ authorization_parameters: parameters,
184
+ };
185
+ }
186
+
187
+ export function parseConnectionVersion(value: unknown): ConnectionVersionInput {
188
+ const input = object(value, "connection.version");
189
+ known(input, [
190
+ "base_url", "service_binding_name", "allowed_hosts", "allowed_methods", "allowed_path_prefixes",
191
+ "request_limit_bytes", "response_limit_bytes", "response_content_types", "timeout_ms", "concurrency_limit",
192
+ "requests_per_window", "window_seconds", "credential_strategy", "secret_reference",
193
+ "signing_strategy", "oauth_configuration",
194
+ ], "connection.version");
195
+ const serviceBindingName = input.service_binding_name === undefined || input.service_binding_name === null
196
+ ? null : text(input.service_binding_name, "service_binding_name", 2, 100);
197
+ let baseUrl: string | null = null;
198
+ let urlHost: string | null = null;
199
+ if (input.base_url !== undefined && input.base_url !== null) {
200
+ try {
201
+ const url = new URL(text(input.base_url, "base_url", 1, 2_048));
202
+ if (url.protocol !== "https:" || url.username || url.password || url.hash || url.search || !safeHostname(url.hostname)) throw new Error("unsafe");
203
+ baseUrl = url.href.endsWith("/") ? url.href.slice(0, -1) : url.href;
204
+ urlHost = url.hostname.toLowerCase().replace(/^\[|\]$/gu, "");
205
+ } catch {
206
+ throw new ApiError(400, "unsafe_connection_url", "base_url must be a public HTTPS origin without credentials, query, or fragment.");
207
+ }
208
+ }
209
+ if (Boolean(baseUrl) === Boolean(serviceBindingName)) throw new ApiError(400, "invalid_registry_configuration", "Exactly one of base_url or service_binding_name is required.");
210
+ const allowedHosts = baseUrl ? stringList(input.allowed_hosts ?? [urlHost], "allowed_hosts", 50).map((host) => host.toLowerCase()) : [];
211
+ if (allowedHosts.some((host) => !safeHostname(host)) || (urlHost && !allowedHosts.includes(urlHost))) throw new ApiError(400, "unsafe_connection_host", "allowed_hosts must contain only public hostnames and include base_url's host.");
212
+ const allowedMethods = stringList(input.allowed_methods ?? ["POST"], "allowed_methods", 6).map((method) => method.toUpperCase());
213
+ if (allowedMethods.some((method) => !methodSet.has(method))) throw new ApiError(400, "invalid_registry_configuration", "allowed_methods contains an unsupported method.");
214
+ const allowedPathPrefixes = stringList(input.allowed_path_prefixes ?? ["/"], "allowed_path_prefixes", 50);
215
+ if (allowedPathPrefixes.some((path) => !path.startsWith("/") || path.includes("..") || path.includes("?") || path.includes("#"))) throw new ApiError(400, "unsafe_connection_path", "allowed_path_prefixes contains an unsafe path.");
216
+ if (typeof input.credential_strategy !== "string" || !credentialStrategies.has(input.credential_strategy as CredentialStrategy)) throw new ApiError(400, "invalid_registry_configuration", "credential_strategy is unsupported.");
217
+ const credentialStrategy = input.credential_strategy as CredentialStrategy;
218
+ const secretReference = input.secret_reference === undefined || input.secret_reference === null ? null : text(input.secret_reference, "secret_reference", 3, 100);
219
+ if (secretReference && !secretReferencePattern.test(secretReference)) throw new ApiError(400, "invalid_secret_reference", "secret_reference must use an approved connection secret binding prefix.");
220
+ if (!["none", "oauth2_authorization_code"].includes(credentialStrategy) && !secretReference && credentialStrategy !== "service_binding") throw new ApiError(400, "connection_secret_required", "The selected credential strategy requires secret_reference.");
221
+ return {
222
+ baseUrl,
223
+ serviceBindingName,
224
+ allowedHosts,
225
+ allowedMethods,
226
+ allowedPathPrefixes,
227
+ requestLimitBytes: integer(input.request_limit_bytes ?? 65_536, "request_limit_bytes", 1, 1_048_576),
228
+ responseLimitBytes: integer(input.response_limit_bytes ?? 262_144, "response_limit_bytes", 1, 5_242_880),
229
+ responseContentTypes: stringList(input.response_content_types ?? ["application/json"], "response_content_types", 20),
230
+ timeoutMs: integer(input.timeout_ms ?? 5_000, "timeout_ms", 100, 120_000),
231
+ concurrencyLimit: integer(input.concurrency_limit ?? 8, "concurrency_limit", 1, 1_000),
232
+ requestsPerWindow: integer(input.requests_per_window ?? 60, "requests_per_window", 1, 1_000_000),
233
+ windowSeconds: integer(input.window_seconds ?? 60, "window_seconds", 1, 86_400),
234
+ credentialStrategy,
235
+ secretReference,
236
+ signingStrategy: jsonObject(input.signing_strategy ?? {}, "signing_strategy"),
237
+ oauthConfiguration: oauthConfiguration(input.oauth_configuration, credentialStrategy),
238
+ };
239
+ }
240
+
241
+ export function parseConnectionCreate(value: unknown): ConnectionCreateInput {
242
+ const input = object(value, "connection");
243
+ known(input, ["key", "name", "description", "kind", "version"], "connection");
244
+ const key = text(input.key, "connection.key", 2, 100);
245
+ if (!keyPattern.test(key)) throw new ApiError(400, "invalid_connection_key", "connection.key must be lower-case kebab-case.");
246
+ if (typeof input.kind !== "string" || !connectionKinds.has(input.kind as ConnectionKind)) throw new ApiError(400, "invalid_registry_configuration", "connection.kind is unsupported.");
247
+ return {
248
+ key,
249
+ name: text(input.name, "connection.name", 2, 160),
250
+ description: input.description === undefined ? "" : text(input.description, "connection.description", 0, 2_000),
251
+ kind: input.kind as ConnectionKind,
252
+ version: parseConnectionVersion(input.version),
253
+ };
254
+ }
255
+
256
+ function schema(value: unknown, name: string): JsonObject {
257
+ const result = jsonObject(value, name);
258
+ if (result.type !== "object" || !result.properties || typeof result.properties !== "object" || Array.isArray(result.properties)) {
259
+ throw new ApiError(400, "invalid_component_schema", `${name} must be an object JSON Schema with properties.`);
260
+ }
261
+ return result;
262
+ }
263
+
264
+ export function parseComponentVersion(value: unknown, effectClass?: "read_only" | "side_effecting"): ComponentVersionInput {
265
+ const input = object(value, "component.version");
266
+ known(input, [
267
+ "implementation_kind", "implementation_reference", "input_schema", "output_schema",
268
+ "allowed_input_fields", "data_classifications", "connection_version_id", "risk_class",
269
+ "timeout_policy", "retry_policy", "cache_policy", "concurrency_policy", "budget_policy",
270
+ "approval_policy", "tool_manifest", "compatibility",
271
+ ], "component.version");
272
+ if (typeof input.implementation_kind !== "string" || !implementationKinds.has(input.implementation_kind as ComponentVersionInput["implementationKind"])) throw new ApiError(400, "invalid_registry_configuration", "implementation_kind is unsupported.");
273
+ const connectionVersionId = input.connection_version_id === undefined || input.connection_version_id === null
274
+ ? null : text(input.connection_version_id, "connection_version_id", 1, 200);
275
+ if (["webhook", "external_api"].includes(input.implementation_kind) && !connectionVersionId) throw new ApiError(400, "component_connection_required", "Webhook and external API components must pin a connection version.");
276
+ const riskClass = input.risk_class ?? (effectClass === "side_effecting" ? "high" : "low");
277
+ if (!["low", "medium", "high", "critical"].includes(String(riskClass))) throw new ApiError(400, "invalid_registry_configuration", "risk_class is unsupported.");
278
+ const approvalPolicy = jsonObject(input.approval_policy ?? {}, "approval_policy");
279
+ if (effectClass === "side_effecting" && !["human", "bounded_auto"].includes(String(approvalPolicy.mode))) throw new ApiError(400, "action_approval_policy_required", "A side-effecting component requires a human or bounded_auto approval policy.");
280
+ const toolManifest = input.tool_manifest ?? [];
281
+ if (!Array.isArray(toolManifest)) throw new ApiError(400, "invalid_registry_configuration", "tool_manifest must be an array.");
282
+ const implementationReference = text(input.implementation_reference, "implementation_reference", 1, 300);
283
+ if (["webhook", "external_api"].includes(input.implementation_kind) && !/^(?:(?:GET|POST|PUT|PATCH|DELETE|HEAD)\s+)?\/[^\s]*$/u.test(implementationReference)) {
284
+ throw new ApiError(400, "component_operation_reference_invalid", "External component implementation_reference must be an optional HTTP method followed by an allowlisted absolute path.");
285
+ }
286
+ if (input.implementation_kind === "workers_ai" && !implementationReference.startsWith("@cf/")) {
287
+ throw new ApiError(400, "component_ai_model_invalid", "Workers AI component references must use an @cf model identifier.");
288
+ }
289
+ if (["built_in", "specialist_agent"].includes(input.implementation_kind) && !/^[a-z0-9]+(?:-[a-z0-9]+)*-v[1-9][0-9]*$/u.test(implementationReference)) {
290
+ throw new ApiError(400, "component_implementation_reference_invalid", "Built-in and specialist references must be immutable kebab-case version identifiers.");
291
+ }
292
+ const inputSchema = schema(input.input_schema, "input_schema");
293
+ const outputSchema = schema(input.output_schema, "output_schema");
294
+ const compatibility = jsonObject(input.compatibility ?? {}, "compatibility");
295
+ const fixtureEvidence = compatibility.fixture_evidence;
296
+ if (!fixtureEvidence || typeof fixtureEvidence !== "object" || Array.isArray(fixtureEvidence)) {
297
+ throw new ApiError(400, "component_fixture_evidence_invalid", "compatibility.fixture_evidence must contain server-verifiable fixtures.");
298
+ }
299
+ const fixturesValue = fixtureEvidence.fixtures;
300
+ if (!Array.isArray(fixturesValue) || fixturesValue.length < 1 || fixturesValue.length > 20) {
301
+ throw new ApiError(400, "component_fixture_evidence_invalid", "One to twenty component fixtures are required.");
302
+ }
303
+ const fixtureNames = new Set<string>();
304
+ const fixtures = fixturesValue.map((entry, index) => {
305
+ const fixture = object(entry, `compatibility.fixture_evidence.fixtures[${index}]`);
306
+ known(fixture, ["name", "input", "expected_output", "expected_status", "mode"], `compatibility.fixture_evidence.fixtures[${index}]`);
307
+ const name = text(fixture.name, `fixture[${index}].name`, 2, 120);
308
+ if (fixtureNames.has(name)) throw new ApiError(400, "component_fixture_evidence_invalid", "Fixture names must be unique.");
309
+ fixtureNames.add(name);
310
+ const fixtureInput = jsonObject(fixture.input, `fixture[${index}].input`);
311
+ const expectedOutput = jsonObject(fixture.expected_output ?? {}, `fixture[${index}].expected_output`);
312
+ const expectedStatus = fixture.expected_status === undefined ? "succeeded" : fixture.expected_status;
313
+ if (typeof expectedStatus !== "string" || !["succeeded", "failed", "timed_out"].includes(expectedStatus)) throw new ApiError(400, "component_fixture_evidence_invalid", "Fixture expected_status is invalid.");
314
+ const mode = fixture.mode ?? "simulated";
315
+ if (mode !== "simulated" && mode !== "live_dry_run") throw new ApiError(400, "component_fixture_evidence_invalid", "Fixture mode is invalid.");
316
+ const inputErrors = validateJsonSchemaValue(fixtureInput, inputSchema);
317
+ const outputErrors = expectedStatus === "succeeded" ? validateJsonSchemaValue(expectedOutput, outputSchema) : [];
318
+ if (inputErrors.length || outputErrors.length) throw new ApiError(422, "component_fixture_schema_invalid", "A component fixture does not match the published schemas.", { name, inputErrors, outputErrors });
319
+ return { name, input: fixtureInput, expected_output: expectedOutput, expected_status: expectedStatus, mode } as JsonObject;
320
+ });
321
+ compatibility.fixture_evidence = { passed: true, validated_by: "server", fixtures };
322
+ return {
323
+ implementationKind: input.implementation_kind as ComponentVersionInput["implementationKind"],
324
+ implementationReference,
325
+ inputSchema,
326
+ outputSchema,
327
+ allowedInputFields: stringList(input.allowed_input_fields, "allowed_input_fields", 100),
328
+ dataClassifications: stringList(input.data_classifications ?? ["report_operational"], "data_classifications", 20),
329
+ connectionVersionId,
330
+ riskClass: riskClass as ComponentVersionInput["riskClass"],
331
+ timeoutPolicy: jsonObject(input.timeout_policy ?? { timeout_ms: 5_000 }, "timeout_policy"),
332
+ retryPolicy: jsonObject(input.retry_policy ?? { maximum_attempts: 3, backoff: "exponential" }, "retry_policy"),
333
+ cachePolicy: jsonObject(input.cache_policy ?? { enabled: false }, "cache_policy"),
334
+ concurrencyPolicy: jsonObject(input.concurrency_policy ?? { maximum_concurrency: 8 }, "concurrency_policy"),
335
+ budgetPolicy: jsonObject(input.budget_policy ?? {}, "budget_policy"),
336
+ approvalPolicy,
337
+ toolManifest: toolManifest.map((entry, index) => json(entry, `tool_manifest[${index}]`)),
338
+ compatibility,
339
+ };
340
+ }
341
+
342
+ export function parseComponentCreate(value: unknown): ComponentCreateInput {
343
+ const input = object(value, "component");
344
+ known(input, ["key", "name", "description", "kind", "effect_class", "owner_kind", "version"], "component");
345
+ const key = text(input.key, "component.key", 2, 100);
346
+ if (!keyPattern.test(key)) throw new ApiError(400, "invalid_component_key", "component.key must be lower-case kebab-case.");
347
+ if (typeof input.kind !== "string" || !componentKinds.has(input.kind as ComponentKind)) throw new ApiError(400, "invalid_registry_configuration", "component.kind is unsupported.");
348
+ if (input.effect_class !== "read_only" && input.effect_class !== "side_effecting") throw new ApiError(400, "invalid_registry_configuration", "effect_class is unsupported.");
349
+ if (input.owner_kind !== "safest" && input.owner_kind !== "customer") throw new ApiError(400, "invalid_registry_configuration", "owner_kind is unsupported.");
350
+ if (input.kind === "action" && input.effect_class !== "side_effecting") throw new ApiError(400, "action_effect_class_required", "Action components must be side_effecting.");
351
+ if (input.kind !== "action" && input.effect_class === "side_effecting") throw new ApiError(400, "component_effect_mismatch", "Only action components may be side_effecting.");
352
+ return {
353
+ key,
354
+ name: text(input.name, "component.name", 2, 160),
355
+ description: input.description === undefined ? "" : text(input.description, "component.description", 0, 2_000),
356
+ kind: input.kind as ComponentKind,
357
+ effectClass: input.effect_class,
358
+ ownerKind: input.owner_kind,
359
+ version: parseComponentVersion(input.version, input.effect_class),
360
+ };
361
+ }
362
+
363
+ export function assertSafeConnectionDestination(url: URL, allowedHosts: string[], allowedPathPrefixes: string[]): void {
364
+ const host = url.hostname.toLowerCase().replace(/^\[|\]$/gu, "");
365
+ if (url.protocol !== "https:" || url.username || url.password || !safeHostname(host) || !allowedHosts.includes(host)) throw new ApiError(400, "connection_destination_denied", "The connection destination is not allowlisted.");
366
+ if (!allowedPathPrefixes.some((prefix) => url.pathname.startsWith(prefix))) throw new ApiError(400, "connection_path_denied", "The connection path is not allowlisted.");
367
+ }