devpush 0.0.0-stage → 1.0.0-beta.4

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 +22 -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 +168 -3
  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,131 @@
1
+ import { and, eq, sql } from 'drizzle-orm'
2
+ import { db } from '#devpush/server/db'
3
+ import {
4
+ notificationPreferences,
5
+ notificationMutes,
6
+ projects,
7
+ teams,
8
+ teamMembers,
9
+ user,
10
+ } from '#devpush/server/db/schema'
11
+ import {
12
+ defaultNotificationPreferences,
13
+ notificationPreferencesSchema,
14
+ } from '#devpush/lib/notification-preferences'
15
+ import type { NotificationScope } from '#devpush/lib/notification-preferences'
16
+ import { PublicError } from '#devpush/lib/public-error'
17
+
18
+ export async function readNotificationPreferences(userId: string) {
19
+ const [row] = await db
20
+ .select()
21
+ .from(notificationPreferences)
22
+ .where(eq(notificationPreferences.userId, userId))
23
+ .limit(1)
24
+ return {
25
+ preferences: { ...defaultNotificationPreferences, ...row?.preferences },
26
+ version: row?.version ?? 0,
27
+ }
28
+ }
29
+ export async function saveNotificationPreferences(
30
+ userId: string,
31
+ preferences: unknown,
32
+ version: number,
33
+ ) {
34
+ const parsed = notificationPreferencesSchema.parse(preferences)
35
+ const [active] = await db
36
+ .select({ id: user.id })
37
+ .from(user)
38
+ .where(and(eq(user.id, userId), eq(user.status, 'active')))
39
+ .limit(1)
40
+ if (!active) throw new PublicError('UNAUTHORIZED')
41
+ const savedRows =
42
+ version === 0
43
+ ? await db
44
+ .insert(notificationPreferences)
45
+ .values({ userId, preferences: parsed, version: 1 })
46
+ .onConflictDoNothing()
47
+ .returning()
48
+ : await db
49
+ .update(notificationPreferences)
50
+ .set({
51
+ preferences: parsed,
52
+ version: sql`${notificationPreferences.version}+1`,
53
+ })
54
+ .where(
55
+ and(
56
+ eq(notificationPreferences.userId, userId),
57
+ eq(notificationPreferences.version, version),
58
+ ),
59
+ )
60
+ .returning()
61
+ const saved = savedRows[0]
62
+ if (!saved) throw new PublicError('SETTINGS_CONFLICT')
63
+ return { preferences: saved.preferences, version: saved.version }
64
+ }
65
+ async function requireNotificationScope(
66
+ userId: string,
67
+ scope: NotificationScope,
68
+ ) {
69
+ const rows =
70
+ scope.kind === 'workspace'
71
+ ? await db
72
+ .select({ teamId: teams.id, projectId: sql<string | null>`null` })
73
+ .from(teams)
74
+ .where(and(eq(teams.id, scope.id), eq(teams.status, 'active')))
75
+ .limit(1)
76
+ : await db
77
+ .select({ teamId: projects.teamId, projectId: projects.id })
78
+ .from(projects)
79
+ .innerJoin(teams, eq(teams.id, projects.teamId))
80
+ .where(
81
+ and(
82
+ eq(projects.id, scope.id),
83
+ eq(projects.status, 'active'),
84
+ eq(teams.status, 'active'),
85
+ ),
86
+ )
87
+ .limit(1)
88
+ const row = rows[0]
89
+ if (!row) throw new PublicError('PROJECT_NOT_FOUND')
90
+ const [member] = await db
91
+ .select({ id: teamMembers.id })
92
+ .from(teamMembers)
93
+ .innerJoin(user, eq(user.id, teamMembers.userId))
94
+ .where(
95
+ and(
96
+ eq(teamMembers.userId, userId),
97
+ eq(teamMembers.teamId, row.teamId),
98
+ eq(user.status, 'active'),
99
+ ),
100
+ )
101
+ .limit(1)
102
+ if (!member) throw new PublicError('PROJECT_NOT_FOUND')
103
+ return row
104
+ }
105
+ export async function readNotificationMute(
106
+ userId: string,
107
+ scope: NotificationScope,
108
+ ) {
109
+ await requireNotificationScope(userId, scope)
110
+ const [row] = await db
111
+ .select({ id: notificationMutes.id })
112
+ .from(notificationMutes)
113
+ .where(eq(notificationMutes.id, `${userId}:${scope.kind}:${scope.id}`))
114
+ .limit(1)
115
+ return { muted: Boolean(row) }
116
+ }
117
+ export async function saveNotificationMute(
118
+ userId: string,
119
+ scope: NotificationScope,
120
+ muted: boolean,
121
+ ) {
122
+ const target = await requireNotificationScope(userId, scope)
123
+ const id = `${userId}:${scope.kind}:${scope.id}`
124
+ if (muted)
125
+ await db
126
+ .insert(notificationMutes)
127
+ .values({ id, userId, ...target })
128
+ .onConflictDoNothing()
129
+ else await db.delete(notificationMutes).where(eq(notificationMutes.id, id))
130
+ return { muted }
131
+ }
@@ -0,0 +1,16 @@
1
+ import type { SQL } from 'drizzle-orm'
2
+ import { db } from '#devpush/server/db'
3
+
4
+ /** Raw Drizzle rows are positional on SQLite/libSQL and named on D1. */
5
+ export async function notificationRows<T>(
6
+ query: SQL,
7
+ columns: readonly (keyof T)[],
8
+ ): Promise<T[]> {
9
+ const rows: unknown[] = await db.all(query)
10
+ return rows.map(
11
+ (row) =>
12
+ (Array.isArray(row)
13
+ ? Object.fromEntries(columns.map((key, index) => [key, row[index]]))
14
+ : row) as T,
15
+ )
16
+ }
@@ -0,0 +1,17 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks'
2
+ import type { PluginServices } from '#devpush/plugins/contract'
3
+
4
+ const scope = new AsyncLocalStorage<{ services?: PluginServices }>()
5
+
6
+ /** Reuse providers within a request/tick, never across unrelated Worker invocations. */
7
+ export function withPluginScope<T>(work: () => T): T {
8
+ return scope.run({}, work)
9
+ }
10
+
11
+ export function scopedPluginServices(
12
+ create: () => PluginServices,
13
+ ): PluginServices {
14
+ const current = scope.getStore()
15
+ if (!current) return create()
16
+ return (current.services ??= create())
17
+ }
@@ -0,0 +1,157 @@
1
+ import { config } from '#devpush/server/config'
2
+ import { deferBackgroundTask } from '#devpush/server/runtime/background'
3
+ import { and, asc, eq, gt, sql } from 'drizzle-orm'
4
+ import { db } from '#devpush/server/db'
5
+ import { getResourceColor } from '#devpush/server/resources/color'
6
+ import { servers, teams, teamMembers, user } from '#devpush/server/db/schema'
7
+ import {
8
+ renderNotificationEmail,
9
+ sendEmail,
10
+ } from '#devpush/server/email/service'
11
+ import type { PluginContext } from '#devpush/plugins/contract'
12
+
13
+ const fields = {
14
+ id: servers.id,
15
+ teamId: servers.teamId,
16
+ teamSlug: teams.slug,
17
+ teamStatus: teams.status,
18
+ handle: servers.handle,
19
+ status: servers.status,
20
+ publicAddress: servers.publicAddress,
21
+ publicIpv4: servers.publicIpv4,
22
+ publicIpv6: servers.publicIpv6,
23
+ agentCapabilities: servers.agentCapabilities,
24
+ }
25
+
26
+ export const pluginContext: PluginContext = {
27
+ defer: deferBackgroundTask,
28
+ async prepareNotification(input) {
29
+ const [recipient] = await db
30
+ .select({ email: user.email })
31
+ .from(user)
32
+ .where(and(eq(user.id, input.userId), eq(user.status, 'active')))
33
+ .limit(1)
34
+ if (!recipient) return null
35
+ if (!config.EMAIL_FROM) throw new Error('EMAIL_FROM is not configured.')
36
+ return {
37
+ from: config.EMAIL_FROM,
38
+ to: recipient.email,
39
+ subject: input.subject,
40
+ text: input.action
41
+ ? `${input.message}\n\n${input.action.label}: ${input.action.url}`
42
+ : input.message,
43
+ html: await renderNotificationEmail({
44
+ to: recipient.email,
45
+ title: input.subject,
46
+ message: input.message,
47
+ action: input.action,
48
+ }),
49
+ }
50
+ },
51
+ async notifyUser(input) {
52
+ const prepared =
53
+ input.prepared ?? (await pluginContext.prepareNotification!(input))
54
+ if (!prepared) return false
55
+ // Never send a queued message to a removed account or an old mailbox. A
56
+ // changed address discards this recipient; it must not mutate a provider key.
57
+ const [recipient] = await db
58
+ .select({ email: user.email })
59
+ .from(user)
60
+ .where(and(eq(user.id, input.userId), eq(user.status, 'active')))
61
+ .limit(1)
62
+ if (!recipient || recipient.email !== prepared.to) return false
63
+ await sendEmail(prepared, input.idempotencyKey)
64
+ return true
65
+ },
66
+ async listWorkspaceOwners(teamId) {
67
+ const rows = await db
68
+ .select({ id: user.id })
69
+ .from(user)
70
+ .innerJoin(teamMembers, eq(teamMembers.userId, user.id))
71
+ .innerJoin(teams, eq(teams.id, teamMembers.teamId))
72
+ .where(
73
+ and(
74
+ eq(teamMembers.teamId, teamId),
75
+ eq(teamMembers.role, 'owner'),
76
+ eq(user.status, 'active'),
77
+ ),
78
+ )
79
+ return rows.map((row) => row.id)
80
+ },
81
+ async readWorkspace(teamId) {
82
+ const rows = await db
83
+ .select({
84
+ createdAt: sql<number>`${teams.createdAt}`,
85
+ personalOwnerId: sql<
86
+ string | null
87
+ >`(select u.id from user u where u.default_team_id=${teamId} and u.status='active' limit 1)`,
88
+ id: teams.id,
89
+ slug: teams.slug,
90
+ name: teams.name,
91
+ image: teams.avatarPath,
92
+ status: teams.status,
93
+ serverCount: sql<number>`(select count(*) from servers s where s.team_id=${teamId} and s.status <> 'deleted')`,
94
+ serverCountChangedAt: sql<number>`(select coalesce(max(case when s.status='deleted' then s.updated_at else s.created_at end), ${teams.createdAt}) from servers s where s.team_id=${teamId})`,
95
+ memberCount: sql<number>`(select count(*) from team_members m join user u on u.id=m.user_id where m.team_id=${teamId} and u.status='active')`,
96
+ })
97
+ .from(teams)
98
+ .where(eq(teams.id, teamId))
99
+ .limit(1)
100
+ return rows[0] ? { ...rows[0], color: getResourceColor(rows[0].id) } : null
101
+ },
102
+ async authorizeWorkspace(userId, teamSlug) {
103
+ const rows = await db
104
+ .select({
105
+ id: teams.id,
106
+ slug: teams.slug,
107
+ name: teams.name,
108
+ role: teamMembers.role,
109
+ })
110
+ .from(teams)
111
+ .innerJoin(teamMembers, eq(teamMembers.teamId, teams.id))
112
+ .innerJoin(user, eq(user.id, teamMembers.userId))
113
+ .where(
114
+ and(
115
+ eq(teams.slug, teamSlug),
116
+ eq(teamMembers.userId, userId),
117
+ eq(teams.status, 'active'),
118
+ eq(user.status, 'active'),
119
+ ),
120
+ )
121
+ .limit(1)
122
+ return rows[0] ?? null
123
+ },
124
+ async hasServerRouting(serverId) {
125
+ const rows = await db.all<{ present: number }>(sql`
126
+ select 1 as present from aliases a
127
+ join environments e on e.id = a.environment_id
128
+ join projects p on p.id = a.project_id
129
+ left join deployments d on d.id = a.deployment_id
130
+ left join deployments previous on previous.id = a.previous_deployment_id
131
+ where d.server_id = ${serverId} or previous.server_id = ${serverId}
132
+ or (a.deployment_id is null and coalesce(e.server_id,p.default_server_id) = ${serverId})
133
+ limit 1
134
+ `)
135
+ return rows.length > 0
136
+ },
137
+ async readServer(serverId) {
138
+ const rows = await db
139
+ .select(fields)
140
+ .from(servers)
141
+ .innerJoin(teams, eq(teams.id, servers.teamId))
142
+ .where(eq(servers.id, serverId))
143
+ .limit(1)
144
+ return rows[0] ?? null
145
+ },
146
+ async listServers({ afterId, limit }) {
147
+ if (!Number.isInteger(limit) || limit < 1 || limit > 100)
148
+ throw new Error('Server page size must be between 1 and 100.')
149
+ return db
150
+ .select(fields)
151
+ .from(servers)
152
+ .innerJoin(teams, eq(teams.id, servers.teamId))
153
+ .where(and(afterId ? gt(servers.id, afterId) : undefined))
154
+ .orderBy(asc(servers.id))
155
+ .limit(limit)
156
+ },
157
+ }
@@ -0,0 +1,10 @@
1
+ import { scopedPluginServices } from './scope'
2
+ import plugins from '#devpush/plugins/registered-server'
3
+ import { pluginContext } from './server-context'
4
+ import { composeServerPlugins } from '#devpush/plugins/server'
5
+
6
+ export function getPluginServices() {
7
+ return scopedPluginServices(() =>
8
+ composeServerPlugins(plugins, process.env, pluginContext),
9
+ )
10
+ }
@@ -0,0 +1,74 @@
1
+ import { getPluginServices } from './services.server'
2
+ import type {
3
+ WorkspaceOperation,
4
+ WorkspaceActor,
5
+ } from '#devpush/plugins/contract'
6
+ import { WorkspacePolicyError } from '#devpush/plugins/server'
7
+ import { PublicError } from '#devpush/lib/public-error'
8
+
9
+ declare const admissionBrand: unique symbol
10
+ export interface WorkspaceAdmission {
11
+ readonly teamId: string
12
+ readonly operation: WorkspaceOperation
13
+ readonly [admissionBrand]: true
14
+ }
15
+ const activeAdmissions = new WeakSet<object>()
16
+ export function requireWorkspaceAdmission(
17
+ admission: WorkspaceAdmission,
18
+ teamId: string,
19
+ operation: WorkspaceOperation,
20
+ ) {
21
+ if (
22
+ !activeAdmissions.has(admission) ||
23
+ admission.teamId !== teamId ||
24
+ admission.operation !== operation
25
+ )
26
+ throw new Error('Invalid workspace mutation admission.')
27
+ }
28
+
29
+ export async function withWorkspaceAccess<T>(
30
+ teamId: string,
31
+ operation: WorkspaceOperation,
32
+ action: (admission: WorkspaceAdmission) => Promise<T>,
33
+ actor: WorkspaceActor,
34
+ ): Promise<T> {
35
+ const access = getPluginServices().workspaceAccess
36
+ const invoke = async () => {
37
+ const admission = Object.freeze({ teamId, operation }) as WorkspaceAdmission
38
+ activeAdmissions.add(admission)
39
+ try {
40
+ return await action(admission)
41
+ } finally {
42
+ activeAdmissions.delete(admission)
43
+ }
44
+ }
45
+ if (!access) return invoke()
46
+ try {
47
+ return await access.run({ teamId, operation, actor }, invoke)
48
+ } catch (error) {
49
+ if (error instanceof WorkspacePolicyError) {
50
+ const codes = {
51
+ restricted: 'WORKSPACE_RESTRICTED',
52
+ limit: 'WORKSPACE_LIMIT_REACHED',
53
+ busy: 'RESOURCE_BUSY',
54
+ } as const
55
+ throw new PublicError(codes[error.reason], undefined, error.publicDetails)
56
+ }
57
+ throw error
58
+ }
59
+ }
60
+
61
+ /** No core effect is performed; the policy may still do its own accounting or I/O. */
62
+ export async function checkWorkspaceAccess(
63
+ teamId: string,
64
+ operation: WorkspaceOperation,
65
+ actor: WorkspaceActor,
66
+ ) {
67
+ return withWorkspaceAccess(teamId, operation, async () => undefined, actor)
68
+ }
69
+ export function isWorkspacePolicyRefusal(error: unknown): boolean {
70
+ return (
71
+ error instanceof PublicError &&
72
+ ['WORKSPACE_RESTRICTED', 'WORKSPACE_LIMIT_REACHED'].includes(error.code)
73
+ )
74
+ }
@@ -0,0 +1,68 @@
1
+ import { backupRetentionSchema } from '#devpush/lib/backup-policy-schema'
2
+ import type { ApiMutationAdmission } from '#devpush/server/api/admission'
3
+ import { sql } from 'drizzle-orm'
4
+ import type { AtomicStore } from '#devpush/server/db/atomic'
5
+ import { teamSettingsAccess } from '#devpush/server/settings-version'
6
+
7
+ export async function saveProjectBackupPolicyState(
8
+ store: AtomicStore,
9
+ input: {
10
+ projectId: string
11
+ teamId: string
12
+ userId: string
13
+ settingsVersion: number | null
14
+ policy: {
15
+ id: string
16
+ name: string
17
+ environmentId: string
18
+ enabled: boolean
19
+ schedule: unknown
20
+ destination: { storageId?: string }
21
+ retention: unknown
22
+ volumes: unknown
23
+ databaseDumps: unknown
24
+ }
25
+ now: Date
26
+ admission?: ApiMutationAdmission
27
+ },
28
+ ) {
29
+ const p = input.policy
30
+ const retention = backupRetentionSchema.parse(p.retention)
31
+ const access = sql`${teamSettingsAccess(input.teamId, input.userId)} and exists(select 1 from projects where id = ${input.projectId} and team_id = ${input.teamId} and status = 'active')
32
+ and exists(select 1 from environments where id = ${p.environmentId} and project_id = ${input.projectId} and status = 'active')
33
+ and (${p.destination.storageId ?? null} is null or exists(select 1 from backup_storage where id = ${p.destination.storageId ?? null} and team_id = ${input.teamId})) and (${Number(retention.remote.enabled)} = 0 or ${p.destination.storageId ?? null} is not null) and ${input.admission?.guard ?? sql`1`}`
34
+ const now = input.now.getTime()
35
+ const effect =
36
+ input.settingsVersion === null
37
+ ? store.all(sql`insert into backup_configs (id,project_id,name,environment_id,enabled,destination_type,schedule,destination,retention,volumes,database_dumps,created_at,updated_at)
38
+ select ${p.id},${input.projectId},${p.name},${p.environmentId},${Number(p.enabled)},'s3',${JSON.stringify(p.schedule)},${JSON.stringify(p.destination)},${JSON.stringify(retention)},${JSON.stringify(p.volumes)},${JSON.stringify(p.databaseDumps)},${now},${now}
39
+ where ${access} and not exists(select 1 from backup_configs where id = ${p.id}) returning id`)
40
+ : store.all(sql`update backup_configs set name = ${p.name},environment_id = ${p.environmentId},enabled = ${Number(p.enabled)},schedule = ${JSON.stringify(p.schedule)},destination = ${JSON.stringify(p.destination)},retention = ${JSON.stringify(retention)},volumes = ${JSON.stringify(p.volumes)},database_dumps = ${JSON.stringify(p.databaseDumps)},updated_at = ${now},settings_version = settings_version + 1
41
+ where id = ${p.id} and project_id = ${input.projectId} and settings_version = ${input.settingsVersion} and ${access} returning id`)
42
+ const [rows] = await store.batch([
43
+ effect,
44
+ ...(input.admission?.statements ?? []),
45
+ ])
46
+ return rows.length === 1
47
+ }
48
+
49
+ export async function deleteProjectBackupPolicyState(
50
+ store: AtomicStore,
51
+ input: {
52
+ projectId: string
53
+ teamId: string
54
+ userId: string
55
+ policyId: string
56
+ settingsVersion: number
57
+ admission?: ApiMutationAdmission
58
+ },
59
+ ) {
60
+ const effect = store.all(
61
+ sql`delete from backup_configs where id = ${input.policyId} and project_id = ${input.projectId} and settings_version = ${input.settingsVersion} and ${teamSettingsAccess(input.teamId, input.userId)} and exists(select 1 from projects where id = ${input.projectId} and status = 'active' and team_id = ${input.teamId}) and ${input.admission?.guard ?? sql`1`} returning id`,
62
+ )
63
+ const [rows] = await store.batch([
64
+ effect,
65
+ ...(input.admission?.statements ?? []),
66
+ ])
67
+ return rows.length === 1
68
+ }
@@ -0,0 +1,219 @@
1
+ import { withWorkspaceAccess } from '#devpush/server/plugins/workspace-access'
2
+ import { requireBackupsEnabled } from '#devpush/server/backups/feature'
3
+ import { getProjectSettingsContext } from './settings-context'
4
+ import { PublicError } from '#devpush/lib/public-error'
5
+ import { prepareBackupPolicy } from '#devpush/server/backups/policy-validation'
6
+
7
+ import { canManageTeamSettings } from '#devpush/lib/team-role'
8
+ import type { TeamRole } from '#devpush/lib/team-role'
9
+ import { db } from '#devpush/server/db'
10
+ import {
11
+ saveProjectBackupPolicyState,
12
+ deleteProjectBackupPolicyState,
13
+ } from './backup-policy-state'
14
+ import {
15
+ queueBackupRestoreJob,
16
+ queueEnvironmentBackup,
17
+ } from '#devpush/server/backups/service'
18
+ import { getProjectContext } from './context'
19
+ import type { BackupDestinationInput, ProjectBackupConfig } from './types'
20
+
21
+ export async function saveProjectBackupPolicy({
22
+ projectId,
23
+ userId,
24
+ teamSlug,
25
+ projectName,
26
+ policy,
27
+ settingsVersion,
28
+ }: {
29
+ projectId?: string
30
+ userId: string
31
+ teamSlug: string
32
+ projectName: string
33
+ settingsVersion: number | null
34
+ policy: Omit<ProjectBackupConfig, 'settingsVersion' | 'destination'> & {
35
+ destination: BackupDestinationInput
36
+ }
37
+ }) {
38
+ requireBackupsEnabled()
39
+ const context = await getProjectSettingsContext({
40
+ projectId,
41
+ reloadSection: 'backups',
42
+ userId,
43
+ teamSlug,
44
+ projectName,
45
+ scope: 'shell',
46
+ })
47
+ return withWorkspaceAccess(
48
+ context.team.id,
49
+ 'backup.policy.save',
50
+ async () => {
51
+ requireBackupManagement(context.team.role)
52
+ const normalized = await prepareBackupPolicy({
53
+ teamId: context.team.id,
54
+ projectId: context.project.id,
55
+ policy,
56
+ })
57
+ const changed = await saveProjectBackupPolicyState(db, {
58
+ projectId: context.project.id,
59
+ teamId: context.team.id,
60
+ userId,
61
+ settingsVersion,
62
+ policy: normalized,
63
+ now: new Date(),
64
+ })
65
+ if (!changed) throw new PublicError('SETTINGS_CONFLICT')
66
+ return getProjectContext({
67
+ userId,
68
+ teamSlug,
69
+ projectName,
70
+ scope: 'backups',
71
+ })
72
+ },
73
+ { kind: 'user', userId },
74
+ )
75
+ }
76
+
77
+ export async function deleteProjectBackupPolicy({
78
+ projectId,
79
+ userId,
80
+ teamSlug,
81
+ projectName,
82
+ policyId,
83
+ settingsVersion,
84
+ }: {
85
+ projectId?: string
86
+ userId: string
87
+ teamSlug: string
88
+ projectName: string
89
+ policyId: string
90
+ settingsVersion: number
91
+ }) {
92
+ requireBackupsEnabled()
93
+ const context = await getProjectSettingsContext({
94
+ projectId,
95
+ reloadSection: 'backups',
96
+ userId,
97
+ teamSlug,
98
+ projectName,
99
+ scope: 'shell',
100
+ })
101
+ return withWorkspaceAccess(
102
+ context.team.id,
103
+ 'backup.policy.delete',
104
+ async () => {
105
+ requireBackupManagement(context.team.role)
106
+ if (
107
+ !(await deleteProjectBackupPolicyState(db, {
108
+ projectId: context.project.id,
109
+ teamId: context.team.id,
110
+ userId,
111
+ policyId,
112
+ settingsVersion,
113
+ }))
114
+ )
115
+ throw new PublicError('SETTINGS_CONFLICT')
116
+ return getProjectContext({
117
+ userId,
118
+ teamSlug,
119
+ projectName,
120
+ scope: 'backups',
121
+ })
122
+ },
123
+ { kind: 'user', userId },
124
+ )
125
+ }
126
+
127
+ export async function createManualBackupRun({
128
+ backupConfigId,
129
+ userId,
130
+ teamSlug,
131
+ projectName,
132
+ }: {
133
+ backupConfigId: string
134
+ userId: string
135
+ teamSlug: string
136
+ projectName: string
137
+ }) {
138
+ requireBackupsEnabled()
139
+ const context = await getProjectContext({
140
+ userId,
141
+ teamSlug,
142
+ projectName,
143
+ scope: 'backups',
144
+ })
145
+ const backupConfig = context.backupConfigs.find(
146
+ (candidate) => candidate.id === backupConfigId,
147
+ )
148
+
149
+ if (!backupConfig) {
150
+ throw new PublicError('BACKUP_POLICY_NOT_FOUND')
151
+ }
152
+
153
+ requireBackupManagement(context.team.role)
154
+ const queued = await queueEnvironmentBackup({
155
+ actor: { kind: 'user', userId: userId },
156
+ backupConfigId,
157
+ projectId: context.project.id,
158
+ environmentId: backupConfig.environmentId,
159
+ manifest: {
160
+ trigger: 'manual',
161
+ backupConfigId,
162
+ requestedAt: new Date().toISOString(),
163
+ },
164
+ })
165
+ if (!queued) {
166
+ throw new PublicError('BACKUP_ALREADY_ACTIVE')
167
+ }
168
+
169
+ return getProjectContext({ userId, teamSlug, projectName, scope: 'backups' })
170
+ }
171
+
172
+ export async function createBackupRestoreJob({
173
+ userId,
174
+ teamSlug,
175
+ projectName,
176
+ backupRunId,
177
+ artifacts,
178
+ }: {
179
+ userId: string
180
+ teamSlug: string
181
+ projectName: string
182
+ backupRunId: string
183
+ artifacts: Array<
184
+ | {
185
+ artifactId: string
186
+ type: 'volume'
187
+ targetVolume: string
188
+ }
189
+ | {
190
+ artifactId: string
191
+ type: 'database'
192
+ targetService: string
193
+ }
194
+ >
195
+ }) {
196
+ requireBackupsEnabled()
197
+ const context = await getProjectContext({
198
+ userId,
199
+ teamSlug,
200
+ projectName,
201
+ scope: 'shell',
202
+ })
203
+ requireBackupManagement(context.team.role)
204
+
205
+ await queueBackupRestoreJob({
206
+ actor: { kind: 'user', userId: userId },
207
+ projectId: context.project.id,
208
+ backupRunId,
209
+ artifacts,
210
+ })
211
+
212
+ return getProjectContext({ userId, teamSlug, projectName, scope: 'backups' })
213
+ }
214
+
215
+ function requireBackupManagement(role: TeamRole) {
216
+ if (!canManageTeamSettings(role)) {
217
+ throw new PublicError('BACKUP_MANAGEMENT_FORBIDDEN')
218
+ }
219
+ }