devpush 0.0.0-stage → 1.0.0-beta.3

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 (456) hide show
  1. package/README.md +6 -2
  2. package/agent-release.json +37 -0
  3. package/dist/package/LICENSE.md +21 -0
  4. package/dist/package/migrate.mjs +182 -0
  5. package/dist/package/plugins.mjs +125 -0
  6. package/dist/package/vite.mjs +439 -0
  7. package/package.json +170 -4
  8. package/public/apple-touch-icon.png +0 -0
  9. package/public/favicon.svg +1 -0
  10. package/public/logo-email.png +0 -0
  11. package/public/robots.txt +3 -0
  12. package/public/social.png +0 -0
  13. package/src/cloudflare-runtime.d.ts +78 -0
  14. package/src/components/about-dialog.tsx +70 -0
  15. package/src/components/copy-button.tsx +74 -0
  16. package/src/components/logo-mark.tsx +20 -0
  17. package/src/components/pending-collection.tsx +103 -0
  18. package/src/components/resource-avatar.tsx +65 -0
  19. package/src/components/route-error-state.tsx +95 -0
  20. package/src/components/section-navigation.tsx +39 -0
  21. package/src/components/settings-layout.tsx +111 -0
  22. package/src/components/storage-capabilities.tsx +11 -0
  23. package/src/components/table-pager.tsx +101 -0
  24. package/src/components/ui/alert-dialog.tsx +180 -0
  25. package/src/components/ui/alert.tsx +78 -0
  26. package/src/components/ui/avatar.tsx +58 -0
  27. package/src/components/ui/badge.tsx +49 -0
  28. package/src/components/ui/button-group.tsx +38 -0
  29. package/src/components/ui/button.tsx +67 -0
  30. package/src/components/ui/card.tsx +82 -0
  31. package/src/components/ui/chart.tsx +307 -0
  32. package/src/components/ui/checkbox.tsx +30 -0
  33. package/src/components/ui/combobox.tsx +256 -0
  34. package/src/components/ui/dialog.tsx +163 -0
  35. package/src/components/ui/dropdown-menu.tsx +110 -0
  36. package/src/components/ui/empty.tsx +65 -0
  37. package/src/components/ui/field.tsx +192 -0
  38. package/src/components/ui/input-group.tsx +118 -0
  39. package/src/components/ui/input-otp.tsx +70 -0
  40. package/src/components/ui/input.tsx +19 -0
  41. package/src/components/ui/label.tsx +24 -0
  42. package/src/components/ui/pagination.tsx +34 -0
  43. package/src/components/ui/popover.tsx +86 -0
  44. package/src/components/ui/select.tsx +145 -0
  45. package/src/components/ui/skeleton.tsx +19 -0
  46. package/src/components/ui/sonner.tsx +47 -0
  47. package/src/components/ui/switch.tsx +31 -0
  48. package/src/components/ui/table.tsx +81 -0
  49. package/src/components/ui/textarea.tsx +18 -0
  50. package/src/components/ui/tooltip.tsx +55 -0
  51. package/src/features/admin/admin-settings.tsx +1048 -0
  52. package/src/features/auth/current-user-context-query.ts +11 -0
  53. package/src/features/auth/login-form.tsx +442 -0
  54. package/src/features/auth/sign-out-menu-item.tsx +42 -0
  55. package/src/features/deployments/deployment-actions.tsx +336 -0
  56. package/src/features/deployments/deployment-alias-badge.tsx +35 -0
  57. package/src/features/deployments/deployment-detail.tsx +596 -0
  58. package/src/features/deployments/deployment-filters.ts +66 -0
  59. package/src/features/deployments/deployment-list.tsx +465 -0
  60. package/src/features/deployments/deployment-visit-links.tsx +104 -0
  61. package/src/features/deployments/deployments-page.tsx +376 -0
  62. package/src/features/logs/log-scroll-area.tsx +69 -0
  63. package/src/features/notifications/email-preferences.tsx +232 -0
  64. package/src/features/onboarding/profile-onboarding.tsx +162 -0
  65. package/src/features/plugins/contributions.tsx +118 -0
  66. package/src/features/projects/backup-job-logs.tsx +143 -0
  67. package/src/features/projects/backup-policy-delete-dialog.tsx +82 -0
  68. package/src/features/projects/backup-restore-actions.tsx +573 -0
  69. package/src/features/projects/backup-status-icon.tsx +33 -0
  70. package/src/features/projects/backup-target-list.tsx +222 -0
  71. package/src/features/projects/backup-timestamp.tsx +32 -0
  72. package/src/features/projects/backup-volume-picker.tsx +158 -0
  73. package/src/features/projects/compose-service-input.tsx +64 -0
  74. package/src/features/projects/deploy-readiness.ts +28 -0
  75. package/src/features/projects/environment-dot.tsx +91 -0
  76. package/src/features/projects/log-keyword-filter.tsx +48 -0
  77. package/src/features/projects/manual-deploy-dialog.tsx +563 -0
  78. package/src/features/projects/pending-section.ts +26 -0
  79. package/src/features/projects/project-backup-details-pending.tsx +305 -0
  80. package/src/features/projects/project-backup-details.tsx +936 -0
  81. package/src/features/projects/project-backup-policy.tsx +644 -0
  82. package/src/features/projects/project-backup-utils.ts +190 -0
  83. package/src/features/projects/project-backups-pending.tsx +136 -0
  84. package/src/features/projects/project-backups.tsx +876 -0
  85. package/src/features/projects/project-environment-settings.tsx +1128 -0
  86. package/src/features/projects/project-heading.tsx +68 -0
  87. package/src/features/projects/project-layout.tsx +188 -0
  88. package/src/features/projects/project-logs.tsx +461 -0
  89. package/src/features/projects/project-overview.tsx +280 -0
  90. package/src/features/projects/project-route-pending.tsx +1064 -0
  91. package/src/features/projects/project-routing-settings.tsx +663 -0
  92. package/src/features/projects/project-settings-section.tsx +64 -0
  93. package/src/features/projects/project-settings-utils.ts +101 -0
  94. package/src/features/projects/project-settings.tsx +1472 -0
  95. package/src/features/projects/repository-picker-dialog.tsx +404 -0
  96. package/src/features/repositories/new-project-dialog-launcher.tsx +44 -0
  97. package/src/features/repositories/new-project-dialog.tsx +1204 -0
  98. package/src/features/repositories/new-project-pending.tsx +28 -0
  99. package/src/features/repositories/repository-list-pending.tsx +59 -0
  100. package/src/features/servers/server-availability-alert.tsx +44 -0
  101. package/src/features/servers/server-availability.ts +29 -0
  102. package/src/features/servers/server-avatar.tsx +52 -0
  103. package/src/features/servers/server-detail-pending.tsx +315 -0
  104. package/src/features/servers/server-detail.tsx +855 -0
  105. package/src/features/servers/server-metric-chart-pending.tsx +38 -0
  106. package/src/features/servers/server-metric-chart.tsx +182 -0
  107. package/src/features/servers/server-metric-format.ts +50 -0
  108. package/src/features/servers/server-metrics-pending.tsx +75 -0
  109. package/src/features/servers/server-metrics.tsx +136 -0
  110. package/src/features/servers/team-servers.tsx +432 -0
  111. package/src/features/settings/settings-route-pending.tsx +406 -0
  112. package/src/features/teams/backup-storage-settings.tsx +483 -0
  113. package/src/features/teams/navigation.ts +51 -0
  114. package/src/features/teams/new-team-dialog.tsx +132 -0
  115. package/src/features/teams/pending-section.ts +19 -0
  116. package/src/features/teams/team-header.tsx +1286 -0
  117. package/src/features/teams/team-heading.tsx +57 -0
  118. package/src/features/teams/team-home.tsx +391 -0
  119. package/src/features/teams/team-invite.tsx +155 -0
  120. package/src/features/teams/team-route-pending.tsx +505 -0
  121. package/src/features/teams/team-settings.tsx +1555 -0
  122. package/src/features/teams/use-workspace-updates.ts +126 -0
  123. package/src/features/users/api-keys-settings.tsx +544 -0
  124. package/src/features/users/api-scope-selection.tsx +269 -0
  125. package/src/features/users/connected-apps-settings.tsx +449 -0
  126. package/src/features/users/mcp-consent.tsx +220 -0
  127. package/src/features/users/profile-picture.tsx +171 -0
  128. package/src/features/users/user-settings.tsx +1064 -0
  129. package/src/lib/account-settings-schema.ts +78 -0
  130. package/src/lib/action-feedback.ts +81 -0
  131. package/src/lib/admin-search-schema.ts +8 -0
  132. package/src/lib/agent-release.d.mts +21 -0
  133. package/src/lib/agent-release.mjs +90 -0
  134. package/src/lib/allowlist-schema.ts +79 -0
  135. package/src/lib/api-key-schema.ts +176 -0
  136. package/src/lib/auth/client.ts +20 -0
  137. package/src/lib/avatar-schema.ts +11 -0
  138. package/src/lib/backup-cron.ts +118 -0
  139. package/src/lib/backup-database.ts +41 -0
  140. package/src/lib/backup-feature.ts +2 -0
  141. package/src/lib/backup-paths.ts +7 -0
  142. package/src/lib/backup-policy-schema.ts +85 -0
  143. package/src/lib/backup-resource-schema.ts +12 -0
  144. package/src/lib/backup-restore-schema.ts +15 -0
  145. package/src/lib/backup-storage-schema.ts +55 -0
  146. package/src/lib/deferred-module.ts +38 -0
  147. package/src/lib/deployment-logs-query.ts +63 -0
  148. package/src/lib/display-name.ts +7 -0
  149. package/src/lib/error-message.ts +119 -0
  150. package/src/lib/form-errors.ts +10 -0
  151. package/src/lib/internal-redirect.ts +25 -0
  152. package/src/lib/manual-deploy-options-query.ts +21 -0
  153. package/src/lib/notification-preferences.ts +43 -0
  154. package/src/lib/onboarding.ts +11 -0
  155. package/src/lib/operation-logs-query.ts +51 -0
  156. package/src/lib/page-head.ts +14 -0
  157. package/src/lib/project-context-query.ts +257 -0
  158. package/src/lib/project-creation-schema.ts +64 -0
  159. package/src/lib/project-env-schema.ts +23 -0
  160. package/src/lib/project-environments-schema.ts +56 -0
  161. package/src/lib/project-routing-schema.ts +94 -0
  162. package/src/lib/project-runtime-config.ts +131 -0
  163. package/src/lib/project-settings-schema.ts +75 -0
  164. package/src/lib/public-api-schema.ts +128 -0
  165. package/src/lib/public-error.ts +337 -0
  166. package/src/lib/query.ts +13 -0
  167. package/src/lib/repository-selection-query.ts +11 -0
  168. package/src/lib/resource-cache.ts +30 -0
  169. package/src/lib/runtime-id.ts +19 -0
  170. package/src/lib/search.ts +14 -0
  171. package/src/lib/server-context-query.ts +43 -0
  172. package/src/lib/server-name-schema.ts +11 -0
  173. package/src/lib/settings-conflict.ts +45 -0
  174. package/src/lib/settings-version.ts +7 -0
  175. package/src/lib/team-context-query.ts +126 -0
  176. package/src/lib/team-context-schema.ts +8 -0
  177. package/src/lib/team-forms-schema.ts +45 -0
  178. package/src/lib/team-role.ts +13 -0
  179. package/src/lib/team-settings-schema.ts +48 -0
  180. package/src/lib/urls.ts +3 -0
  181. package/src/lib/use-avatar-input.ts +61 -0
  182. package/src/lib/use-refresh-error.ts +18 -0
  183. package/src/lib/use-settings-version.ts +23 -0
  184. package/src/lib/utils.ts +6 -0
  185. package/src/lib/validation.ts +5 -0
  186. package/src/lib/workspace-updates.ts +53 -0
  187. package/src/package/config.ts +10 -0
  188. package/src/package/env.mjs +54 -0
  189. package/src/package/migrate-api.d.ts +5 -0
  190. package/src/package/migrate.ts +56 -0
  191. package/src/package/plugins.ts +141 -0
  192. package/src/package/sqlite.mjs +2 -0
  193. package/src/package/vite-api.d.ts +5 -0
  194. package/src/package/vite.ts +324 -0
  195. package/src/package/watch-guard.cjs +26 -0
  196. package/src/plugins/client.ts +57 -0
  197. package/src/plugins/contract.ts +188 -0
  198. package/src/plugins/default.server.ts +4 -0
  199. package/src/plugins/default.ui.ts +4 -0
  200. package/src/plugins/operations.ts +45 -0
  201. package/src/plugins/server.ts +197 -0
  202. package/src/plugins/version.ts +1 -0
  203. package/src/routeTree.gen.ts +1275 -0
  204. package/src/router.tsx +46 -0
  205. package/src/routes/$teamSlug.index.tsx +53 -0
  206. package/src/routes/$teamSlug.new-project.tsx +13 -0
  207. package/src/routes/$teamSlug.projects.$projectName.backups.$backupId.tsx +40 -0
  208. package/src/routes/$teamSlug.projects.$projectName.backups.tsx +82 -0
  209. package/src/routes/$teamSlug.projects.$projectName.deployments.$deploymentId.tsx +52 -0
  210. package/src/routes/$teamSlug.projects.$projectName.deployments.tsx +86 -0
  211. package/src/routes/$teamSlug.projects.$projectName.logs.tsx +55 -0
  212. package/src/routes/$teamSlug.projects.$projectName.settings.tsx +51 -0
  213. package/src/routes/$teamSlug.projects.$projectName.tsx +106 -0
  214. package/src/routes/$teamSlug.projects.tsx +40 -0
  215. package/src/routes/$teamSlug.servers.$serverId.tsx +31 -0
  216. package/src/routes/$teamSlug.servers.index.tsx +59 -0
  217. package/src/routes/$teamSlug.servers.tsx +7 -0
  218. package/src/routes/$teamSlug.settings.tsx +56 -0
  219. package/src/routes/$teamSlug.tsx +86 -0
  220. package/src/routes/[.]well-known.oauth-authorization-server.api.auth.ts +13 -0
  221. package/src/routes/[.]well-known.oauth-protected-resource.mcp.ts +6 -0
  222. package/src/routes/__root.tsx +155 -0
  223. package/src/routes/admin.tsx +55 -0
  224. package/src/routes/api/agents/certificates.ts +21 -0
  225. package/src/routes/api/agents/credentials/rotate.ts +22 -0
  226. package/src/routes/api/agents/heartbeat.ts +42 -0
  227. package/src/routes/api/agents/job-logs.ts +22 -0
  228. package/src/routes/api/agents/jobs/complete.ts +22 -0
  229. package/src/routes/api/agents/jobs/next.ts +29 -0
  230. package/src/routes/api/agents/jobs/progress.ts +32 -0
  231. package/src/routes/api/agents/jobs/status.ts +22 -0
  232. package/src/routes/api/agents/register.ts +45 -0
  233. package/src/routes/api/agents/runtime-logs.ts +22 -0
  234. package/src/routes/api/agents/switch.ts +25 -0
  235. package/src/routes/api/auth/$.ts +38 -0
  236. package/src/routes/api/cron/backups.ts +33 -0
  237. package/src/routes/api/cron/maintenance.ts +30 -0
  238. package/src/routes/api/github/install.callback.ts +51 -0
  239. package/src/routes/api/github/install.ts +41 -0
  240. package/src/routes/api/github/webhook.ts +11 -0
  241. package/src/routes/api/health.ts +19 -0
  242. package/src/routes/api/install/agent.$token.ts +50 -0
  243. package/src/routes/api/install/agent.binary.ts +83 -0
  244. package/src/routes/api/install/agent.checksum.ts +45 -0
  245. package/src/routes/api/install/agent.ts +26 -0
  246. package/src/routes/api/plugins/$.ts +11 -0
  247. package/src/routes/api/v1/$.ts +15 -0
  248. package/src/routes/api/workspaces/updates.ts +8 -0
  249. package/src/routes/auth.login.tsx +50 -0
  250. package/src/routes/deployments.$deploymentId.unavailable.tsx +21 -0
  251. package/src/routes/index.tsx +23 -0
  252. package/src/routes/invites.$token.tsx +35 -0
  253. package/src/routes/mcp.ts +13 -0
  254. package/src/routes/oauth.consent.tsx +54 -0
  255. package/src/routes/onboarding.tsx +37 -0
  256. package/src/routes/onboarding.workspace.tsx +44 -0
  257. package/src/routes/settings.tsx +30 -0
  258. package/src/routes/uploads/avatars/$fileName.ts +44 -0
  259. package/src/server/admin/access.ts +67 -0
  260. package/src/server/admin/functions.ts +122 -0
  261. package/src/server/admin/service.ts +260 -0
  262. package/src/server/agents/backup-state.ts +210 -0
  263. package/src/server/agents/certificates.ts +96 -0
  264. package/src/server/agents/claims.ts +116 -0
  265. package/src/server/agents/cleanup.ts +52 -0
  266. package/src/server/agents/compatibility.ts +116 -0
  267. package/src/server/agents/completion.ts +83 -0
  268. package/src/server/agents/container-cleanup-state.ts +51 -0
  269. package/src/server/agents/deployment-state.ts +111 -0
  270. package/src/server/agents/job-payloads.ts +159 -0
  271. package/src/server/agents/jobs.ts +1293 -0
  272. package/src/server/agents/protocol.ts +400 -0
  273. package/src/server/agents/reconciliation.ts +43 -0
  274. package/src/server/agents/runtime-logs.ts +337 -0
  275. package/src/server/agents/runtime-state.ts +125 -0
  276. package/src/server/api/admission.ts +12 -0
  277. package/src/server/api/authorization.ts +115 -0
  278. package/src/server/api/credentials.ts +569 -0
  279. package/src/server/api/cursors.ts +72 -0
  280. package/src/server/api/errors.ts +24 -0
  281. package/src/server/api/functions.ts +60 -0
  282. package/src/server/api/handler.ts +274 -0
  283. package/src/server/api/mutation-operations.ts +913 -0
  284. package/src/server/api/operation-context.ts +139 -0
  285. package/src/server/api/operation-contract.ts +1099 -0
  286. package/src/server/api/operations.ts +64 -0
  287. package/src/server/api/read-operations.ts +891 -0
  288. package/src/server/api/retention.ts +33 -0
  289. package/src/server/auth/access.ts +109 -0
  290. package/src/server/auth/auth-methods.ts +28 -0
  291. package/src/server/auth/better-auth.ts +518 -0
  292. package/src/server/auth/context.ts +28 -0
  293. package/src/server/auth/email-change.ts +47 -0
  294. package/src/server/auth/email-delivery.ts +51 -0
  295. package/src/server/auth/provider-identity.ts +19 -0
  296. package/src/server/auth/provider-tokens.ts +27 -0
  297. package/src/server/auth/session.server.ts +19 -0
  298. package/src/server/backups/database-commands.ts +40 -0
  299. package/src/server/backups/destination.ts +12 -0
  300. package/src/server/backups/feature.ts +6 -0
  301. package/src/server/backups/policy-validation.ts +51 -0
  302. package/src/server/backups/queue-state.ts +137 -0
  303. package/src/server/backups/service.ts +918 -0
  304. package/src/server/backups/storage-functions.ts +42 -0
  305. package/src/server/backups/storage-state.ts +67 -0
  306. package/src/server/backups/storage.ts +237 -0
  307. package/src/server/config.ts +274 -0
  308. package/src/server/db/atomic.ts +16 -0
  309. package/src/server/db/d1-connection.ts +83 -0
  310. package/src/server/db/errors.ts +17 -0
  311. package/src/server/db/index.cloudflare.ts +15 -0
  312. package/src/server/db/index.ts +35 -0
  313. package/src/server/db/libsql-connection.ts +72 -0
  314. package/src/server/db/log-schema.ts +102 -0
  315. package/src/server/db/migrate-libsql.ts +29 -0
  316. package/src/server/db/migrate-sqlite.ts +47 -0
  317. package/src/server/db/migration-history.ts +12 -0
  318. package/src/server/db/migrations-logs/0000_initial.sql +48 -0
  319. package/src/server/db/migrations-logs/0001_runtime_log_order.sql +2 -0
  320. package/src/server/db/migrations-logs/meta/0000_snapshot.json +327 -0
  321. package/src/server/db/migrations-logs/meta/0001_snapshot.json +329 -0
  322. package/src/server/db/migrations-logs/meta/_journal.json +20 -0
  323. package/src/server/db/migrations-sqlite/0000_initial.sql +894 -0
  324. package/src/server/db/migrations-sqlite/0001_agent_indexes.sql +5 -0
  325. package/src/server/db/migrations-sqlite/meta/0000_snapshot.json +5816 -0
  326. package/src/server/db/migrations-sqlite/meta/0001_snapshot.json +5841 -0
  327. package/src/server/db/migrations-sqlite/meta/_journal.json +20 -0
  328. package/src/server/db/oauth-schema.ts +216 -0
  329. package/src/server/db/remote-config.ts +47 -0
  330. package/src/server/db/schema.ts +1218 -0
  331. package/src/server/db/sqlite-connection.ts +94 -0
  332. package/src/server/db/sqlite-files.ts +27 -0
  333. package/src/server/db/sqlite-params.ts +19 -0
  334. package/src/server/db/worker-connection.ts +47 -0
  335. package/src/server/deleted-resources.ts +133 -0
  336. package/src/server/deletion-recovery.ts +272 -0
  337. package/src/server/email/layout.tsx +85 -0
  338. package/src/server/email/notification-template.tsx +62 -0
  339. package/src/server/email/otp-template.tsx +48 -0
  340. package/src/server/email/service.ts +251 -0
  341. package/src/server/email/team-invite-template.tsx +59 -0
  342. package/src/server/email/theme.ts +10 -0
  343. package/src/server/github/http.ts +14 -0
  344. package/src/server/github/install-state.ts +48 -0
  345. package/src/server/github/service.ts +831 -0
  346. package/src/server/github/webhook-handler.ts +198 -0
  347. package/src/server/github/webhooks.ts +205 -0
  348. package/src/server/http/auth.ts +11 -0
  349. package/src/server/http/error-diagnostics.ts +66 -0
  350. package/src/server/http/plugin-request.ts +15 -0
  351. package/src/server/http/proxy-request.ts +53 -0
  352. package/src/server/http/public-errors.ts +48 -0
  353. package/src/server/http/rate-limit.ts +59 -0
  354. package/src/server/http/request-body.ts +54 -0
  355. package/src/server/http/request-ip.ts +54 -0
  356. package/src/server/http/security-headers.ts +36 -0
  357. package/src/server/install/agent-binary.ts +230 -0
  358. package/src/server/install/agent-script.ts +565 -0
  359. package/src/server/install/release-binary.ts +65 -0
  360. package/src/server/logs/budget.ts +72 -0
  361. package/src/server/logs/cleanup.ts +64 -0
  362. package/src/server/logs/deployments.ts +98 -0
  363. package/src/server/logs/operations.ts +145 -0
  364. package/src/server/logs/retention.ts +39 -0
  365. package/src/server/maintenance.ts +61 -0
  366. package/src/server/mcp/connections.ts +358 -0
  367. package/src/server/mcp/functions.ts +63 -0
  368. package/src/server/mcp/handler.ts +295 -0
  369. package/src/server/mcp/oauth-discovery.cloudflare.ts +6 -0
  370. package/src/server/mcp/oauth-discovery.ts +8 -0
  371. package/src/server/mcp/oauth-provider.ts +220 -0
  372. package/src/server/mcp/urls.ts +20 -0
  373. package/src/server/notifications/functions.ts +51 -0
  374. package/src/server/notifications/observe.ts +303 -0
  375. package/src/server/notifications/outbox.ts +335 -0
  376. package/src/server/notifications/preferences.ts +131 -0
  377. package/src/server/notifications/query.ts +16 -0
  378. package/src/server/plugins/scope.ts +17 -0
  379. package/src/server/plugins/server-context.ts +157 -0
  380. package/src/server/plugins/services.server.ts +10 -0
  381. package/src/server/plugins/workspace-access.ts +74 -0
  382. package/src/server/projects/backup-policy-state.ts +68 -0
  383. package/src/server/projects/backups.ts +219 -0
  384. package/src/server/projects/compose-metadata.ts +174 -0
  385. package/src/server/projects/configuration-state.ts +90 -0
  386. package/src/server/projects/context.ts +909 -0
  387. package/src/server/projects/creation-state.ts +46 -0
  388. package/src/server/projects/deletion-state.ts +69 -0
  389. package/src/server/projects/deployment-config.ts +139 -0
  390. package/src/server/projects/deployment-logs.ts +97 -0
  391. package/src/server/projects/deployments.ts +1123 -0
  392. package/src/server/projects/domain-state.ts +71 -0
  393. package/src/server/projects/environment-branches.ts +75 -0
  394. package/src/server/projects/environment-state.ts +158 -0
  395. package/src/server/projects/environments.ts +424 -0
  396. package/src/server/projects/functions.ts +748 -0
  397. package/src/server/projects/generated-hostname.ts +77 -0
  398. package/src/server/projects/hostnames.ts +244 -0
  399. package/src/server/projects/mappers.ts +457 -0
  400. package/src/server/projects/names.ts +41 -0
  401. package/src/server/projects/project-env-vars.ts +28 -0
  402. package/src/server/projects/queue-state.ts +118 -0
  403. package/src/server/projects/repository.ts +368 -0
  404. package/src/server/projects/route-state.ts +37 -0
  405. package/src/server/projects/runtime-logs.ts +97 -0
  406. package/src/server/projects/settings-context.ts +58 -0
  407. package/src/server/projects/settings.ts +738 -0
  408. package/src/server/projects/types.ts +355 -0
  409. package/src/server/realtime/agent-events.ts +32 -0
  410. package/src/server/realtime/node-broadcaster.ts +71 -0
  411. package/src/server/realtime/node-handler.ts +55 -0
  412. package/src/server/realtime/service.ts +125 -0
  413. package/src/server/realtime/workspace-object.cloudflare.ts +62 -0
  414. package/src/server/realtime/workspace-object.ts +3 -0
  415. package/src/server/resources/color.ts +13 -0
  416. package/src/server/runtime/background.ts +21 -0
  417. package/src/server/runtime/node-asset-cache-plugin.ts +12 -0
  418. package/src/server/runtime/node-scheduler-plugin.ts +17 -0
  419. package/src/server/runtime/node-scheduler.ts +51 -0
  420. package/src/server/runtime/platform-env.cloudflare.ts +17 -0
  421. package/src/server/runtime/platform-env.ts +19 -0
  422. package/src/server/runtime/scheduled-task-error.ts +9 -0
  423. package/src/server/scheduled-tasks.ts +28 -0
  424. package/src/server/secrets/crypto.ts +56 -0
  425. package/src/server/servers/functions.ts +233 -0
  426. package/src/server/servers/lifecycle.ts +122 -0
  427. package/src/server/servers/metric-history-schema.ts +24 -0
  428. package/src/server/servers/metric-history.ts +55 -0
  429. package/src/server/servers/monitoring.ts +592 -0
  430. package/src/server/servers/service.ts +1211 -0
  431. package/src/server/servers/status.ts +32 -0
  432. package/src/server/servers/switch.ts +96 -0
  433. package/src/server/settings-version.ts +43 -0
  434. package/src/server/storage/capabilities.ts +6 -0
  435. package/src/server/storage/local-object-store.cloudflare.ts +7 -0
  436. package/src/server/storage/local-object-store.ts +50 -0
  437. package/src/server/storage/object-store.ts +160 -0
  438. package/src/server/teams/creation.ts +51 -0
  439. package/src/server/teams/delete-policy.ts +13 -0
  440. package/src/server/teams/deletion-state.ts +80 -0
  441. package/src/server/teams/functions.ts +324 -0
  442. package/src/server/teams/invitations.ts +38 -0
  443. package/src/server/teams/membership.ts +86 -0
  444. package/src/server/teams/queries.ts +261 -0
  445. package/src/server/teams/service.ts +1127 -0
  446. package/src/server/teams/types.ts +101 -0
  447. package/src/server/uploads/avatar.ts +133 -0
  448. package/src/server/users/deletion-state.ts +93 -0
  449. package/src/server/users/functions.ts +160 -0
  450. package/src/server/users/onboarding.ts +17 -0
  451. package/src/server/users/service.ts +644 -0
  452. package/src/server.ts +55 -0
  453. package/src/styles/shadcn.css +653 -0
  454. package/src/styles/theme.css +76 -0
  455. package/src/styles.css +177 -0
  456. package/wrangler.template.jsonc +37 -0
@@ -0,0 +1,913 @@
1
+ import { withWorkspaceAccess } from '#devpush/server/plugins/workspace-access'
2
+ import {
3
+ hostLifecycleCommandsChanged,
4
+ hostLifecycleCommandsUseChangedSource,
5
+ requireProjectRuntimeConfig,
6
+ } from '#devpush/lib/project-runtime-config'
7
+ import { PublicError } from '#devpush/lib/public-error'
8
+ import { prepareBackupPolicy } from '#devpush/server/backups/policy-validation'
9
+ import {
10
+ queueBackupRestoreJob,
11
+ queueEnvironmentBackup,
12
+ } from '#devpush/server/backups/service'
13
+ import {
14
+ getOwnedBackupStorage,
15
+ prepareBackupStorage,
16
+ } from '#devpush/server/backups/storage'
17
+ import {
18
+ deleteBackupStorageState,
19
+ saveBackupStorageState,
20
+ } from '#devpush/server/backups/storage-state'
21
+ import { db } from '#devpush/server/db'
22
+ import { isUniqueConstraintError } from '#devpush/server/db/errors'
23
+ import {
24
+ agentJobs,
25
+ apiAudit,
26
+ apiIdempotency,
27
+ backupConfigs,
28
+ backupRuns,
29
+ deployments,
30
+ environments,
31
+ projects,
32
+ servers,
33
+ teams,
34
+ teamMembers,
35
+ } from '#devpush/server/db/schema'
36
+ import {
37
+ deleteProjectBackupPolicyState,
38
+ saveProjectBackupPolicyState,
39
+ } from '#devpush/server/projects/backup-policy-state'
40
+ import { updateProjectRuntimeState } from '#devpush/server/projects/configuration-state'
41
+ import { deleteProjectState } from '#devpush/server/projects/deletion-state'
42
+ import {
43
+ createManualDeployment,
44
+ createRedeployDeployment,
45
+ createRollbackDeployment,
46
+ } from '#devpush/server/projects/deployments'
47
+ import {
48
+ updateProjectEnvironments,
49
+ updateProjectEnvironmentServers,
50
+ } from '#devpush/server/projects/environments'
51
+ import { normalizeProjectRoutes } from '#devpush/server/projects/mappers'
52
+ import {
53
+ encryptProjectEnvVars,
54
+ readProjectEnvVars,
55
+ } from '#devpush/server/projects/project-env-vars'
56
+ import { createProjectFromGitHub } from '#devpush/server/projects/repository'
57
+ import {
58
+ notifyProject,
59
+ notifyWorkspace,
60
+ } from '#devpush/server/realtime/service'
61
+ import {
62
+ changeServerEnabled,
63
+ deleteServerState,
64
+ } from '#devpush/server/servers/lifecycle'
65
+ import { getEffectiveServerStatus } from '#devpush/server/servers/status'
66
+ import type { SQL } from 'drizzle-orm'
67
+ import { and, eq, ne, sql } from 'drizzle-orm'
68
+ import type { ApiMutationAdmission } from './admission'
69
+ import { apiAuthorizationCondition, requireApiScope } from './authorization'
70
+ import { apiFingerprint } from './cursors'
71
+ import { ApiError } from './errors'
72
+
73
+ import type { OperationContext } from './operation-context'
74
+ import {
75
+ assertCurrentAuthorization,
76
+ backupRow,
77
+ deploymentRow,
78
+ projectRow,
79
+ requireWholeTeam,
80
+ serialize,
81
+ serverSelection,
82
+ } from './operation-context'
83
+
84
+ const API_IDEMPOTENCY_RETENTION_MS = 24 * 60 * 60 * 1000
85
+ type Replay = { keyHash: string; requestHash: string; resourceId: string }
86
+ function replayIdentity(context: OperationContext, resourceId: string): Replay {
87
+ const key = context.input.idempotencyKey
88
+ if (typeof key !== 'string' || !key.trim())
89
+ throw new ApiError('IDEMPOTENCY_REQUIRED', 400)
90
+ const { idempotencyKey: _key, ...payload } = context.input
91
+ return {
92
+ keyHash: apiFingerprint(key),
93
+ requestHash: apiFingerprint(payload),
94
+ resourceId,
95
+ }
96
+ }
97
+ async function readReplay(context: OperationContext, identity: Replay) {
98
+ const [record] = await db
99
+ .select({
100
+ response: apiIdempotency.response,
101
+ requestHash: apiIdempotency.requestHash,
102
+ })
103
+ .from(apiIdempotency)
104
+ .where(
105
+ and(
106
+ eq(apiIdempotency.membershipId, context.principal.membershipId),
107
+ eq(apiIdempotency.operation, context.operation.id),
108
+ eq(apiIdempotency.resourceId, identity.resourceId),
109
+ eq(apiIdempotency.keyHash, identity.keyHash),
110
+ ),
111
+ )
112
+ .limit(1)
113
+ if (!record) return null
114
+ if (record.requestHash !== identity.requestHash)
115
+ throw new ApiError('IDEMPOTENCY_CONFLICT', 409)
116
+ await assertCurrentAuthorization(context)
117
+ return context.operation.output.parse(record.response)
118
+ }
119
+ function admission(
120
+ context: OperationContext,
121
+ identity: Replay,
122
+ resourceId: string,
123
+ response: unknown,
124
+ effectExists: SQL,
125
+ secondaryResourceId?: string,
126
+ ): ApiMutationAdmission {
127
+ const now = Date.now()
128
+ const { principal, operation } = context
129
+ const safeResponse = operation.output.parse(response)
130
+ const replayId = crypto.randomUUID()
131
+ return {
132
+ resourceId,
133
+ secondaryResourceId,
134
+ idempotencyId: replayId,
135
+ guard: apiAuthorizationCondition(principal, {
136
+ scope: operation.scope,
137
+ projectId: context.input.projectId,
138
+ }),
139
+ statements: [
140
+ db.run(sql`insert into ${apiIdempotency} (id,membership_id,credential_id,operation,resource_id,key_hash,request_hash,response,status_code,created_at,expires_at)
141
+ select ${replayId},${principal.membershipId},${principal.credentialId},${operation.id},${identity.resourceId},${identity.keyHash},${identity.requestHash},${JSON.stringify(safeResponse)},${operation.status},${now},${now + API_IDEMPOTENCY_RETENTION_MS} where ${effectExists}`),
142
+ db.run(sql`insert into ${apiAudit} (id,team_id,user_id,credential_id,request_id,operation,resource_id,outcome,created_at)
143
+ select ${crypto.randomUUID()},${principal.teamId},${principal.userId},${principal.credentialId},${context.requestId},${operation.id},${resourceId},'accepted',${now} where exists(select 1 from ${apiIdempotency} where id = ${replayId})`),
144
+ ],
145
+ }
146
+ }
147
+ export async function mutateOperation(
148
+ context: OperationContext,
149
+ ): Promise<unknown> {
150
+ const { operation, principal, input: i } = context
151
+ let projectId: string | undefined = i.projectId
152
+ let source: Awaited<ReturnType<typeof deploymentRow>> | undefined
153
+ if (['redeploy', 'rollback'].includes(operation.id)) {
154
+ source = await deploymentRow(context, i.deploymentId)
155
+ projectId = source.projectId
156
+ }
157
+ if (operation.id === 'restore_backup')
158
+ projectId = (await backupRow(context, i.backupRunId)).projectId
159
+ if (projectId) i.projectId = projectId
160
+ if (
161
+ [
162
+ 'create_project',
163
+ 'rename_server',
164
+ 'create_server',
165
+ 'enable_server',
166
+ 'disable_server',
167
+ 'delete_server',
168
+ 'create_backup_storage',
169
+ 'update_backup_storage',
170
+ 'delete_backup_storage',
171
+ ].includes(operation.id)
172
+ )
173
+ requireWholeTeam(principal)
174
+ const identity = replayIdentity(
175
+ context,
176
+ i.deploymentId ??
177
+ i.backupRunId ??
178
+ i.policyId ??
179
+ i.storageId ??
180
+ i.projectId ??
181
+ i.serverId ??
182
+ principal.teamId,
183
+ )
184
+ const existing = await readReplay(context, identity)
185
+ if (existing) return existing
186
+ if (projectId) await projectRow(context, projectId)
187
+ const apply = async () => {
188
+ try {
189
+ if (
190
+ operation.id === 'update_environments' ||
191
+ operation.id === 'update_environment_servers'
192
+ ) {
193
+ const { project, teamSlug } = await projectRow(context, projectId!)
194
+ if (project.settingsVersion !== i.settingsVersion)
195
+ throw new ApiError('SETTINGS_CONFLICT', 409)
196
+ const now = new Date(
197
+ Math.max(Date.now(), project.updatedAt.getTime() + 1),
198
+ )
199
+ const response = {
200
+ projectId,
201
+ settingsVersion: project.settingsVersion + 1,
202
+ }
203
+ const admitted = admission(
204
+ context,
205
+ identity,
206
+ projectId!,
207
+ response,
208
+ sql`changes() = 1`,
209
+ )
210
+ const assignedServers =
211
+ operation.id === 'update_environment_servers'
212
+ ? [
213
+ i.defaultServerId,
214
+ ...i.environmentServers.map(
215
+ (assignment: OperationContext['input']) =>
216
+ assignment.serverId,
217
+ ),
218
+ ].filter(Boolean)
219
+ : []
220
+ admitted.guard = and(
221
+ admitted.guard,
222
+ sql`not exists(select 1 from json_each(${JSON.stringify(assignedServers)}) as assigned where not exists(select 1 from ${servers} where id = assigned.value and team_id = ${principal.teamId} and status <> 'deleted'))`,
223
+ )!
224
+ const base = {
225
+ userId: principal.userId,
226
+ teamSlug,
227
+ projectName: project.name,
228
+ admission: admitted,
229
+ settingsVersion: project.settingsVersion,
230
+ mutationTime: now,
231
+ }
232
+ if (operation.id === 'update_environments')
233
+ await updateProjectEnvironments({
234
+ ...base,
235
+ projectEnvironments: i.environments,
236
+ })
237
+ else
238
+ await updateProjectEnvironmentServers({
239
+ ...base,
240
+ defaultServerId: i.defaultServerId,
241
+ environmentServers: i.environmentServers,
242
+ })
243
+ const recorded = await readReplay(context, identity)
244
+ if (!recorded) throw new ApiError('OPERATION_CONFLICT', 409)
245
+ await notifyProject(projectId!)
246
+ return recorded
247
+ }
248
+ if (
249
+ [
250
+ 'create_backup_policy',
251
+ 'update_backup_policy',
252
+ 'delete_backup_policy',
253
+ ].includes(operation.id)
254
+ ) {
255
+ requireApiScope(principal, operation.scope, 'admin')
256
+ await projectRow(context, projectId!)
257
+ const [policy] = i.policyId
258
+ ? await db
259
+ .select()
260
+ .from(backupConfigs)
261
+ .where(
262
+ and(
263
+ eq(backupConfigs.id, i.policyId),
264
+ eq(backupConfigs.projectId, projectId!),
265
+ ),
266
+ )
267
+ .limit(1)
268
+ : []
269
+ if (i.policyId && !policy)
270
+ throw new PublicError('BACKUP_POLICY_NOT_FOUND')
271
+ if (policy && policy.settingsVersion !== i.settingsVersion)
272
+ throw new ApiError('SETTINGS_CONFLICT', 409)
273
+ const policyId = i.policyId ?? crypto.randomUUID()
274
+ const now = Date.now()
275
+ const response = {
276
+ policyId,
277
+ projectId,
278
+ settingsVersion: policy ? policy.settingsVersion + 1 : 1,
279
+ }
280
+ const admitted = admission(
281
+ context,
282
+ identity,
283
+ policyId,
284
+ response,
285
+ sql`changes() = 1`,
286
+ )
287
+ admitted.guard = apiAuthorizationCondition(principal, {
288
+ scope: operation.scope,
289
+ projectId,
290
+ minimumRole: 'admin',
291
+ })
292
+ const base = {
293
+ projectId: projectId!,
294
+ teamId: principal.teamId,
295
+ userId: principal.userId,
296
+ settingsVersion:
297
+ operation.id === 'create_backup_policy' ? null : i.settingsVersion,
298
+ admission: admitted,
299
+ }
300
+ const changed =
301
+ operation.id === 'delete_backup_policy'
302
+ ? await deleteProjectBackupPolicyState(db, {
303
+ ...base,
304
+ policyId,
305
+ settingsVersion: i.settingsVersion,
306
+ })
307
+ : await saveProjectBackupPolicyState(db, {
308
+ ...base,
309
+ policy: await prepareBackupPolicy({
310
+ teamId: principal.teamId,
311
+ projectId: projectId!,
312
+ policy: { ...i, id: policyId },
313
+ }),
314
+ now: new Date(now),
315
+ })
316
+ if (!changed) throw new ApiError('SETTINGS_CONFLICT', 409)
317
+ await notifyProject(projectId!)
318
+ return response
319
+ }
320
+ if (
321
+ [
322
+ 'create_backup_storage',
323
+ 'update_backup_storage',
324
+ 'delete_backup_storage',
325
+ ].includes(operation.id)
326
+ ) {
327
+ requireApiScope(principal, operation.scope, 'admin')
328
+ const storageId = i.storageId ?? crypto.randomUUID()
329
+ const prepared =
330
+ operation.id === 'delete_backup_storage'
331
+ ? null
332
+ : await prepareBackupStorage({
333
+ teamId: principal.teamId,
334
+ input: { ...i, id: i.storageId },
335
+ settingsVersion: i.settingsVersion,
336
+ newStorageId: storageId,
337
+ })
338
+ const existingStorage =
339
+ operation.id === 'delete_backup_storage'
340
+ ? await getOwnedBackupStorage(principal.teamId, storageId)
341
+ : null
342
+ if (operation.id === 'delete_backup_storage') {
343
+ if (!existingStorage)
344
+ throw new PublicError('BACKUP_STORAGE_NOT_FOUND')
345
+ if (existingStorage.settingsVersion !== i.settingsVersion)
346
+ throw new PublicError('SETTINGS_CONFLICT')
347
+ }
348
+ const response = {
349
+ storageId,
350
+ settingsVersion: prepared
351
+ ? (prepared.settingsVersion ?? 0) + 1
352
+ : i.settingsVersion + 1,
353
+ }
354
+ const admitted = admission(
355
+ context,
356
+ identity,
357
+ storageId,
358
+ response,
359
+ sql`changes() = 1`,
360
+ )
361
+ admitted.guard = apiAuthorizationCondition(principal, {
362
+ scope: operation.scope,
363
+ minimumRole: 'admin',
364
+ })
365
+ const base = {
366
+ teamId: principal.teamId,
367
+ userId: principal.userId,
368
+ storageId,
369
+ admission: admitted,
370
+ }
371
+ const changed =
372
+ operation.id === 'delete_backup_storage'
373
+ ? await deleteBackupStorageState(db, {
374
+ ...base,
375
+ settingsVersion: i.settingsVersion,
376
+ })
377
+ : await saveBackupStorageState(db, {
378
+ ...base,
379
+ settingsVersion: prepared!.settingsVersion,
380
+ values: prepared!.values,
381
+ now: new Date(),
382
+ })
383
+ if (!changed)
384
+ throw new PublicError(
385
+ operation.id === 'delete_backup_storage'
386
+ ? 'BACKUP_STORAGE_IN_USE'
387
+ : prepared!.settingsVersion === null
388
+ ? 'FORBIDDEN'
389
+ : 'SETTINGS_CONFLICT',
390
+ )
391
+ return response
392
+ }
393
+ if (operation.id === 'create_project') {
394
+ if (i.envVars?.length) requireApiScope(principal, 'secrets:write')
395
+ const createdProjectId = crypto.randomUUID()
396
+ const response = {
397
+ projectId: createdProjectId,
398
+ statusUrl: `/api/v1/projects/${createdProjectId}`,
399
+ }
400
+ const admitted = admission(
401
+ context,
402
+ identity,
403
+ createdProjectId,
404
+ response,
405
+ sql`exists(select 1 from ${projects} where id = ${createdProjectId})`,
406
+ )
407
+ if (i.envVars?.length)
408
+ admitted.guard = and(
409
+ admitted.guard,
410
+ apiAuthorizationCondition(principal, { scope: 'secrets:write' }),
411
+ )!
412
+ const [team] = await db
413
+ .select({ slug: teams.slug })
414
+ .from(teams)
415
+ .where(eq(teams.id, principal.teamId))
416
+ .limit(1)
417
+ if (!team) throw new ApiError('NOT_FOUND', 404)
418
+ await createProjectFromGitHub({
419
+ ...i,
420
+ userId: principal.userId,
421
+ teamSlug: team.slug,
422
+ projectName: i.projectName,
423
+ repoFullName: i.repoFullName,
424
+ installationId: i.installationId,
425
+ serverId: i.serverId ?? null,
426
+ admission: admitted,
427
+ })
428
+ const recorded = await readReplay(context, identity)
429
+ if (!recorded) throw new ApiError('OPERATION_CONFLICT', 409)
430
+ await notifyWorkspace(principal.teamId, 'deployments')
431
+ return recorded
432
+ }
433
+ if (operation.id === 'delete_project') {
434
+ const { project } = await projectRow(context, projectId!)
435
+ requireApiScope(principal, operation.scope, 'admin')
436
+ if (project.name !== i.confirmationName)
437
+ throw new PublicError('PROJECT_DELETE_CONFIRMATION')
438
+ const response = { projectId, status: 'deleted' }
439
+ const admitted = admission(
440
+ context,
441
+ identity,
442
+ projectId!,
443
+ response,
444
+ sql`changes() = 1`,
445
+ )
446
+ admitted.guard = apiAuthorizationCondition(principal, {
447
+ scope: operation.scope,
448
+ projectId,
449
+ minimumRole: 'admin',
450
+ })
451
+ const changed = await deleteProjectState(db, {
452
+ projectId: projectId!,
453
+ teamId: principal.teamId,
454
+ userId: principal.userId,
455
+ removeDeployments: i.removeDeployments,
456
+ now: new Date(),
457
+ admission: admitted,
458
+ })
459
+ if (!changed) throw new ApiError('OPERATION_CONFLICT', 409)
460
+ await notifyWorkspace(principal.teamId, 'deployments')
461
+ return response
462
+ }
463
+ if (
464
+ ['enable_server', 'disable_server', 'delete_server'].includes(
465
+ operation.id,
466
+ )
467
+ ) {
468
+ const [server] = await db
469
+ .select({ id: servers.id, name: servers.name })
470
+ .from(servers)
471
+ .where(
472
+ and(
473
+ eq(servers.id, i.serverId),
474
+ eq(servers.teamId, principal.teamId),
475
+ ne(servers.status, 'deleted'),
476
+ ),
477
+ )
478
+ .limit(1)
479
+ if (!server) throw new ApiError('NOT_FOUND', 404)
480
+ if (
481
+ operation.id !== 'enable_server' &&
482
+ i.confirmationName !== server.name
483
+ )
484
+ throw new PublicError(
485
+ operation.id === 'disable_server'
486
+ ? 'SERVER_DISABLE_CONFIRMATION'
487
+ : 'SERVER_DELETE_CONFIRMATION',
488
+ )
489
+ const response =
490
+ operation.id === 'delete_server'
491
+ ? { serverId: server.id, status: 'deleted' }
492
+ : { serverId: server.id, enabled: operation.id === 'enable_server' }
493
+ const admitted = admission(
494
+ context,
495
+ identity,
496
+ server.id,
497
+ response,
498
+ sql`changes() = 1`,
499
+ )
500
+ const base = {
501
+ userId: principal.userId,
502
+ teamId: principal.teamId,
503
+ serverId: server.id,
504
+ now: new Date(),
505
+ admission: admitted,
506
+ }
507
+ const changed =
508
+ operation.id === 'delete_server'
509
+ ? await withWorkspaceAccess(
510
+ principal.teamId,
511
+ 'server.delete',
512
+ () =>
513
+ deleteServerState(db, {
514
+ ...base,
515
+ cancelOngoingDeployments: i.cancelOngoingDeployments,
516
+ }),
517
+ {
518
+ kind: 'user',
519
+ userId: principal.userId,
520
+ roleCeiling: principal.roleCeiling,
521
+ },
522
+ )
523
+ : await changeServerEnabled(db, {
524
+ ...base,
525
+ enabled: operation.id === 'enable_server',
526
+ })
527
+ if (!changed) throw new ApiError('OPERATION_CONFLICT', 409)
528
+ await notifyWorkspace(principal.teamId, 'servers')
529
+ return response
530
+ }
531
+ if (operation.id === 'update_project_configuration') {
532
+ const { project } = await projectRow(context, projectId!)
533
+ if (project.settingsVersion !== i.settingsVersion)
534
+ throw new ApiError('SETTINGS_CONFLICT', 409)
535
+ const previous = requireProjectRuntimeConfig(project.runtimeConfig)
536
+ const changedHostCommands =
537
+ hostLifecycleCommandsChanged(previous, i.runtimeConfig) ||
538
+ hostLifecycleCommandsUseChangedSource({
539
+ currentRepoFullName: project.githubRepo,
540
+ nextRepoFullName: project.githubRepo,
541
+ currentRuntimeConfig: previous,
542
+ nextRuntimeConfig: i.runtimeConfig,
543
+ })
544
+ if (changedHostCommands)
545
+ requireApiScope(principal, 'projects:host-commands', 'admin')
546
+ const envIds = [
547
+ ...new Set(
548
+ i.routes
549
+ .map((route: OperationContext['input']) => route.environmentId)
550
+ .filter(Boolean),
551
+ ),
552
+ ]
553
+ const available = await db
554
+ .select({ id: environments.id })
555
+ .from(environments)
556
+ .where(
557
+ and(
558
+ eq(environments.projectId, projectId!),
559
+ eq(environments.status, 'active'),
560
+ ),
561
+ )
562
+ .limit(1001)
563
+ if (envIds.some((id) => !available.some((row) => row.id === id)))
564
+ throw new PublicError('PROJECT_ROUTE_ENVIRONMENT_INVALID')
565
+ const now = new Date(
566
+ Math.max(Date.now(), project.updatedAt.getTime() + 1),
567
+ )
568
+ const response = {
569
+ projectId,
570
+ settingsVersion: project.settingsVersion + 1,
571
+ }
572
+ const admitted = admission(
573
+ context,
574
+ identity,
575
+ projectId!,
576
+ response,
577
+ sql`changes() = 1`,
578
+ )
579
+ const hostGuard = changedHostCommands
580
+ ? apiAuthorizationCondition(principal, {
581
+ scope: 'projects:host-commands',
582
+ projectId,
583
+ minimumRole: 'admin',
584
+ })
585
+ : sql`1`
586
+ admitted.guard = and(admitted.guard, hostGuard)!
587
+ const changed = await updateProjectRuntimeState(db, {
588
+ projectId: projectId!,
589
+ userId: principal.userId,
590
+ settingsVersion: project.settingsVersion,
591
+ currentRuntimeConfig: project.runtimeConfig,
592
+ runtimeConfig: i.runtimeConfig,
593
+ routes: normalizeProjectRoutes(i.routes),
594
+ now,
595
+ admission: admitted,
596
+ })
597
+ if (!changed) throw new ApiError('SETTINGS_CONFLICT', 409)
598
+ await notifyProject(projectId!)
599
+ return response
600
+ }
601
+ if (['deploy', 'redeploy', 'rollback'].includes(operation.id)) {
602
+ const { project, teamSlug } = await projectRow(context, projectId!)
603
+ const deploymentId = crypto.randomUUID()
604
+ const response = {
605
+ deploymentId,
606
+ statusUrl: `/api/v1/deployments/${deploymentId}`,
607
+ }
608
+ const admitted = admission(
609
+ context,
610
+ identity,
611
+ deploymentId,
612
+ response,
613
+ sql`exists(select 1 from ${deployments} where id = ${deploymentId})`,
614
+ )
615
+ if (operation.id === 'deploy')
616
+ await createManualDeployment({
617
+ userId: principal.userId,
618
+ teamSlug,
619
+ projectName: project.name,
620
+ environmentId: i.environmentId,
621
+ gitBranch: i.branch,
622
+ gitSha: i.sha,
623
+ admission: admitted,
624
+ })
625
+ else
626
+ await (
627
+ operation.id === 'rollback'
628
+ ? createRollbackDeployment
629
+ : createRedeployDeployment
630
+ )({
631
+ userId: principal.userId,
632
+ teamSlug,
633
+ projectName: project.name,
634
+ deploymentId: source!.deployment.id,
635
+ admission: admitted,
636
+ })
637
+ const recorded = await readReplay(context, identity)
638
+ if (!recorded) throw new ApiError('OPERATION_CONFLICT', 409)
639
+ return recorded
640
+ }
641
+ if (operation.id === 'run_backup') {
642
+ const backupRunId = crypto.randomUUID()
643
+ const agentJobId = crypto.randomUUID()
644
+ const response = {
645
+ backupRunId,
646
+ agentJobId,
647
+ statusUrl: `/api/v1/backups/${backupRunId}`,
648
+ }
649
+ const admitted = admission(
650
+ context,
651
+ identity,
652
+ backupRunId,
653
+ response,
654
+ sql`exists(select 1 from ${backupRuns} where id = ${backupRunId})`,
655
+ agentJobId,
656
+ )
657
+ const queued = await queueEnvironmentBackup({
658
+ actor: {
659
+ kind: 'user',
660
+ userId: principal.userId,
661
+ roleCeiling: principal.roleCeiling,
662
+ },
663
+ projectId: projectId!,
664
+ environmentId: i.environmentId,
665
+ backupConfigId: i.policyId,
666
+ admission: admitted,
667
+ })
668
+ if (!queued) throw new ApiError('OPERATION_CONFLICT', 409)
669
+ await notifyProject(projectId!)
670
+ return response
671
+ }
672
+ if (operation.id === 'restore_backup') {
673
+ const agentJobId = crypto.randomUUID()
674
+ const response = { agentJobId, statusUrl: `/api/v1/jobs/${agentJobId}` }
675
+ const admitted = admission(
676
+ context,
677
+ identity,
678
+ agentJobId,
679
+ response,
680
+ sql`exists(select 1 from ${agentJobs} where id = ${agentJobId})`,
681
+ )
682
+ await queueBackupRestoreJob({
683
+ actor: {
684
+ kind: 'user',
685
+ userId: principal.userId,
686
+ roleCeiling: principal.roleCeiling,
687
+ },
688
+ projectId: projectId!,
689
+ backupRunId: i.backupRunId,
690
+ artifacts: i.artifacts,
691
+ admission: admitted,
692
+ })
693
+ await notifyProject(projectId!)
694
+ const recorded = await readReplay(context, identity)
695
+ if (!recorded) throw new ApiError('OPERATION_CONFLICT', 409)
696
+ return recorded
697
+ }
698
+ if (
699
+ operation.id === 'rename_project' ||
700
+ operation.id === 'update_variables'
701
+ ) {
702
+ const { project } = await projectRow(context, projectId!)
703
+ if (project.settingsVersion !== i.settingsVersion)
704
+ throw new ApiError('SETTINGS_CONFLICT', 409)
705
+ const now = new Date(
706
+ Math.max(Date.now(), project.updatedAt.getTime() + 1),
707
+ )
708
+ let ciphertext = project.envVarsEncrypted
709
+ if (operation.id === 'update_variables') {
710
+ const rows = readProjectEnvVars(project)
711
+ const seen = new Set<string>()
712
+ const environmentIds = [
713
+ ...new Set(
714
+ i.changes
715
+ .map(
716
+ (change: OperationContext['input']) => change.environmentId,
717
+ )
718
+ .filter(Boolean),
719
+ ),
720
+ ]
721
+ const valid = await db
722
+ .select({ id: environments.id })
723
+ .from(environments)
724
+ .where(
725
+ and(
726
+ eq(environments.projectId, projectId!),
727
+ eq(environments.status, 'active'),
728
+ ),
729
+ )
730
+ .limit(1001)
731
+ if (environmentIds.some((id) => !valid.some((row) => row.id === id)))
732
+ throw new PublicError('PROJECT_VARIABLE_ENVIRONMENT_INVALID')
733
+ for (const change of i.changes) {
734
+ const key = `${change.environmentId ?? ''}\0${change.key}`
735
+ if (seen.has(key))
736
+ throw new PublicError('PROJECT_VARIABLE_DUPLICATED')
737
+ seen.add(key)
738
+ const index = rows.findIndex(
739
+ (row) =>
740
+ (row.environment || null) === change.environmentId &&
741
+ row.key === change.key,
742
+ )
743
+ if (index >= 0) rows.splice(index, 1)
744
+ if (change.value !== null)
745
+ rows.push({
746
+ key: change.key,
747
+ environment: change.environmentId ?? undefined,
748
+ value: change.value,
749
+ })
750
+ }
751
+ ciphertext = encryptProjectEnvVars(rows)
752
+ }
753
+ const response =
754
+ operation.id === 'rename_project'
755
+ ? serialize({
756
+ id: project.id,
757
+ teamId: project.teamId,
758
+ name: i.name,
759
+ slug: i.name,
760
+ repository: project.githubRepo,
761
+ repositoryId: project.githubRepoId,
762
+ defaultServerId: project.defaultServerId,
763
+ settingsVersion: project.settingsVersion + 1,
764
+ createdAt: project.createdAt,
765
+ })
766
+ : { projectId, settingsVersion: project.settingsVersion + 1 }
767
+ const effect = sql`changes() = 1`
768
+ const admitted = admission(
769
+ context,
770
+ identity,
771
+ projectId!,
772
+ response,
773
+ effect,
774
+ )
775
+ const [changed] = await db.batch([
776
+ db.all(
777
+ sql`update ${projects} set settings_version = settings_version + 1,updated_at = ${now.getTime()}, ${operation.id === 'rename_project' ? sql`name = ${i.name}, slug = ${i.name}` : sql`env_vars_encrypted = ${ciphertext}`} where id = ${projectId} and status = 'active' and settings_version = ${project.settingsVersion} and env_vars_encrypted is ${project.envVarsEncrypted} and ${admitted.guard} returning id`,
778
+ ),
779
+ ...admitted.statements,
780
+ ])
781
+ if (!changed.length) throw new ApiError('SETTINGS_CONFLICT', 409)
782
+ await notifyProject(projectId!)
783
+ return response
784
+ }
785
+ if (
786
+ operation.id === 'rename_server' ||
787
+ operation.id === 'create_server'
788
+ ) {
789
+ const now = new Date()
790
+ const serverId = i.serverId ?? crypto.randomUUID()
791
+ const [existingServer] = i.serverId
792
+ ? await db
793
+ .select(serverSelection)
794
+ .from(servers)
795
+ .where(
796
+ and(
797
+ eq(servers.id, i.serverId),
798
+ eq(servers.teamId, principal.teamId),
799
+ ne(servers.status, 'deleted'),
800
+ ),
801
+ )
802
+ .limit(1)
803
+ : []
804
+ if (i.serverId && !existingServer) throw new ApiError('NOT_FOUND', 404)
805
+ if (
806
+ existingServer &&
807
+ existingServer.settingsVersion !== i.settingsVersion
808
+ )
809
+ throw new ApiError('SETTINGS_CONFLICT', 409)
810
+ const response = existingServer
811
+ ? serialize({
812
+ ...existingServer,
813
+ settingsVersion: existingServer.settingsVersion + 1,
814
+ name: i.name,
815
+ status: getEffectiveServerStatus(existingServer),
816
+ })
817
+ : {
818
+ settingsVersion: 1,
819
+ id: serverId,
820
+ teamId: principal.teamId,
821
+ name: i.name,
822
+ status: 'pending',
823
+ publicAddress: null,
824
+ agentVersion: null,
825
+ lastSeenAt: null,
826
+ createdAt: now.toISOString(),
827
+ }
828
+ const effect = sql`changes() = 1`
829
+ const admitted = admission(
830
+ context,
831
+ identity,
832
+ serverId,
833
+ response,
834
+ effect,
835
+ )
836
+ if (operation.id === 'create_server') {
837
+ const [member] = await db
838
+ .select({ role: teamMembers.role })
839
+ .from(teamMembers)
840
+ .where(
841
+ and(
842
+ eq(teamMembers.id, principal.membershipId),
843
+ eq(teamMembers.userId, principal.userId),
844
+ eq(teamMembers.teamId, principal.teamId),
845
+ ),
846
+ )
847
+ .limit(1)
848
+ if (!member) throw new PublicError('FORBIDDEN')
849
+ admitted.guard = sql`${admitted.guard} and exists(select 1 from team_members where id=${principal.membershipId} and role=${member.role})`
850
+ }
851
+ const write = () =>
852
+ db.batch([
853
+ operation.id === 'rename_server'
854
+ ? db.all(
855
+ sql`update ${servers} set settings_version = settings_version + 1,name = ${i.name}, handle = ${i.name}, updated_at = ${now.getTime()} where id = ${serverId} and settings_version = ${i.settingsVersion} and team_id = ${principal.teamId} and status <> 'deleted' and ${admitted.guard} returning id`,
856
+ )
857
+ : db.all(
858
+ sql`insert into ${servers} (id,team_id,name,handle,status,created_at,updated_at) select ${serverId},${principal.teamId},${i.name},${i.name},'pending',${now.getTime()},${now.getTime()} where ${admitted.guard} returning id`,
859
+ ),
860
+ ...(operation.id === 'create_server'
861
+ ? [
862
+ db.run(
863
+ sql`insert into server_availability_windows (id,server_id,started_at) select ${crypto.randomUUID()},${serverId},${now.getTime()} where changes()=1`,
864
+ ),
865
+ ]
866
+ : []),
867
+ ...admitted.statements,
868
+ ])
869
+ const [changed] =
870
+ operation.id === 'create_server'
871
+ ? await withWorkspaceAccess(
872
+ principal.teamId,
873
+ 'server.create',
874
+ write,
875
+ {
876
+ kind: 'user',
877
+ userId: principal.userId,
878
+ roleCeiling: principal.roleCeiling,
879
+ },
880
+ )
881
+ : await write()
882
+ if (!changed.length) throw new PublicError('FORBIDDEN')
883
+ await notifyWorkspace(principal.teamId, 'servers')
884
+ return response
885
+ }
886
+ } catch (error) {
887
+ // A duplicate idempotency insert rolls back every preceding effect and
888
+ // cancellation in the same batch. Replays never queue a second job.
889
+ const replay = await readReplay(context, identity)
890
+ if (replay) return replay
891
+ if (isUniqueConstraintError(error)) {
892
+ if (operation.id === 'rename_project')
893
+ throw new PublicError('PROJECT_NAME_CONFLICT')
894
+ throw new ApiError('OPERATION_CONFLICT', 409)
895
+ }
896
+ throw error
897
+ }
898
+ throw new ApiError('NOT_FOUND', 404)
899
+ }
900
+ if (!operation.workspacePolicy) throw new Error('Mutation policy is missing.')
901
+ return operation.workspacePolicy.boundary === 'dispatcher'
902
+ ? withWorkspaceAccess(
903
+ principal.teamId,
904
+ operation.workspacePolicy.operation,
905
+ apply,
906
+ {
907
+ kind: 'user',
908
+ userId: principal.userId,
909
+ roleCeiling: principal.roleCeiling,
910
+ },
911
+ )
912
+ : apply()
913
+ }