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,94 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks'
2
+ import { mkdirSync } from 'node:fs'
3
+ import { dirname } from 'node:path'
4
+
5
+ import Sqlite from 'better-sqlite3'
6
+ import { drizzle } from 'drizzle-orm/sqlite-proxy'
7
+ import type {
8
+ AsyncRemoteCallback,
9
+ SqliteRemoteDatabase,
10
+ } from 'drizzle-orm/sqlite-proxy'
11
+
12
+ import { sqliteParams } from './sqlite-params'
13
+
14
+ export function openSqlite<TSchema extends Record<string, unknown>>(
15
+ path: string,
16
+ schema: TSchema,
17
+ ) {
18
+ if (path !== ':memory:') mkdirSync(dirname(path), { recursive: true })
19
+ const connection = new Sqlite(path)
20
+ connection.pragma('foreign_keys = ON')
21
+ connection.pragma('journal_mode = WAL')
22
+ connection.pragma('synchronous = FULL')
23
+ connection.pragma('busy_timeout = 5000')
24
+
25
+ // Callbacks may await between statements. Serialize unrelated operations on
26
+ // this connection so they cannot join another request's uncommitted work.
27
+ const transactionScope = new AsyncLocalStorage<{ active: boolean }>()
28
+ let tail = Promise.resolve()
29
+ async function exclusive<T>(work: () => T | Promise<T>): Promise<T> {
30
+ if (transactionScope.getStore()?.active) return work()
31
+ const previous = tail
32
+ let release = () => {}
33
+ tail = new Promise<void>((resolve) => {
34
+ release = resolve
35
+ })
36
+ await previous
37
+ const scope = { active: true }
38
+ try {
39
+ return await transactionScope.run(scope, work)
40
+ } finally {
41
+ scope.active = false
42
+ release()
43
+ }
44
+ }
45
+ function query(
46
+ sql: string,
47
+ params: unknown[],
48
+ method: Parameters<AsyncRemoteCallback>[2],
49
+ batch = false,
50
+ ) {
51
+ const statement = connection.prepare(sql)
52
+ const values = sqliteParams(params)
53
+ if (method === 'run') {
54
+ statement.run(...values)
55
+ return { rows: [] }
56
+ }
57
+ const rows = statement.raw(true).all(...values) as unknown[][]
58
+ // Drizzle's callback type requires an array even though get() uses an
59
+ // undefined first row to represent no match. Keep that library contract.
60
+ return { rows: method === 'get' && !batch ? (rows[0] as unknown[]) : rows }
61
+ }
62
+ const database = drizzle<TSchema>(
63
+ (sql, params: unknown[], method) =>
64
+ exclusive(() => query(sql, params, method)),
65
+ (queries) =>
66
+ exclusive(() =>
67
+ connection
68
+ .transaction(() =>
69
+ queries.map((entry) =>
70
+ query(entry.sql, entry.params, entry.method, true),
71
+ ),
72
+ )
73
+ .immediate(),
74
+ ),
75
+ { schema },
76
+ )
77
+ const db = new Proxy(database, {
78
+ get(target, property) {
79
+ if (property === 'transaction') {
80
+ const transaction: SqliteRemoteDatabase<TSchema>['transaction'] = (
81
+ callback,
82
+ options,
83
+ ) =>
84
+ exclusive(() =>
85
+ target.transaction(callback, { ...options, behavior: 'immediate' }),
86
+ )
87
+ return transaction
88
+ }
89
+ const value: unknown = Reflect.get(target, property, target)
90
+ return typeof value === 'function' ? value.bind(target) : value
91
+ },
92
+ })
93
+ return { db, connection, close: () => exclusive(() => connection.close()) }
94
+ }
@@ -0,0 +1,27 @@
1
+ import { existsSync, realpathSync, statSync } from 'node:fs'
2
+ import { basename, dirname, resolve } from 'node:path'
3
+
4
+ export function assertSeparateSqliteFiles(mainPath: string, logPath: string) {
5
+ // Each :memory: connection owns an independent database.
6
+ if (mainPath === ':memory:' || logPath === ':memory:') return
7
+ let same = canonicalPath(mainPath) === canonicalPath(logPath)
8
+ if (!same && existsSync(mainPath) && existsSync(logPath)) {
9
+ const main = statSync(mainPath)
10
+ const logs = statSync(logPath)
11
+ same = main.dev === logs.dev && main.ino === logs.ino
12
+ }
13
+ if (same) {
14
+ throw new Error(
15
+ 'DEVPUSH_DATABASE_PATH and DEVPUSH_LOG_DATABASE_PATH must identify separate SQLite files.',
16
+ )
17
+ }
18
+ }
19
+
20
+ // Resolve existing parent symlinks even before the database files are created.
21
+ function canonicalPath(path: string): string {
22
+ const absolute = resolve(path)
23
+ if (existsSync(absolute)) return realpathSync(absolute)
24
+ const parent = dirname(absolute)
25
+ if (parent === absolute) return absolute
26
+ return resolve(canonicalPath(parent), basename(absolute))
27
+ }
@@ -0,0 +1,19 @@
1
+ export function sqliteParams(
2
+ params: unknown[],
3
+ ): Array<string | number | bigint | Uint8Array | null> {
4
+ return params.map((value) => {
5
+ if (value instanceof Date) return value.getTime()
6
+ if (typeof value === 'boolean') return Number(value)
7
+ if (
8
+ value === null ||
9
+ typeof value === 'string' ||
10
+ typeof value === 'number' ||
11
+ typeof value === 'bigint' ||
12
+ value instanceof Uint8Array
13
+ )
14
+ return value
15
+ throw new Error(
16
+ 'Unsupported SQLite bound parameter; encode values at the schema boundary.',
17
+ )
18
+ })
19
+ }
@@ -0,0 +1,47 @@
1
+ import { createClient } from '@libsql/client/web'
2
+ import type { D1Database } from '@cloudflare/workers-types'
3
+ import { openD1 } from './d1-connection'
4
+ import { openLibsql } from './libsql-connection'
5
+ import { remoteDatabaseConfig } from './remote-config'
6
+ import * as schema from './schema'
7
+ import * as logSchema from './log-schema'
8
+
9
+ type DatabaseEnvironment = Parameters<typeof remoteDatabaseConfig>[0] & {
10
+ DEVPUSH_DATABASE?: D1Database
11
+ DEVPUSH_LOG_DATABASE?: D1Database
12
+ }
13
+
14
+ export function openWorkerDatabases(env: DatabaseEnvironment) {
15
+ const remote = remoteDatabaseConfig(env)
16
+ if (remote)
17
+ return {
18
+ db: openLibsql(() => createClient(remote.main), schema).db,
19
+ logsDb: openLibsql(() => createClient(remote.logs), logSchema).db,
20
+ }
21
+ if (!env.DEVPUSH_DATABASE || !env.DEVPUSH_LOG_DATABASE)
22
+ throw new Error('Configure both D1 bindings or both remote database URLs.')
23
+ return {
24
+ db: openD1(env.DEVPUSH_DATABASE, schema),
25
+ logsDb: openD1(env.DEVPUSH_LOG_DATABASE, logSchema),
26
+ }
27
+ }
28
+
29
+ // Cloudflare's upload validation can evaluate modules before resource bindings
30
+ // are available. Resolve the stores when application code first uses them.
31
+ export function createWorkerDatabases(env: DatabaseEnvironment) {
32
+ let stores: ReturnType<typeof openWorkerDatabases> | undefined
33
+ const getStores = () => (stores ??= openWorkerDatabases(env))
34
+ function lazy<TStore extends object>(getStore: () => TStore): TStore {
35
+ return new Proxy({} as TStore, {
36
+ get(_target, property) {
37
+ const store = getStore()
38
+ const value: unknown = Reflect.get(store, property, store)
39
+ return typeof value === 'function' ? value.bind(store) : value
40
+ },
41
+ })
42
+ }
43
+ return {
44
+ db: lazy(() => getStores().db),
45
+ logsDb: lazy(() => getStores().logsDb),
46
+ }
47
+ }
@@ -0,0 +1,133 @@
1
+ import { sql } from 'drizzle-orm'
2
+ import { removeOwnedAvatar } from '#devpush/server/uploads/avatar'
3
+ import { db } from '#devpush/server/db'
4
+ import type { AtomicStore } from '#devpush/server/db/atomic'
5
+
6
+ // Keep terminal job records long enough for an agent to retry a lost acknowledgement.
7
+ // Unresolved external resources are never treated as successfully cleaned up.
8
+ export const deletedResourceRetentionMs = 30 * 24 * 60 * 60 * 1000
9
+
10
+ const resolvedJob = sql`(status = 'succeeded' or (status = 'canceled' and type not in ('deprovision', 'purge') and (attempt = 0
11
+ or json_extract(case when json_valid(payload) then payload else '{}' end, '$.operatorResolution.kind') = 'confirmed-cleanup')))`
12
+
13
+ export async function pruneDeletedResources({
14
+ store = db,
15
+ now = new Date(),
16
+ limit = 100,
17
+ }: {
18
+ store?: AtomicStore
19
+ now?: Date
20
+ limit?: number
21
+ } = {}) {
22
+ const cutoff = now.getTime() - deletedResourceRetentionMs
23
+ const batchLimit = Math.max(1, Math.min(100, Math.trunc(limit)))
24
+ // Keep the owning row if object storage is unavailable, so a later sweep
25
+ // can retry rather than losing the only reference to an uploaded avatar.
26
+ for (const table of ['projects', 'teams'] as const) {
27
+ const rows =
28
+ await store.all(sql`select id, avatar_path from ${sql.raw(table)}
29
+ where status = 'deleted' and updated_at <= ${cutoff} and avatar_path is not null
30
+ order by id limit ${batchLimit}`)
31
+ for (const row of rows) {
32
+ const values = Array.isArray(row)
33
+ ? row
34
+ : Object.values(row as Record<string, unknown>)
35
+ const [id, path] = values
36
+ if (typeof id !== 'string' || typeof path !== 'string')
37
+ throw new Error('Invalid deleted avatar row.')
38
+ try {
39
+ await removeOwnedAvatar(
40
+ path,
41
+ {
42
+ filePrefix: table === 'projects' ? 'project' : 'team',
43
+ resourceId: id,
44
+ },
45
+ { strict: true },
46
+ )
47
+ await store.run(sql`update ${sql.raw(table)} set avatar_path = null where id = ${id}
48
+ and status = 'deleted' and avatar_path = ${path}`)
49
+ } catch {
50
+ console.error('Unable to remove deleted resource avatar.', {
51
+ resourceId: id,
52
+ table,
53
+ })
54
+ }
55
+ }
56
+ }
57
+ const unprovisionedProjects = sql`select id from projects where status = 'deleted'
58
+ and (exists(select 1 from aliases a where a.project_id = projects.id)
59
+ or exists(select 1 from domains d where d.project_id = projects.id))
60
+ and not exists(select 1 from environments e where e.project_id = projects.id and e.status <> 'deleted')
61
+ and not exists(select 1 from agent_jobs j where j.type = 'deprovision' and j.status <> 'succeeded'
62
+ and (j.project_id = projects.id or (j.team_id = projects.team_id and j.project_id is null)))
63
+ order by id limit ${batchLimit}`
64
+ await store.batch([
65
+ store.run(
66
+ sql`delete from aliases where project_id in (${unprovisionedProjects})`,
67
+ ),
68
+ store.run(
69
+ sql`delete from domains where project_id in (${unprovisionedProjects})`,
70
+ ),
71
+ ])
72
+ const expiredJobs = sql`${resolvedJob} and coalesce(finished_at, updated_at) <= ${cutoff}`
73
+ const projectEligible = sql`avatar_path is null and status = 'deleted' and updated_at <= ${cutoff}
74
+ and not exists(select 1 from environments e where e.project_id = projects.id and e.status <> 'deleted')
75
+ and not exists(select 1 from backup_runs r where r.project_id = projects.id and (
76
+ r.status in ('queued', 'running') or r.cleanup_queued_at is not null
77
+ or exists(select 1 from backup_artifacts a where a.backup_run_id = r.id)))
78
+ and not exists(select 1 from agent_jobs j where
79
+ (j.project_id = projects.id or (j.team_id = projects.team_id and j.project_id is null))
80
+ and not (${expiredJobs}))`
81
+ const serverEligible = sql`status = 'deleted' and updated_at <= ${cutoff}
82
+ and not exists(select 1 from projects p where p.default_server_id = servers.id)
83
+ and not exists(select 1 from environments e where e.server_id = servers.id)
84
+ and not exists(select 1 from deployments d where d.server_id = servers.id)
85
+ and not exists(select 1 from backup_runs r where r.server_id = servers.id)
86
+ and not exists(select 1 from agent_jobs j where j.server_id = servers.id and not (${expiredJobs}))
87
+ and not exists(select 1 from agent_runtime_observations o where o.server_id = servers.id
88
+ and not exists(select 1 from agent_jobs j,
89
+ json_each(case when json_valid(j.payload) then j.payload else '{}' end, '$.runtimeIds') target
90
+ where j.server_id = servers.id and j.type = 'purge' and j.status = 'succeeded'
91
+ and j.finished_at >= o.last_seen_at and target.value = o.runtime_id))`
92
+ // No asynchronous reads authorize a delete. Each mutation repeats its guard in
93
+ // the atomic batch; foreign-key cascades then remove owned settings/credentials.
94
+ const projectIds = sql`select id from projects where ${projectEligible} order by id limit ${batchLimit}`
95
+ const serverIds = sql`select id from servers where ${serverEligible} order by id limit ${batchLimit}`
96
+ const teamEligible = sql`avatar_path is null and status = 'deleted' and updated_at <= ${cutoff}
97
+ and not exists(select 1 from projects p where p.team_id = teams.id)
98
+ and not exists(select 1 from servers s where s.team_id = teams.id)
99
+ and not exists(select 1 from agent_jobs j where j.team_id = teams.id and not (${expiredJobs}))`
100
+ const teamIds = sql`select id from teams where ${teamEligible} order by id limit ${batchLimit}`
101
+ const userEligible = sql`status = 'deleted' and updated_at <= ${cutoff}
102
+ and not exists(select 1 from team_members m where m.user_id = user.id)
103
+ and exists(select 1 from user_cleanup_jobs j where j.user_id = user.id and j.status = 'succeeded')
104
+ and not exists(select 1 from user_cleanup_jobs j where j.user_id = user.id and j.status <> 'succeeded')`
105
+ const results = await store.batch([
106
+ store.run(
107
+ sql`delete from agent_jobs where project_id in (${projectIds}) and ${expiredJobs}
108
+ and exists(select 1 from servers s where s.id = agent_jobs.server_id and s.status <> 'deleted')`,
109
+ ),
110
+ store.all(
111
+ sql`delete from projects where id in (${projectIds}) returning id`,
112
+ ),
113
+ store.run(
114
+ sql`delete from agent_runtime_observations where server_id in (${serverIds})`,
115
+ ),
116
+ store.run(
117
+ sql`delete from agent_jobs where server_id in (${serverIds}) and ${expiredJobs}`,
118
+ ),
119
+ store.all(sql`delete from servers where id in (${serverIds}) returning id`),
120
+ store.run(
121
+ sql`delete from agent_jobs where team_id in (${teamIds}) and ${expiredJobs}`,
122
+ ),
123
+ store.all(sql`delete from teams where id in (${teamIds}) returning id`),
124
+ store.all(sql`delete from user where id in (select id from user where ${userEligible}
125
+ order by id limit ${batchLimit}) returning id`),
126
+ ])
127
+ return {
128
+ projects: results[1].length,
129
+ servers: results[4].length,
130
+ teams: results[6].length,
131
+ users: results[7].length,
132
+ }
133
+ }
@@ -0,0 +1,272 @@
1
+ import { sql } from 'drizzle-orm'
2
+ import type { SQL } from 'drizzle-orm'
3
+ import type { AtomicStore } from './db/atomic'
4
+ import { runtimeCleanupInsert } from './agents/cleanup'
5
+
6
+ export type RecoveryAction =
7
+ | 'retry-job'
8
+ | 'confirm-job'
9
+ | 'cleanup-project'
10
+ | 'confirm-artifact'
11
+ export type RecoveryTarget = 'project' | 'server' | 'team' | 'job' | 'artifact'
12
+ type Row = Record<string, unknown>
13
+
14
+ export class RecoveryInputError extends Error {}
15
+
16
+ export function recoveryRows(rows: unknown[]): Row[] {
17
+ return rows.map((row) => {
18
+ const encoded = Array.isArray(row)
19
+ ? row[0]
20
+ : row && typeof row === 'object' && 'record' in row
21
+ ? row.record
22
+ : undefined
23
+ if (typeof encoded === 'string') return JSON.parse(encoded) as Row
24
+ if (!row || typeof row !== 'object' || Array.isArray(row))
25
+ throw new RecoveryInputError('Recovery requires named database results.')
26
+ return row as Row
27
+ })
28
+ }
29
+
30
+ export function listDeletedResourcesQuery() {
31
+ return jsonRows(
32
+ sql`select 'project' as target, id, name, updated_at from projects where status = 'deleted'
33
+ union all select 'team', id, name, updated_at from teams where status = 'deleted'
34
+ union all select 'server', id, name, updated_at from servers where status = 'deleted'
35
+ order by updated_at, target, id limit 101`,
36
+ ['target', 'id', 'name', 'updated_at'],
37
+ )
38
+ }
39
+
40
+ export function recoveryTargetQuery(target: RecoveryTarget, id: string) {
41
+ const table = {
42
+ project: 'projects',
43
+ server: 'servers',
44
+ team: 'teams',
45
+ job: 'agent_jobs',
46
+ artifact: 'backup_artifacts',
47
+ }[target]
48
+ const fields =
49
+ target === 'job'
50
+ ? [
51
+ 'id',
52
+ 'type',
53
+ 'status',
54
+ 'server_id',
55
+ 'team_id',
56
+ 'project_id',
57
+ 'payload',
58
+ 'attempt',
59
+ 'max_attempts',
60
+ 'updated_at',
61
+ 'claimed_at',
62
+ 'available_at',
63
+ ]
64
+ : target === 'artifact'
65
+ ? ['id', 'backup_run_id', 'storage', 'object_key', 'checksum_sha256']
66
+ : target === 'team'
67
+ ? ['id', 'status', 'updated_at']
68
+ : ['id', 'status', 'updated_at', 'team_id']
69
+ return jsonRows(sql`select * from ${sql.raw(table)} where id = ${id}`, fields)
70
+ }
71
+
72
+ function jsonRows(query: SQL, fields: string[]) {
73
+ return sql`select json_object(${sql.join(
74
+ fields.flatMap((field) => [sql`${field}`, sql.raw(field)]),
75
+ sql`, `,
76
+ )}) as record from (${query})`
77
+ }
78
+
79
+ // Whitelist output: job payloads and resource rows can contain credentials.
80
+ export async function inspectDeletion(
81
+ store: AtomicStore,
82
+ target: RecoveryTarget,
83
+ row: Row,
84
+ ) {
85
+ const id = String(row.id)
86
+ const projectIds =
87
+ target === 'project'
88
+ ? sql`select id from projects where id = ${id}`
89
+ : target === 'team'
90
+ ? sql`select id from projects where team_id = ${id}`
91
+ : target === 'server'
92
+ ? sql`select id from projects where default_server_id = ${id}
93
+ union select project_id from environments where server_id = ${id}
94
+ union select project_id from deployments where server_id = ${id}`
95
+ : sql`select null where 0`
96
+ const jobs = await store.all(
97
+ jsonRows(
98
+ sql`select id, type, status, attempt, max_attempts, server_id, project_id,
99
+ available_at, claimed_at, updated_at, json_extract(payload, '$.runtimeIds') as runtime_ids,
100
+ json_extract(payload, '$.backupRunIds') as backup_run_ids, json_extract(payload, '$.localObjectKeys') as local_object_keys,
101
+ json_extract(payload, '$.remoteObjectKeys') as remote_object_keys, json_extract(payload, '$.operatorResolution') as resolution from agent_jobs where
102
+ ${target === 'job' ? sql`id = ${id}` : target === 'server' ? sql`server_id = ${id}` : target === 'team' ? sql`team_id = ${id} or project_id in (${projectIds})` : sql`project_id in (${projectIds}) or (project_id is null and team_id = ${row.team_id ?? null})`}
103
+ order by id limit 101`,
104
+ [
105
+ 'id',
106
+ 'type',
107
+ 'status',
108
+ 'attempt',
109
+ 'max_attempts',
110
+ 'server_id',
111
+ 'project_id',
112
+ 'available_at',
113
+ 'claimed_at',
114
+ 'updated_at',
115
+ 'runtime_ids',
116
+ 'backup_run_ids',
117
+ 'local_object_keys',
118
+ 'remote_object_keys',
119
+ 'resolution',
120
+ ],
121
+ ),
122
+ )
123
+ const artifacts = await store.all(
124
+ jsonRows(
125
+ sql`select a.id, a.backup_run_id, a.storage, a.object_key, a.checksum_sha256,
126
+ a.size_bytes, r.project_id, r.server_id, json_extract(r.destination, '$.bucket') as bucket,
127
+ json_extract(r.destination, '$.storageId') as storage_id from backup_artifacts a join backup_runs r on r.id = a.backup_run_id
128
+ where ${target === 'artifact' ? sql`a.id = ${id}` : target === 'server' ? sql`r.server_id = ${id}` : sql`r.project_id in (${projectIds})`}
129
+ order by a.id limit 101`,
130
+ [
131
+ 'id',
132
+ 'backup_run_id',
133
+ 'storage',
134
+ 'object_key',
135
+ 'checksum_sha256',
136
+ 'size_bytes',
137
+ 'project_id',
138
+ 'server_id',
139
+ 'bucket',
140
+ 'storage_id',
141
+ ],
142
+ ),
143
+ )
144
+ const environments = await store.all(
145
+ jsonRows(
146
+ sql`select id, project_id, runtime_id, status from environments
147
+ where project_id in (${projectIds}) order by id limit 101`,
148
+ ['id', 'project_id', 'runtime_id', 'status'],
149
+ ),
150
+ )
151
+ return {
152
+ target,
153
+ id,
154
+ status: row.status,
155
+ jobs: recoveryRows(jobs),
156
+ artifacts: recoveryRows(artifacts),
157
+ environments: recoveryRows(environments),
158
+ note: 'Each list is limited to 101 rows. Inspect individual IDs; never treat this as a complete archive inventory.',
159
+ }
160
+ }
161
+
162
+ export function deletionRecoveryPlan(input: {
163
+ action: RecoveryAction
164
+ row: Row
165
+ operator: string
166
+ reason: string
167
+ now: Date
168
+ }): [SQL, ...SQL[]] {
169
+ const { action, row, now } = input
170
+ if (!input.operator.trim() || !input.reason.trim())
171
+ throw new RecoveryInputError('Operator and evidence/reason are required.')
172
+ const id = String(row.id),
173
+ time = now.getTime()
174
+ const resolution = JSON.stringify({
175
+ kind: 'confirmed-cleanup',
176
+ operator: input.operator,
177
+ reason: input.reason,
178
+ at: time,
179
+ })
180
+ const statements: SQL[] = []
181
+ let guard: SQL
182
+ let team: SQL
183
+ if (action === 'cleanup-project') {
184
+ if (row.status !== 'deleted')
185
+ throw new RecoveryInputError('Only deleted projects can be cleaned up.')
186
+ guard = sql`exists(select 1 from projects where id = ${id} and status = 'deleted' and updated_at = ${row.updated_at}
187
+ and exists(select 1 from environments where project_id = ${id} and status <> 'deleted'))`
188
+ team = sql`select team_id from projects where id = ${id}`
189
+ statements.push(
190
+ runtimeCleanupInsert({
191
+ projectIds: sql`select id from projects where id = ${id}`,
192
+ projectId: id,
193
+ guard,
194
+ reason: `Operator ${input.operator}: ${input.reason}`,
195
+ now,
196
+ }),
197
+ )
198
+ statements.push(sql`update environments set status = 'deleted', is_production = 0, updated_at = ${time}
199
+ where project_id = ${id} and ${guard}`)
200
+ } else if (action === 'confirm-artifact') {
201
+ guard = sql`exists(select 1 from backup_artifacts a join backup_runs r on r.id = a.backup_run_id
202
+ join projects p on p.id = r.project_id where a.id = ${id} and p.status = 'deleted'
203
+ and a.object_key = ${row.object_key} and a.checksum_sha256 = ${row.checksum_sha256}
204
+ and a.backup_run_id = ${row.backup_run_id} and a.storage = ${row.storage}
205
+ and r.status not in ('queued','running') and r.cleanup_queued_at is null)`
206
+ team = sql`select p.team_id from projects p join backup_runs r on r.project_id = p.id where r.id = ${row.backup_run_id}`
207
+ statements.push(sql`update backup_runs set manifest = json_set(coalesce(manifest, '{}'), '$.operatorRecovery', json(${resolution})),
208
+ updated_at = ${time} where id = ${row.backup_run_id} and ${guard}`)
209
+ statements.push(
210
+ sql`delete from backup_artifacts where id = ${id} and ${guard}`,
211
+ )
212
+ } else {
213
+ const cleanup = row.type === 'deprovision' || row.type === 'purge'
214
+ if (row.status === 'succeeded')
215
+ throw new RecoveryInputError('This job has already completed.')
216
+ const staleCleanup =
217
+ cleanup &&
218
+ row.status === 'running' &&
219
+ (row.claimed_at === null ||
220
+ Number(row.claimed_at) < time - 60 * 60 * 1000)
221
+ if (row.status === 'running' && !staleCleanup)
222
+ throw new RecoveryInputError(
223
+ 'Wait for the running job to finish or become stale before recovery.',
224
+ )
225
+ if (action === 'retry-job' && !cleanup)
226
+ throw new RecoveryInputError(
227
+ 'Only container cleanup jobs can be retried; do not replay deployments, backups or restores.',
228
+ )
229
+ if (
230
+ action === 'retry-job' &&
231
+ row.status !== 'failed' &&
232
+ row.status !== 'abandoned' &&
233
+ row.status !== 'canceled' &&
234
+ !staleCleanup
235
+ )
236
+ throw new RecoveryInputError(
237
+ 'Only terminal unsuccessful cleanup jobs can be retried.',
238
+ )
239
+ guard = sql`exists(select 1 from agent_jobs j where j.id = ${id} and j.status = ${row.status}
240
+ and j.type = ${row.type} and j.server_id = ${row.server_id}
241
+ and j.project_id is ${row.project_id ?? null} and j.team_id is ${row.team_id ?? null}
242
+ and j.available_at = ${row.available_at} and j.attempt = ${row.attempt} and j.claimed_at is ${row.claimed_at ?? null} and j.updated_at = ${row.updated_at} and j.payload = ${row.payload}
243
+ and (exists(select 1 from projects p where p.id = j.project_id and p.status = 'deleted')
244
+ or exists(select 1 from servers s where s.id = j.server_id and s.status = 'deleted')
245
+ or exists(select 1 from teams t where t.id = j.team_id and t.status = 'deleted')))`
246
+ team = sql`select coalesce(j.team_id, p.team_id, s.team_id) from agent_jobs j
247
+ left join projects p on p.id = j.project_id join servers s on s.id = j.server_id where j.id = ${id}`
248
+ if (action === 'retry-job') {
249
+ statements.push(sql`update agent_jobs set status = 'queued', claimed_at = null, finished_at = null,
250
+ available_at = max(available_at, ${time}), updated_at = ${time}, max_attempts = max(max_attempts, attempt + 10),
251
+ error_message = null, payload = json_set(payload, '$.operatorRetry', json(${resolution})) where id = ${id} and ${guard}`)
252
+ } else {
253
+ if (row.type === 'backup_cleanup')
254
+ statements.push(sql`update backup_runs set cleanup_queued_at = null, cleanup_error_message = null, updated_at = ${time}
255
+ where project_id = ${row.project_id} and server_id = ${row.server_id}
256
+ and id in (select value from json_each(${String(row.payload)}, '$.backupRunIds')) and ${guard}`)
257
+ if (row.type === 'backup')
258
+ statements.push(sql`update backup_runs set status = 'canceled', finished_at = ${time}, updated_at = ${time}
259
+ where id = json_extract(${String(row.payload)}, '$.backupRunId') and project_id = ${row.project_id}
260
+ and server_id = ${row.server_id} and status in ('queued','running') and ${guard}`)
261
+ // A confirmed cleanup is a real success; failed executions remain canceled,
262
+ // with an explicit operator resolution instead of a fabricated backup success.
263
+ statements.push(sql`update agent_jobs set status = ${cleanup ? 'succeeded' : 'canceled'},
264
+ finished_at = ${time}, updated_at = ${time}, error_message = ${`Operator resolution: ${input.reason}`},
265
+ payload = json_set(payload, '$.operatorResolution', json(${resolution})) where id = ${id} and ${guard}`)
266
+ }
267
+ }
268
+ const audit = sql`insert into api_audit (id, team_id, user_id, credential_id, request_id, operation, resource_id, outcome, created_at)
269
+ select ${crypto.randomUUID()}, coalesce((${team}), 'installation'), ${input.operator}, 'operator-cli',
270
+ ${crypto.randomUUID()}, ${`deletion.${action}`}, ${id}, 'succeeded', ${time} where ${guard} returning id`
271
+ return [audit, ...statements]
272
+ }
@@ -0,0 +1,85 @@
1
+ import type { ReactNode } from 'react'
2
+ import {
3
+ Body,
4
+ Container,
5
+ Head,
6
+ Heading,
7
+ Html,
8
+ Img,
9
+ Link,
10
+ Preview,
11
+ Section,
12
+ Tailwind,
13
+ Text,
14
+ } from '@react-email/components'
15
+ import { config } from '#devpush/server/config'
16
+ import { emailTheme } from './theme'
17
+
18
+ export function EmailLayout({
19
+ title,
20
+ preview,
21
+ to,
22
+ children,
23
+ allowIgnore = true,
24
+ }: {
25
+ title: string
26
+ preview: string
27
+ to: string
28
+ children: ReactNode
29
+ allowIgnore?: boolean
30
+ }) {
31
+ return (
32
+ <Html>
33
+ <Head />
34
+ <Preview>{preview}</Preview>
35
+ <Tailwind>
36
+ <Body
37
+ className="my-auto mx-auto px-2 font-sans antialiased"
38
+ style={{
39
+ backgroundColor: emailTheme.background,
40
+ color: emailTheme.foreground,
41
+ }}
42
+ >
43
+ <Container className="my-[40px] mx-auto max-w-[465px] p-[20px]">
44
+ <Section className="mt-[24px]">
45
+ <Img
46
+ alt="devpush"
47
+ className="my-0 mx-auto"
48
+ width="32"
49
+ height="32"
50
+ src={new URL(
51
+ '/logo-email.png',
52
+ config.BETTER_AUTH_URL,
53
+ ).toString()}
54
+ />
55
+ </Section>
56
+ <Heading
57
+ className="my-[30px] mx-0 p-0 text-center text-[24px] font-semibold tracking-tight"
58
+ style={{ color: emailTheme.foreground }}
59
+ >
60
+ {title}
61
+ </Heading>
62
+ {children}
63
+ <Text
64
+ className="mt-[36px] text-[14px] leading-[24px]"
65
+ style={{ color: emailTheme.mutedForeground }}
66
+ >
67
+ This email was intended for{' '}
68
+ <Link
69
+ className="underline"
70
+ href={`mailto:${to}`}
71
+ style={{ color: emailTheme.mutedLink }}
72
+ >
73
+ {to}
74
+ </Link>
75
+ .
76
+ {allowIgnore
77
+ ? ' If you think this is a mistake, you can safely ignore this email.'
78
+ : ''}
79
+ </Text>
80
+ </Container>
81
+ </Body>
82
+ </Tailwind>
83
+ </Html>
84
+ )
85
+ }