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,918 @@
1
+ import { safeErrorDiagnostics } from '#devpush/server/http/error-diagnostics'
2
+ import {
3
+ withWorkspaceAccess,
4
+ isWorkspacePolicyRefusal,
5
+ } from '#devpush/server/plugins/workspace-access'
6
+ import type { WorkspaceActor } from '#devpush/plugins/operations'
7
+ import { teamSettingsAccess } from '#devpush/server/settings-version'
8
+ import { backupDestinationSnapshot } from './destination'
9
+ import { requireBackupsEnabled } from '#devpush/server/backups/feature'
10
+ import { backupRestoreArtifactSchema as restoreArtifactSchema } from '#devpush/lib/backup-restore-schema'
11
+ import type { backupVolumesSchema } from '#devpush/lib/backup-policy-schema'
12
+ import {
13
+ normalizeBackupVolumes,
14
+ backupRetentionSchema,
15
+ backupScheduleSchema,
16
+ } from '#devpush/lib/backup-policy-schema'
17
+ import type { ApiMutationAdmission } from '#devpush/server/api/admission'
18
+ import { PublicError } from '#devpush/lib/public-error'
19
+ import {
20
+ backupDatabaseSchema,
21
+ databaseArtifactTarget,
22
+ } from '#devpush/lib/backup-database'
23
+ import type { BackupDatabase } from '#devpush/lib/backup-database'
24
+ import {
25
+ buildDatabaseBackupCommand,
26
+ buildDatabaseRestoreCommand,
27
+ } from './database-commands'
28
+ import { backupStorageReferenceSchema } from '#devpush/lib/backup-storage-schema'
29
+ import { resolveBackupDestination, refreshBackupCredentials } from './storage'
30
+ import {
31
+ enqueueBackup,
32
+ enqueueBackupCleanup,
33
+ backupCleanupRetryDelayMs,
34
+ } from './queue-state'
35
+ import { randomUUID } from 'node:crypto'
36
+
37
+ import {
38
+ and,
39
+ desc,
40
+ eq,
41
+ inArray,
42
+ isNotNull,
43
+ isNull,
44
+ lte,
45
+ sql,
46
+ } from 'drizzle-orm'
47
+ import { z } from 'zod'
48
+
49
+ import { db } from '#devpush/server/db'
50
+ import {
51
+ agentJobs,
52
+ backupArtifacts,
53
+ backupConfigs,
54
+ backupRuns,
55
+ environments,
56
+ projects,
57
+ servers,
58
+ } from '#devpush/server/db/schema'
59
+ import { isCronDue } from '#devpush/lib/backup-cron'
60
+
61
+ const s3DestinationSchema = z.object({
62
+ storageId: z.string().optional(),
63
+ bucket: z
64
+ .string()
65
+ .trim()
66
+ .min(3)
67
+ .max(63)
68
+ .regex(
69
+ /^[a-z0-9][a-z0-9.-]*[a-z0-9]$/,
70
+ 'S3 bucket names must use lowercase DNS-compatible characters.',
71
+ ),
72
+ region: z.string().trim().min(1).max(100).optional(),
73
+ endpoint: z.string().trim().url().optional(),
74
+ prefix: z.string().trim().max(512).optional(),
75
+ accessKeyId: z.string().trim().min(1).max(256).optional(),
76
+ secretAccessKeyCiphertext: z.string().trim().min(1).optional(),
77
+ secretAccessKeyPreview: z.string().trim().min(1).max(64).optional(),
78
+ })
79
+
80
+ const databaseDumpSchema = backupDatabaseSchema
81
+
82
+ export type BackupDestination = z.infer<typeof s3DestinationSchema>
83
+ type BackupRetentionLimit = z.infer<typeof backupRetentionSchema>['local']
84
+ export type BackupRetentionPolicy = z.infer<typeof backupRetentionSchema>
85
+ export type BackupDatabaseDump = BackupDatabase
86
+ export type BackupVolumeSelection = z.infer<typeof backupVolumesSchema>
87
+ export type BackupRestoreArtifactInput = z.infer<typeof restoreArtifactSchema>
88
+
89
+ export type BackupJobPayload = {
90
+ schemaVersion: 1
91
+ backupRunId: string
92
+ artifactPrefix: string
93
+ localBasePath: string
94
+ projectId: string
95
+ environmentId: string
96
+ runtimeId: string
97
+ destination: BackupDestination & {
98
+ secretAccessKey?: string
99
+ }
100
+ retention: BackupRetentionPolicy
101
+ volumes: BackupVolumeSelection
102
+ databaseDumps: Array<{ service: string; command: string; format: string }>
103
+ }
104
+
105
+ export type BackupCleanupJobPayload = {
106
+ schemaVersion: 1
107
+ backupRunIds: Array<string>
108
+ destination?: BackupDestination
109
+ localObjectKeys: Array<string>
110
+ remoteObjectKeys: Array<string>
111
+ reason: string
112
+ }
113
+
114
+ export type BackupRestoreJobPayload = {
115
+ schemaVersion: 1
116
+ backupRunId: string
117
+ projectId: string
118
+ environmentId: string
119
+ runtimeId: string
120
+ destination?: BackupDestination & {
121
+ secretAccessKey?: string
122
+ }
123
+ artifacts: Array<
124
+ | {
125
+ id: string
126
+ type: 'volume'
127
+ storage: 'local' | 'remote'
128
+ objectKey: string
129
+ checksumSha256: string
130
+ targetVolume: string
131
+ }
132
+ | {
133
+ id: string
134
+ type: 'database'
135
+ storage: 'local' | 'remote'
136
+ objectKey: string
137
+ checksumSha256: string
138
+ targetService: string
139
+ restoreCommand: string
140
+ }
141
+ >
142
+ }
143
+
144
+ export async function queueEnvironmentBackup({
145
+ projectId,
146
+ environmentId,
147
+ deploymentId,
148
+ manifest,
149
+ scheduleKey,
150
+ backupConfigId,
151
+ admission,
152
+ actor,
153
+ }: {
154
+ projectId: string
155
+ environmentId: string
156
+ deploymentId?: string | null
157
+ manifest?: unknown
158
+ scheduleKey?: string
159
+ backupConfigId?: string
160
+ admission?: ApiMutationAdmission
161
+ actor: WorkspaceActor
162
+ }) {
163
+ requireBackupsEnabled()
164
+ const rows = await db
165
+ .select({
166
+ config: backupConfigs,
167
+ serverCapabilities: servers.agentCapabilities,
168
+ environment: environments,
169
+ project: projects,
170
+ })
171
+ .from(backupConfigs)
172
+ .innerJoin(environments, eq(environments.id, backupConfigs.environmentId))
173
+ .innerJoin(projects, eq(projects.id, environments.projectId))
174
+ .innerJoin(
175
+ servers,
176
+ eq(
177
+ servers.id,
178
+ sql<string>`coalesce(${environments.serverId}, ${projects.defaultServerId})`,
179
+ ),
180
+ )
181
+ .where(
182
+ and(
183
+ eq(projects.id, projectId),
184
+ eq(projects.status, 'active'),
185
+ eq(environments.id, environmentId),
186
+ eq(environments.status, 'active'),
187
+ eq(backupConfigs.enabled, true),
188
+ backupConfigId ? eq(backupConfigs.id, backupConfigId) : undefined,
189
+ inArray(servers.status, ['connected', 'disconnected']),
190
+ ),
191
+ )
192
+ .limit(2)
193
+ if (!backupConfigId && rows.length > 1) {
194
+ throw new PublicError('BACKUP_POLICY_REQUIRED')
195
+ }
196
+ const row = rows.at(0)
197
+
198
+ if (!row) {
199
+ throw new PublicError('BACKUP_ENABLED_CONFIG_NOT_FOUND')
200
+ }
201
+ return withWorkspaceAccess(
202
+ row.project.teamId,
203
+ 'backup.run',
204
+ async () => {
205
+ admission = {
206
+ ...admission,
207
+ resourceId: admission?.resourceId ?? randomUUID(),
208
+ statements: admission?.statements ?? [],
209
+ guard: sql`${admission?.guard ?? sql`1`} and ${actor.kind === 'user' ? teamSettingsAccess(row.project.teamId, actor.userId) : sql`1`}`,
210
+ }
211
+
212
+ const serverId = row.environment.serverId ?? row.project.defaultServerId
213
+
214
+ if (!serverId) {
215
+ throw new PublicError('BACKUP_SERVER_REQUIRED')
216
+ }
217
+ const reference = backupStorageReferenceSchema.parse(
218
+ row.config.destination,
219
+ )
220
+ const retention = backupRetentionSchema.parse(row.config.retention)
221
+ const destination = retention.remote.enabled
222
+ ? normalizeS3Destination(
223
+ await resolveBackupDestination(row.project.teamId, reference),
224
+ )
225
+ : { bucket: 'local-backups', prefix: reference.prefix }
226
+ const volumes = normalizeBackupVolumes(row.config.volumes)
227
+ const databaseDumps = z
228
+ .array(databaseDumpSchema)
229
+ .parse(row.config.databaseDumps)
230
+ if (
231
+ databaseDumps.some((target) => target.engine === 'mongo') &&
232
+ !(row.serverCapabilities ?? []).includes('jobs.backup.archive.v1')
233
+ ) {
234
+ throw new PublicError('BACKUP_MONGODB_AGENT_UPDATE_REQUIRED')
235
+ }
236
+ const now = new Date()
237
+ const backupRunId = admission.resourceId
238
+ const agentJobId = admission.secondaryResourceId ?? randomUUID()
239
+ const artifactPrefix = buildBackupArtifactPrefix({
240
+ destination,
241
+ projectId,
242
+ environmentId,
243
+ backupRunId,
244
+ })
245
+ const localBasePath = buildBackupLocalBasePath({
246
+ projectId,
247
+ environmentId,
248
+ backupRunId,
249
+ })
250
+
251
+ const payload = {
252
+ schemaVersion: 1,
253
+ backupRunId,
254
+ artifactPrefix,
255
+ localBasePath,
256
+ projectId,
257
+ environmentId,
258
+ runtimeId: row.environment.runtimeId,
259
+ destination: buildBackupJobDestination({ destination, retention }),
260
+ retention,
261
+ volumes,
262
+ databaseDumps: databaseDumps.map((target) => ({
263
+ service: target.service,
264
+ command: buildDatabaseBackupCommand(target),
265
+ format: target.engine === 'mongo' ? 'archive' : 'sql',
266
+ })),
267
+ } satisfies BackupJobPayload
268
+
269
+ const created = await enqueueBackup(db, {
270
+ backupRunId,
271
+ agentJobId,
272
+ configId: row.config.id,
273
+ projectId,
274
+ environmentId,
275
+ deploymentId: deploymentId ?? null,
276
+ serverId,
277
+ runtimeId: row.environment.runtimeId,
278
+ scheduleKey: scheduleKey ?? null,
279
+ manifest: {
280
+ ...(typeof manifest === 'object' && manifest !== null
281
+ ? manifest
282
+ : {}),
283
+ databaseTargets: databaseDumps,
284
+ },
285
+ payload,
286
+ now,
287
+ admission,
288
+ })
289
+
290
+ if (!created) {
291
+ return null
292
+ }
293
+
294
+ return {
295
+ backupRunId,
296
+ agentJobId,
297
+ payload,
298
+ }
299
+ },
300
+ actor,
301
+ )
302
+ }
303
+
304
+ function buildBackupJobDestination({
305
+ destination,
306
+ retention,
307
+ }: {
308
+ destination: BackupDestination
309
+ retention: BackupRetentionPolicy
310
+ }): BackupJobPayload['destination'] {
311
+ if (!retention.remote.enabled) {
312
+ return backupDestinationSnapshot(destination)
313
+ }
314
+
315
+ if (!destination.accessKeyId || !destination.secretAccessKeyCiphertext) {
316
+ throw new PublicError('BACKUP_CREDENTIALS_MISSING')
317
+ }
318
+
319
+ return {
320
+ ...backupDestinationSnapshot(destination),
321
+ }
322
+ }
323
+
324
+ export async function queueBackupRestoreJob({
325
+ projectId,
326
+ backupRunId,
327
+ artifacts,
328
+ admission,
329
+ actor,
330
+ }: {
331
+ projectId: string
332
+ backupRunId: string
333
+ artifacts: Array<BackupRestoreArtifactInput>
334
+ admission?: ApiMutationAdmission
335
+ actor: WorkspaceActor
336
+ }) {
337
+ requireBackupsEnabled()
338
+ const requestedArtifacts = artifacts.map((artifact) =>
339
+ restoreArtifactSchema.parse(artifact),
340
+ )
341
+
342
+ if (requestedArtifacts.length === 0) {
343
+ throw new PublicError('BACKUP_RESTORE_ARTIFACT_REQUIRED')
344
+ }
345
+
346
+ const runRows = await db
347
+ .select({
348
+ run: backupRuns,
349
+ environment: environments,
350
+ config: backupConfigs,
351
+ project: projects,
352
+ })
353
+ .from(backupRuns)
354
+ .innerJoin(environments, eq(environments.id, backupRuns.environmentId))
355
+ .innerJoin(projects, eq(projects.id, backupRuns.projectId))
356
+ .leftJoin(
357
+ backupConfigs,
358
+ and(
359
+ eq(backupConfigs.projectId, backupRuns.projectId),
360
+ eq(backupConfigs.environmentId, backupRuns.environmentId),
361
+ ),
362
+ )
363
+ .where(
364
+ and(eq(backupRuns.id, backupRunId), eq(backupRuns.projectId, projectId)),
365
+ )
366
+ .limit(1)
367
+ const row = runRows.at(0)
368
+
369
+ if (!row) {
370
+ throw new PublicError('BACKUP_RUN_NOT_FOUND')
371
+ }
372
+ return withWorkspaceAccess(
373
+ row.project.teamId,
374
+ 'backup.restore',
375
+ async () => {
376
+ admission = {
377
+ ...admission,
378
+ resourceId: admission?.resourceId ?? randomUUID(),
379
+ statements: admission?.statements ?? [],
380
+ guard: sql`${admission?.guard ?? sql`1`} and ${actor.kind === 'user' ? teamSettingsAccess(row.project.teamId, actor.userId) : sql`1`}`,
381
+ }
382
+
383
+ const localRecovery =
384
+ row.run.status === 'failed' &&
385
+ row.run.manifest !== null &&
386
+ typeof row.run.manifest === 'object' &&
387
+ 'localRecoveryAvailable' in row.run.manifest &&
388
+ row.run.manifest.localRecoveryAvailable === true
389
+ if (row.run.status !== 'succeeded' && !localRecovery) {
390
+ throw new PublicError('BACKUP_NO_RECOVERABLE_ARTIFACTS')
391
+ }
392
+
393
+ const effectiveServerId =
394
+ row.environment.serverId ?? row.project.defaultServerId
395
+
396
+ if (!row.run.serverId || row.run.serverId !== effectiveServerId) {
397
+ throw new PublicError('BACKUP_SERVER_CHANGED')
398
+ }
399
+
400
+ const artifactRows = await db
401
+ .select()
402
+ .from(backupArtifacts)
403
+ .where(
404
+ and(
405
+ eq(backupArtifacts.backupRunId, backupRunId),
406
+ sql`${backupArtifacts.id} in (select value from json_each(${JSON.stringify(requestedArtifacts.map((artifact) => artifact.artifactId))}))`,
407
+ ),
408
+ )
409
+
410
+ if (
411
+ localRecovery &&
412
+ artifactRows.some((artifact) => artifact.storage !== 'local')
413
+ )
414
+ throw new PublicError('BACKUP_FAILED_REMOTE_RESTORE')
415
+
416
+ if (artifactRows.length !== requestedArtifacts.length) {
417
+ throw new PublicError('BACKUP_ARTIFACT_NOT_FOUND')
418
+ }
419
+
420
+ const requestedByArtifactId = new Map(
421
+ requestedArtifacts.map((artifact) => [artifact.artifactId, artifact]),
422
+ )
423
+ const restoreArtifacts: BackupRestoreJobPayload['artifacts'] = []
424
+ let needsRemoteDestination = false
425
+
426
+ for (const artifact of artifactRows) {
427
+ const requested = requestedByArtifactId.get(artifact.id)
428
+
429
+ if (!requested) {
430
+ continue
431
+ }
432
+
433
+ if (artifact.type !== requested.type) {
434
+ throw new PublicError('BACKUP_ARTIFACT_TYPE_MISMATCH')
435
+ }
436
+
437
+ if (artifact.storage === 'remote') {
438
+ needsRemoteDestination = true
439
+ }
440
+
441
+ if (requested.type === 'volume') {
442
+ restoreArtifacts.push({
443
+ id: artifact.id,
444
+ type: 'volume',
445
+ storage: artifact.storage,
446
+ objectKey: artifact.objectKey,
447
+ checksumSha256: artifact.checksumSha256,
448
+ targetVolume: requested.targetVolume,
449
+ })
450
+ } else {
451
+ restoreArtifacts.push({
452
+ id: artifact.id,
453
+ type: 'database',
454
+ storage: artifact.storage,
455
+ objectKey: artifact.objectKey,
456
+ checksumSha256: artifact.checksumSha256,
457
+ targetService: requested.targetService,
458
+ restoreCommand: buildDatabaseRestoreCommand(
459
+ requireDatabaseArtifactTarget(
460
+ artifact.objectKey,
461
+ row.run.manifest,
462
+ ),
463
+ ),
464
+ })
465
+ }
466
+ }
467
+
468
+ const snapshot = row.run.destination
469
+ ? s3DestinationSchema.parse(row.run.destination)
470
+ : null
471
+ const destination =
472
+ needsRemoteDestination && snapshot
473
+ ? buildRestoreJobDestination(
474
+ await refreshBackupCredentials(row.project.teamId, snapshot),
475
+ )
476
+ : undefined
477
+ if (needsRemoteDestination && !destination)
478
+ throw new PublicError('BACKUP_DESTINATION_UNAVAILABLE')
479
+
480
+ const now = new Date()
481
+ const jobId = admission.resourceId
482
+ const payload = {
483
+ schemaVersion: 1,
484
+ backupRunId,
485
+ projectId,
486
+ environmentId: row.run.environmentId,
487
+ runtimeId: row.run.runtimeId,
488
+ destination,
489
+ artifacts: restoreArtifacts,
490
+ } satisfies BackupRestoreJobPayload
491
+
492
+ const [inserted] = await db.batch([
493
+ db.all(sql`insert into ${agentJobs}
494
+ (id,type,status,server_id,team_id,project_id,payload,max_attempts,available_at,created_at,updated_at)
495
+ select ${jobId},'restore','queued',${row.run.serverId},null,${projectId},${JSON.stringify(payload)},1,${now.getTime()},${now.getTime()},${now.getTime()}
496
+ where exists(select 1 from ${agentJobs} where id = ${jobId}) or (
497
+ ${admission.guard}
498
+ and exists(select 1 from ${backupRuns} as run
499
+ join ${environments} as env on env.id = run.environment_id
500
+ join ${projects} as project on project.id = env.project_id
501
+ join ${servers} as server on server.id = coalesce(env.server_id,project.default_server_id)
502
+ where run.id = ${backupRunId} and run.project_id = ${projectId}
503
+ and project.status = 'active' and env.status = 'active'
504
+ and server.id = run.server_id and server.status in ('connected','disconnected')
505
+ and run.status = ${row.run.status})
506
+ and not exists(select 1 from ${agentJobs} where type = 'restore'
507
+ and status in ('queued','running') and project_id = ${projectId}
508
+ and json_extract(payload,'$.runtimeId') = ${row.run.runtimeId})
509
+ ) returning id`),
510
+ ...admission.statements,
511
+ ])
512
+ if (inserted.length !== 1) throw new PublicError('BACKUP_ALREADY_ACTIVE')
513
+
514
+ return {
515
+ agentJobId: jobId,
516
+ payload,
517
+ }
518
+ },
519
+ actor,
520
+ )
521
+ }
522
+
523
+ function buildRestoreJobDestination(
524
+ destination: BackupDestination,
525
+ ): BackupRestoreJobPayload['destination'] {
526
+ if (!destination.accessKeyId || !destination.secretAccessKeyCiphertext) {
527
+ throw new PublicError('BACKUP_RESTORE_CREDENTIALS_MISSING')
528
+ }
529
+
530
+ return {
531
+ ...backupDestinationSnapshot(destination),
532
+ }
533
+ }
534
+
535
+ export async function queueBackupRetentionCleanupJobs({
536
+ projectId,
537
+ environmentId,
538
+ now = new Date(),
539
+ }: {
540
+ projectId?: string
541
+ environmentId?: string
542
+ now?: Date
543
+ } = {}) {
544
+ const configRows = await db
545
+ .select()
546
+ .from(backupConfigs)
547
+ .where(
548
+ projectId && environmentId
549
+ ? and(
550
+ eq(backupConfigs.projectId, projectId),
551
+ eq(backupConfigs.environmentId, environmentId),
552
+ )
553
+ : projectId
554
+ ? eq(backupConfigs.projectId, projectId)
555
+ : environmentId
556
+ ? eq(backupConfigs.environmentId, environmentId)
557
+ : undefined,
558
+ )
559
+
560
+ if (configRows.length === 0) {
561
+ return []
562
+ }
563
+
564
+ const queuedJobs: Array<{ id: string; backupRunIds: Array<string> }> = []
565
+
566
+ for (const config of configRows) {
567
+ const parsedRetention = backupRetentionSchema.safeParse(config.retention)
568
+ if (!parsedRetention.success) {
569
+ console.error('Skipping backup retention policy with invalid settings.', {
570
+ policyId: config.id,
571
+ })
572
+ continue
573
+ }
574
+ const retention = parsedRetention.data
575
+
576
+ const runRows = await db
577
+ .select()
578
+ .from(backupRuns)
579
+ .where(
580
+ and(
581
+ sql`exists(select 1 from projects where id = ${backupRuns.projectId} and status = 'active')`,
582
+ sql`(${backupRuns.cleanupErrorMessage} is null or ${backupRuns.updatedAt} <= ${now.getTime() - backupCleanupRetryDelayMs})`,
583
+ sql`exists(select 1 from servers where id = ${backupRuns.serverId} and status <> 'deleted')`,
584
+ eq(backupRuns.environmentId, config.environmentId),
585
+ sql`(${backupRuns.configId} = ${config.id} or (${backupRuns.configId} is null and (select count(*) from ${backupConfigs} where environment_id = ${config.environmentId}) = 1))`,
586
+ sql`(${backupRuns.status} = 'succeeded' or (${backupRuns.status} = 'failed' and json_extract(${backupRuns.manifest}, '$.localRecoveryAvailable') = 1))`,
587
+ isNotNull(backupRuns.finishedAt),
588
+ isNull(backupRuns.cleanupQueuedAt),
589
+ ),
590
+ )
591
+ .orderBy(desc(backupRuns.finishedAt), desc(backupRuns.createdAt))
592
+
593
+ if (runRows.length === 0) {
594
+ continue
595
+ }
596
+
597
+ const allArtifacts: Array<
598
+ Pick<
599
+ typeof backupArtifacts.$inferSelect,
600
+ 'backupRunId' | 'storage' | 'objectKey' | 'sizeBytes'
601
+ >
602
+ > = []
603
+ for (let offset = 0; offset < runRows.length; offset += 80) {
604
+ allArtifacts.push(
605
+ ...(await db
606
+ .select({
607
+ backupRunId: backupArtifacts.backupRunId,
608
+ storage: backupArtifacts.storage,
609
+ objectKey: backupArtifacts.objectKey,
610
+ sizeBytes: backupArtifacts.sizeBytes,
611
+ })
612
+ .from(backupArtifacts)
613
+ .where(
614
+ inArray(
615
+ backupArtifacts.backupRunId,
616
+ runRows.slice(offset, offset + 80).map((run) => run.id),
617
+ ),
618
+ )),
619
+ )
620
+ }
621
+ const expiredStorageByRunId = new Map<string, Set<'local' | 'remote'>>()
622
+
623
+ for (const storage of ['local', 'remote'] as const) {
624
+ const sizeByRunId = new Map<string, number>()
625
+ const storageArtifacts = allArtifacts.filter(
626
+ (artifact) => artifact.storage === storage,
627
+ )
628
+
629
+ if (storageArtifacts.length === 0) {
630
+ continue
631
+ }
632
+
633
+ if (storage === 'remote' && !retention.remote.enabled) {
634
+ continue
635
+ }
636
+
637
+ for (const artifact of storageArtifacts) {
638
+ sizeByRunId.set(
639
+ artifact.backupRunId,
640
+ (sizeByRunId.get(artifact.backupRunId) ?? 0) +
641
+ Number(artifact.sizeBytes),
642
+ )
643
+ }
644
+ const storageExpiredRunIds = selectExpiredBackupRunIds({
645
+ runs: runRows
646
+ .filter((run) => sizeByRunId.has(run.id))
647
+ .map((run) => ({
648
+ id: run.id,
649
+ finishedAt: run.finishedAt ?? run.createdAt,
650
+ sizeBytes: sizeByRunId.get(run.id) ?? 0,
651
+ })),
652
+ retention: retention[storage],
653
+ now,
654
+ })
655
+
656
+ if (storageExpiredRunIds.length === 0) {
657
+ continue
658
+ }
659
+
660
+ for (const runId of storageExpiredRunIds) {
661
+ const expiredStorage = expiredStorageByRunId.get(runId) ?? new Set()
662
+ expiredStorage.add(storage)
663
+ expiredStorageByRunId.set(runId, expiredStorage)
664
+ }
665
+ }
666
+
667
+ if (expiredStorageByRunId.size === 0) {
668
+ continue
669
+ }
670
+
671
+ const runIdsByServer = new Map<string, Array<string>>()
672
+ for (const run of runRows) {
673
+ if (!run.serverId || !expiredStorageByRunId.has(run.id)) continue
674
+ const key = JSON.stringify([run.serverId, run.destination])
675
+ const runIds = runIdsByServer.get(key) ?? []
676
+ runIds.push(run.id)
677
+ runIdsByServer.set(key, runIds)
678
+ }
679
+
680
+ for (const [groupKey, backupRunIdsToClean] of runIdsByServer) {
681
+ const [serverId, snapshot] = JSON.parse(groupKey) as [
682
+ string,
683
+ BackupDestination | null,
684
+ ]
685
+ const hasExpiredRemoteArtifacts = allArtifacts.some(
686
+ (artifact) =>
687
+ backupRunIdsToClean.includes(artifact.backupRunId) &&
688
+ artifact.storage === 'remote' &&
689
+ expiredStorageByRunId.get(artifact.backupRunId)?.has('remote'),
690
+ )
691
+ // Only a registered destination can safely identify credentials for old files.
692
+ // Resolve them at claim time; one unsupported snapshot must not stop a sweep.
693
+ const parsedSnapshot = s3DestinationSchema.safeParse(snapshot)
694
+ if (
695
+ hasExpiredRemoteArtifacts &&
696
+ (!parsedSnapshot.success || !parsedSnapshot.data.storageId)
697
+ )
698
+ continue
699
+ const destination =
700
+ hasExpiredRemoteArtifacts && parsedSnapshot.success
701
+ ? backupDestinationSnapshot(parsedSnapshot.data)
702
+ : { bucket: 'local-backups' }
703
+ for (let offset = 0; offset < backupRunIdsToClean.length; offset += 50) {
704
+ const backupRunIdChunk = backupRunIdsToClean.slice(offset, offset + 50)
705
+ const runIds = new Set(backupRunIdChunk)
706
+ const artifacts = allArtifacts.filter(
707
+ (artifact) =>
708
+ runIds.has(artifact.backupRunId) &&
709
+ expiredStorageByRunId
710
+ .get(artifact.backupRunId)
711
+ ?.has(artifact.storage),
712
+ )
713
+ const jobId = randomUUID()
714
+ const backupRunIds = await enqueueBackupCleanup(db, {
715
+ jobId,
716
+ projectId: config.projectId,
717
+ serverId,
718
+ backupRunIds: backupRunIdChunk,
719
+ artifacts,
720
+ destination,
721
+ now,
722
+ })
723
+ if (backupRunIds.length > 0)
724
+ queuedJobs.push({ id: jobId, backupRunIds })
725
+ }
726
+ }
727
+ }
728
+
729
+ return queuedJobs
730
+ }
731
+
732
+ export async function queueDueBackupSchedules({ now = new Date() } = {}) {
733
+ const configRows = await db
734
+ .select({
735
+ config: backupConfigs,
736
+ environment: environments,
737
+ project: projects,
738
+ })
739
+ .from(backupConfigs)
740
+ .innerJoin(environments, eq(environments.id, backupConfigs.environmentId))
741
+ .innerJoin(projects, eq(projects.id, backupConfigs.projectId))
742
+ .innerJoin(
743
+ servers,
744
+ eq(
745
+ servers.id,
746
+ sql<string>`coalesce(${environments.serverId}, ${projects.defaultServerId})`,
747
+ ),
748
+ )
749
+ .where(
750
+ and(
751
+ eq(backupConfigs.enabled, true),
752
+ eq(projects.status, 'active'),
753
+ eq(environments.status, 'active'),
754
+ inArray(servers.status, ['connected', 'disconnected']),
755
+ lte(backupConfigs.createdAt, now),
756
+ ),
757
+ )
758
+
759
+ const queued: Array<{ backupRunId: string; agentJobId: string }> = []
760
+
761
+ for (const row of configRows) {
762
+ try {
763
+ const schedule = backupScheduleSchema.parse(row.config.schedule)
764
+
765
+ if (!isCronDue(schedule.cron, now)) {
766
+ continue
767
+ }
768
+
769
+ if (!(row.environment.serverId ?? row.project.defaultServerId)) {
770
+ continue
771
+ }
772
+
773
+ const queuedJob = await queueEnvironmentBackup({
774
+ actor: { kind: 'system', source: 'backup.schedule' },
775
+ projectId: row.config.projectId,
776
+ environmentId: row.config.environmentId,
777
+ backupConfigId: row.config.id,
778
+ manifest: {
779
+ trigger: 'schedule',
780
+ schedule,
781
+ scheduledAt: now.toISOString(),
782
+ },
783
+ scheduleKey: `${row.config.id}:${toUtcMinute(now)}`,
784
+ })
785
+ if (!queuedJob) {
786
+ continue
787
+ }
788
+
789
+ queued.push({
790
+ backupRunId: queuedJob.backupRunId,
791
+ agentJobId: queuedJob.agentJobId,
792
+ })
793
+ } catch (error) {
794
+ if (isWorkspacePolicyRefusal(error)) continue
795
+ console.error('Scheduled backup admission failed', {
796
+ policyId: row.config.id,
797
+ ...safeErrorDiagnostics(error),
798
+ })
799
+ }
800
+ }
801
+
802
+ return queued
803
+ }
804
+
805
+ function selectExpiredBackupRunIds({
806
+ runs,
807
+ retention,
808
+ now,
809
+ }: {
810
+ runs: Array<{ id: string; finishedAt: Date; sizeBytes: number }>
811
+ retention: BackupRetentionLimit
812
+ now: Date
813
+ }) {
814
+ if (
815
+ retention.count === 0 &&
816
+ retention.days === 0 &&
817
+ retention.maxStorageGb === 0
818
+ ) {
819
+ return []
820
+ }
821
+
822
+ const expiredRunIds = new Set<string>()
823
+
824
+ if (retention.count > 0) {
825
+ for (const run of runs.slice(retention.count)) {
826
+ expiredRunIds.add(run.id)
827
+ }
828
+ }
829
+
830
+ if (retention.days > 0) {
831
+ const cutoff = now.getTime() - retention.days * 86_400_000
832
+ for (const run of runs) {
833
+ if (run.finishedAt.getTime() < cutoff) {
834
+ expiredRunIds.add(run.id)
835
+ }
836
+ }
837
+ }
838
+
839
+ if (retention.maxStorageGb > 0) {
840
+ const maxStorageBytes = retention.maxStorageGb * 1024 * 1024 * 1024
841
+ let keptBytes = 0
842
+
843
+ for (const run of runs) {
844
+ keptBytes += run.sizeBytes
845
+ if (keptBytes > maxStorageBytes) {
846
+ expiredRunIds.add(run.id)
847
+ }
848
+ }
849
+ }
850
+
851
+ return runs.filter((run) => expiredRunIds.has(run.id)).map((run) => run.id)
852
+ }
853
+
854
+ export function normalizeS3Destination(destination: BackupDestination) {
855
+ const normalized = s3DestinationSchema.parse(destination)
856
+ const prefix = normalized.prefix
857
+ ?.replace(/^\/+/, '')
858
+ .replace(/\/+$/, '')
859
+ .trim()
860
+
861
+ if (prefix?.includes('..')) {
862
+ throw new PublicError('BACKUP_DESTINATION_PREFIX_INVALID')
863
+ }
864
+
865
+ return {
866
+ ...normalized,
867
+ ...(prefix ? { prefix } : { prefix: undefined }),
868
+ }
869
+ }
870
+
871
+ function buildBackupArtifactPrefix({
872
+ destination,
873
+ projectId,
874
+ environmentId,
875
+ backupRunId,
876
+ }: {
877
+ destination: BackupDestination
878
+ projectId: string
879
+ environmentId: string
880
+ backupRunId: string
881
+ }) {
882
+ const prefix = destination.prefix?.trim().replace(/^\/+|\/+$/g, '')
883
+ const base =
884
+ prefix || `projects/${projectId}/environments/${environmentId}/backups`
885
+
886
+ return `${base}/runs/${backupRunId}`
887
+ }
888
+
889
+ function buildBackupLocalBasePath({
890
+ projectId,
891
+ environmentId,
892
+ backupRunId,
893
+ }: {
894
+ projectId: string
895
+ environmentId: string
896
+ backupRunId: string
897
+ }) {
898
+ return `/var/lib/devpush-agent/backups/${projectId}/${environmentId}/${backupRunId}`
899
+ }
900
+
901
+ function toUtcMinute(value: Date) {
902
+ return value.toISOString().slice(0, 16)
903
+ }
904
+
905
+ function requireDatabaseArtifactTarget(
906
+ objectKey: string,
907
+ manifest: unknown,
908
+ ): BackupDatabase {
909
+ const targets =
910
+ typeof manifest === 'object' &&
911
+ manifest !== null &&
912
+ 'databaseTargets' in manifest
913
+ ? z.array(backupDatabaseSchema).parse(manifest.databaseTargets)
914
+ : []
915
+ const target = databaseArtifactTarget(objectKey, targets)
916
+ if (!target) throw new PublicError('BACKUP_DATABASE_ENGINE_UNSUPPORTED')
917
+ return target
918
+ }