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,260 @@
1
+ import { and, desc, eq, sql, or, count } from 'drizzle-orm'
2
+ import {
3
+ allowlistRuleSchema,
4
+ allowlistImportSchema,
5
+ parseAllowlistEmails,
6
+ } from '#devpush/lib/allowlist-schema'
7
+
8
+ import { adminSearchSchema } from '#devpush/lib/admin-search-schema'
9
+ import { db } from '#devpush/server/db'
10
+ import { allowlist, session, user } from '#devpush/server/db/schema'
11
+ import { deleteUserAccountAsAdmin } from '#devpush/server/users/service'
12
+ import {
13
+ requireInstallationAdmin,
14
+ requireInstallationAdminById,
15
+ installationAdminPredicate,
16
+ } from '#devpush/server/admin/access'
17
+
18
+ export type AdminOverview = Awaited<ReturnType<typeof getAdminOverview>>
19
+ export type AllowlistRuleType = 'email' | 'domain' | 'pattern'
20
+
21
+ export async function getAdminOverview(
22
+ actor: { id: string; email: string },
23
+ search: {
24
+ allowlistQuery?: string
25
+ allowlistPage?: number
26
+ userQuery?: string
27
+ userPage?: number
28
+ } = {},
29
+ ) {
30
+ await requireInstallationAdmin(actor)
31
+ const [users, allowlistEntries] = await Promise.all([
32
+ getAdminUsers(actor, search),
33
+ getAdminAllowlist(actor, search),
34
+ ])
35
+
36
+ return {
37
+ users: users.items,
38
+ userPagination: users.pagination,
39
+ allowlist: allowlistEntries.items,
40
+ allowlistPagination: allowlistEntries.pagination,
41
+ }
42
+ }
43
+
44
+ export async function getAdminUsers(
45
+ actor: { id: string; email: string },
46
+ search: { userQuery?: string; userPage?: number } = {},
47
+ ) {
48
+ await requireInstallationAdmin(actor)
49
+ const { userQuery: query, userPage: requestedPage } =
50
+ adminSearchSchema.parse(search)
51
+ const escaped = query.replace(/[\\%_]/g, '\\$&')
52
+ const where = query
53
+ ? or(
54
+ ...[user.name, user.username, user.email, user.status].map(
55
+ (column) => sql`${column} like ${`%${escaped}%`} escape '\\'`,
56
+ ),
57
+ )
58
+ : undefined
59
+ const [totalRow] = await db.select({ total: count() }).from(user).where(where)
60
+ const total = totalRow?.total ?? 0
61
+ const pageSize = 25
62
+ const pageCount = Math.max(1, Math.ceil(total / pageSize))
63
+ const page = Math.min(requestedPage, pageCount)
64
+ const entries = await db
65
+ .select({
66
+ id: user.id,
67
+ name: user.name,
68
+ username: user.username,
69
+ email: user.email,
70
+ emailVerified: user.emailVerified,
71
+ status: user.status,
72
+ image: user.image,
73
+ createdAt: user.createdAt,
74
+ })
75
+ .from(user)
76
+ .where(where)
77
+ .orderBy(desc(user.createdAt), desc(user.id))
78
+ .limit(pageSize)
79
+ .offset((page - 1) * pageSize)
80
+ return {
81
+ items: entries.map((entry) => ({
82
+ ...entry,
83
+ createdAt: entry.createdAt.toISOString(),
84
+ })),
85
+ pagination: { query, page, pageCount, total, pageSize },
86
+ }
87
+ }
88
+
89
+ export async function getAdminAllowlist(
90
+ actor: { id: string; email: string },
91
+ search: { allowlistQuery?: string; allowlistPage?: number } = {},
92
+ ) {
93
+ await requireInstallationAdmin(actor)
94
+ const { allowlistQuery: query, allowlistPage: requestedPage } =
95
+ adminSearchSchema.parse(search)
96
+ // Treat SQL wildcard characters as literal search text.
97
+ const escaped = query.replace(/[\\%_]/g, '\\$&')
98
+ const where = query
99
+ ? or(
100
+ sql`${allowlist.value} like ${`%${escaped}%`} escape '\\'`,
101
+ sql`${allowlist.type} like ${`%${escaped}%`} escape '\\'`,
102
+ )
103
+ : undefined
104
+ const [totalRow] = await db
105
+ .select({ total: count() })
106
+ .from(allowlist)
107
+ .where(where)
108
+ const total = totalRow?.total ?? 0
109
+ const pageSize = 25
110
+ const pageCount = Math.max(1, Math.ceil(total / pageSize))
111
+ const page = Math.min(requestedPage, pageCount)
112
+ const entries = await db
113
+ .select({
114
+ id: allowlist.id,
115
+ type: allowlist.type,
116
+ value: allowlist.value,
117
+ createdAt: allowlist.createdAt,
118
+ })
119
+ .from(allowlist)
120
+ .where(where)
121
+ .orderBy(desc(allowlist.createdAt), desc(allowlist.id))
122
+ .limit(pageSize)
123
+ .offset((page - 1) * pageSize)
124
+ return {
125
+ items: entries.map((entry) => ({
126
+ ...entry,
127
+ createdAt: entry.createdAt.toISOString(),
128
+ })),
129
+ pagination: { query, page, pageCount, total, pageSize },
130
+ }
131
+ }
132
+
133
+ export async function addAllowlistRule({
134
+ actorUserId,
135
+ type,
136
+ value,
137
+ }: {
138
+ actorUserId: string
139
+ type: AllowlistRuleType
140
+ value: string
141
+ }) {
142
+ await requireInstallationAdminById(actorUserId)
143
+ const normalizedValue = allowlistRuleSchema.parse({ type, value }).value
144
+
145
+ await db
146
+ .insert(allowlist)
147
+ .values({
148
+ id: crypto.randomUUID(),
149
+ type,
150
+ value: normalizedValue,
151
+ })
152
+ .onConflictDoNothing()
153
+ }
154
+
155
+ export async function importAllowlistEmails({
156
+ actorUserId,
157
+ emails,
158
+ }: {
159
+ actorUserId: string
160
+ emails: string
161
+ }) {
162
+ await requireInstallationAdminById(actorUserId)
163
+ const parsed = allowlistImportSchema.parse({ emails })
164
+ const entries = parseAllowlistEmails(parsed.emails).map((value) => ({
165
+ id: crypto.randomUUID(),
166
+ value,
167
+ }))
168
+ // One bound JSON value keeps large imports within every SQLite driver's limit.
169
+ await db.run(sql`insert into ${allowlist} (id, type, value)
170
+ select json_extract(value, '$.id'), 'email', json_extract(value, '$.value')
171
+ from json_each(${JSON.stringify(entries)}) where true
172
+ on conflict do nothing`)
173
+ }
174
+
175
+ export async function deleteAllowlistRule({
176
+ actorUserId,
177
+ id,
178
+ }: {
179
+ actorUserId: string
180
+ id: string
181
+ }) {
182
+ await requireInstallationAdminById(actorUserId)
183
+ await db.delete(allowlist).where(eq(allowlist.id, id))
184
+ }
185
+
186
+ export async function banUser({
187
+ actorUserId,
188
+ targetUserId,
189
+ }: {
190
+ actorUserId: string
191
+ targetUserId: string
192
+ }) {
193
+ await requireInstallationAdminById(actorUserId)
194
+ if (actorUserId === targetUserId) {
195
+ throw new Error('You cannot ban yourself.')
196
+ }
197
+
198
+ await db.batch([
199
+ db
200
+ .delete(session)
201
+ .where(
202
+ and(
203
+ eq(session.userId, targetUserId),
204
+ installationAdminPredicate(actorUserId),
205
+ ),
206
+ ),
207
+ db
208
+ .update(user)
209
+ .set({ status: 'banned', updatedAt: new Date() })
210
+ .where(
211
+ and(
212
+ eq(user.id, targetUserId),
213
+ eq(user.status, 'active'),
214
+ installationAdminPredicate(actorUserId),
215
+ ),
216
+ ),
217
+ ])
218
+ }
219
+
220
+ export async function unbanUser({
221
+ actorUserId,
222
+ targetUserId,
223
+ }: {
224
+ actorUserId: string
225
+ targetUserId: string
226
+ }) {
227
+ await requireInstallationAdminById(actorUserId)
228
+ if (actorUserId === targetUserId) {
229
+ throw new Error('You cannot unban yourself.')
230
+ }
231
+
232
+ await db
233
+ .update(user)
234
+ .set({ status: 'active', updatedAt: new Date() })
235
+ .where(
236
+ and(
237
+ eq(user.id, targetUserId),
238
+ eq(user.status, 'banned'),
239
+ installationAdminPredicate(actorUserId),
240
+ ),
241
+ )
242
+ }
243
+
244
+ export async function deleteAdminUser({
245
+ actorUserId,
246
+ targetUserId,
247
+ }: {
248
+ actorUserId: string
249
+ targetUserId: string
250
+ }) {
251
+ await requireInstallationAdminById(actorUserId)
252
+ if (actorUserId === targetUserId) {
253
+ throw new Error('You cannot delete yourself from admin.')
254
+ }
255
+
256
+ await deleteUserAccountAsAdmin({
257
+ userId: targetUserId,
258
+ adminActorUserId: actorUserId,
259
+ })
260
+ }
@@ -0,0 +1,210 @@
1
+ import { sql } from 'drizzle-orm'
2
+ import type { AtomicStore } from '#devpush/server/db/atomic'
3
+ import {
4
+ agentJobs,
5
+ backupArtifacts,
6
+ backupRuns,
7
+ } from '#devpush/server/db/schema'
8
+ import type {
9
+ AgentBackupCompletion,
10
+ AgentBackupCleanupCompletion,
11
+ } from './protocol'
12
+
13
+ function runningJob(
14
+ jobId: string,
15
+ serverId: string,
16
+ type: 'backup' | 'backup_cleanup',
17
+ ) {
18
+ return sql`exists(select 1 from ${agentJobs} where id = ${jobId} and server_id = ${serverId}
19
+ and type = ${type} and status = 'running')`
20
+ }
21
+
22
+ export async function failStaleAgentJob(
23
+ store: AtomicStore,
24
+ input: {
25
+ job: typeof agentJobs.$inferSelect
26
+ payloadSnapshot: string
27
+ now: Date
28
+ timeoutMs: number
29
+ message: string
30
+ backupRunId: string | null
31
+ cleanupRunIds: string[]
32
+ },
33
+ ) {
34
+ const now = input.now.getTime()
35
+ const guard = sql`id = ${input.job.id} and server_id = ${input.job.serverId}
36
+ and type = ${input.job.type} and payload = ${input.payloadSnapshot} and status = ${input.job.status}
37
+ and attempt = ${input.job.attempt} and updated_at = ${input.job.updatedAt.getTime()}
38
+ and ((status = 'running' and (claimed_at is null or claimed_at < ${now - input.timeoutMs}))
39
+ or (status = 'queued' and available_at < ${now - input.timeoutMs}))`
40
+ const eligible = sql`exists(select 1 from ${agentJobs} where ${guard})`
41
+ const effects = []
42
+ if (input.backupRunId) {
43
+ effects.push(
44
+ store.run(sql`update ${backupRuns} set status = 'failed', error_message = ${input.message},
45
+ finished_at = ${now}, updated_at = ${now} where id = ${input.backupRunId}
46
+ and server_id = ${input.job.serverId} and status = ${input.job.status} and ${eligible}`),
47
+ )
48
+ }
49
+ for (let offset = 0; offset < input.cleanupRunIds.length; offset += 80) {
50
+ effects.push(
51
+ store.run(sql`update ${backupRuns} set cleanup_queued_at = null,
52
+ cleanup_error_message = ${input.message}, updated_at = ${now}
53
+ where id in (select value from json_each(${JSON.stringify(input.cleanupRunIds.slice(offset, offset + 80))}))
54
+ and server_id = ${input.job.serverId} and ${eligible}`),
55
+ )
56
+ }
57
+ const finish =
58
+ store.all(sql`update ${agentJobs} set status = 'failed', error_message = ${input.message},
59
+ finished_at = ${now}, updated_at = ${now} where ${guard} returning id`)
60
+ const [first, ...rest] = effects
61
+ if (!first) return (await finish).length === 1
62
+ const results = await store.batch([first, ...rest, finish])
63
+ const completed = results.at(-1)
64
+ return Array.isArray(completed) && completed.length === 1
65
+ }
66
+
67
+ export async function finishBackup(
68
+ store: AtomicStore,
69
+ input: {
70
+ serverId: string
71
+ completion: AgentBackupCompletion
72
+ errorMessage: string | null
73
+ now: Date
74
+ manifest: unknown
75
+ },
76
+ ) {
77
+ const { completion } = input
78
+ const now = input.now.getTime()
79
+ const guard = runningJob(completion.jobId, input.serverId, 'backup')
80
+ const artifactGuard = sql`${guard} and exists(select 1 from ${backupRuns}
81
+ where id = ${completion.backupRunId} and server_id = ${input.serverId} and status = 'running')`
82
+ const artifacts =
83
+ completion.status === 'succeeded'
84
+ ? (completion.artifacts ?? [])
85
+ : completion.status === 'failed' &&
86
+ typeof input.manifest === 'object' &&
87
+ input.manifest !== null &&
88
+ 'localRecoveryAvailable' in input.manifest &&
89
+ input.manifest.localRecoveryAvailable === true
90
+ ? (completion.artifacts ?? []).filter(
91
+ (artifact) => artifact.storage === 'local',
92
+ )
93
+ : []
94
+ const inserts = []
95
+ for (let offset = 0; offset < artifacts.length; offset += 8) {
96
+ const rows = artifacts.slice(offset, offset + 8).map(
97
+ (artifact) =>
98
+ sql`(${crypto.randomUUID()}, ${completion.backupRunId}, ${artifact.type}, ${artifact.storage},
99
+ ${artifact.objectKey}, ${artifact.checksumSha256}, ${artifact.sizeBytes}, ${now})`,
100
+ )
101
+ inserts.push(
102
+ store.run(sql`insert into ${backupArtifacts} (id, backup_run_id, type, storage, object_key, checksum_sha256, size_bytes, created_at)
103
+ select column1, column2, column3, column4, column5, column6, column7, column8
104
+ from (values ${sql.join(rows, sql`, `)}) where ${artifactGuard}
105
+ on conflict(backup_run_id, object_key) do nothing`),
106
+ )
107
+ }
108
+ const patch =
109
+ store.run(sql`update ${backupRuns} set status = ${completion.status},
110
+ manifest = json_set(json_patch(coalesce(manifest, '{}'), ${JSON.stringify(input.manifest ?? {})}), '$.databaseTargets', json(coalesce(json_extract(manifest, '$.databaseTargets'), '[]'))), error_message = ${input.errorMessage},
111
+ started_at = coalesce(started_at, ${now}), finished_at = ${now}, updated_at = ${now}
112
+ where id = ${completion.backupRunId} and server_id = ${input.serverId} and status = 'running' and ${guard}`)
113
+ const finish =
114
+ store.all(sql`update ${agentJobs} set status = ${completion.status}, error_message = ${input.errorMessage},
115
+ finished_at = ${now}, updated_at = ${now} where id = ${completion.jobId} and server_id = ${input.serverId}
116
+ and type = 'backup' and status = 'running' returning id`)
117
+ const [first, ...rest] = inserts
118
+ const results = first
119
+ ? await store.batch([first, ...rest, patch, finish])
120
+ : await store.batch([patch, finish])
121
+ const completed = results.at(-1)
122
+ return Array.isArray(completed) && completed.length === 1
123
+ }
124
+
125
+ export async function finishBackupCleanup(
126
+ store: AtomicStore,
127
+ input: {
128
+ serverId: string
129
+ completion: AgentBackupCleanupCompletion
130
+ errorMessage: string | null
131
+ now: Date
132
+ backupRunIds: string[]
133
+ localObjectKeys: string[]
134
+ remoteObjectKeys: string[]
135
+ },
136
+ ) {
137
+ const now = input.now.getTime()
138
+ const { completion } = input
139
+ const guard = runningJob(completion.jobId, input.serverId, 'backup_cleanup')
140
+ const deletes = []
141
+ if (completion.status === 'succeeded') {
142
+ for (const [storage, keys] of [
143
+ ['local', input.localObjectKeys],
144
+ ['remote', input.remoteObjectKeys],
145
+ ] as const) {
146
+ for (let offset = 0; offset < keys.length; offset += 80) {
147
+ deletes.push(
148
+ store.run(sql`delete from ${backupArtifacts}
149
+ where backup_run_id in (select value from json_each(${JSON.stringify(input.backupRunIds)}))
150
+ and storage = ${storage} and object_key in (select value from json_each(${JSON.stringify(keys.slice(offset, offset + 80))}))
151
+ and ${guard} and exists(select 1 from ${backupRuns} as run
152
+ where run.id = ${backupArtifacts.backupRunId} and run.server_id = ${input.serverId})`),
153
+ )
154
+ }
155
+ }
156
+ }
157
+ const patches = []
158
+ for (let offset = 0; offset < input.backupRunIds.length; offset += 80) {
159
+ patches.push(
160
+ store.run(sql`update ${backupRuns} set cleanup_queued_at = null,
161
+ cleanup_error_message = ${input.errorMessage}, updated_at = ${now}
162
+ ${completion.status === 'succeeded' ? sql`, cleanup_finished_at = ${now}` : sql``}
163
+ where id in (select value from json_each(${JSON.stringify(input.backupRunIds.slice(offset, offset + 80))}))
164
+ and server_id = ${input.serverId} and ${guard}`),
165
+ )
166
+ }
167
+ const finish =
168
+ store.all(sql`update ${agentJobs} set status = ${completion.status}, error_message = ${input.errorMessage},
169
+ finished_at = ${now}, updated_at = ${now} where id = ${completion.jobId} and server_id = ${input.serverId}
170
+ and type = 'backup_cleanup' and status = 'running' returning id`)
171
+ const [first, ...rest] = [...deletes, ...patches]
172
+ if (!first) return (await finish).length === 1
173
+ const results = await store.batch([first, ...rest, finish])
174
+ const completed = results.at(-1)
175
+ return Array.isArray(completed) && completed.length === 1
176
+ }
177
+
178
+ export async function failClaimedAgentJob(
179
+ store: AtomicStore,
180
+ input: {
181
+ job: typeof agentJobs.$inferSelect
182
+ now: Date
183
+ message: string
184
+ backupRunId: string | null
185
+ cleanupRunIds: string[]
186
+ },
187
+ ) {
188
+ const job = input.job,
189
+ now = input.now.getTime()
190
+ const guard = sql`id = ${job.id} and server_id = ${job.serverId} and type = ${job.type} and status = 'running'
191
+ and attempt = ${job.attempt} and claimed_at is ${job.claimedAt?.getTime() ?? null} and updated_at = ${job.updatedAt.getTime()}`
192
+ const eligible = sql`exists(select 1 from agent_jobs where ${guard})`
193
+ const effects = []
194
+ if (input.backupRunId)
195
+ effects.push(
196
+ store.run(sql`update backup_runs set status = 'failed', error_message = ${input.message}, finished_at = ${now}, updated_at = ${now}
197
+ where id = ${input.backupRunId} and project_id = ${job.projectId} and server_id = ${job.serverId} and status = 'running' and ${eligible}`),
198
+ )
199
+ if (input.cleanupRunIds.length)
200
+ effects.push(
201
+ store.run(sql`update backup_runs set cleanup_queued_at = null, cleanup_error_message = ${input.message}, updated_at = ${now}
202
+ where id in (select value from json_each(${JSON.stringify(input.cleanupRunIds)})) and project_id = ${job.projectId} and server_id = ${job.serverId} and ${eligible}`),
203
+ )
204
+ const finish = store.run(
205
+ sql`update agent_jobs set status = 'failed', error_message = ${input.message}, finished_at = ${now}, updated_at = ${now} where ${guard}`,
206
+ )
207
+ const [first, ...rest] = effects
208
+ if (first) await store.batch([first, ...rest, finish])
209
+ else await finish
210
+ }
@@ -0,0 +1,96 @@
1
+ import { z } from 'zod'
2
+ import { getPluginServices } from '#devpush/server/plugins/services.server'
3
+ import { pluginContext } from '#devpush/server/plugins/server-context'
4
+ import { requireAgentServer } from '#devpush/server/servers/service'
5
+ import { AgentRequestError } from './protocol'
6
+
7
+ export const certificateRequestSchema = z.strictObject({
8
+ serverId: z.string().min(1).max(128),
9
+ token: z.string().min(1).max(1024),
10
+ csrPem: z.string().min(1).max(16384).optional(),
11
+ installedCertificateSha256: z
12
+ .string()
13
+ .regex(/^[a-f0-9]{64}$/)
14
+ .optional(),
15
+ })
16
+
17
+ const resultSchema = z.discriminatedUnion('status', [
18
+ z.strictObject({
19
+ status: z.literal('configuration'),
20
+ dnsNames: z
21
+ .array(
22
+ z
23
+ .string()
24
+ .max(253)
25
+ .regex(
26
+ /^\*\.(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/,
27
+ ),
28
+ )
29
+ .length(1),
30
+ }),
31
+ z.strictObject({
32
+ status: z.literal('pending'),
33
+ retryAfter: z.number().int().min(1).max(3600),
34
+ }),
35
+ z.strictObject({
36
+ status: z.literal('ready'),
37
+ certificatePem: z
38
+ .string()
39
+ .min(1)
40
+ .max(65536)
41
+ .regex(
42
+ /^(?:-----BEGIN CERTIFICATE-----\r?\n[A-Za-z0-9+/=\r\n]+\r?\n-----END CERTIFICATE-----\s*)+$/,
43
+ ),
44
+ expiresAt: z.iso.datetime(),
45
+ }),
46
+ ])
47
+
48
+ export async function requestAgentCertificate(
49
+ input: z.infer<typeof certificateRequestSchema>,
50
+ ) {
51
+ await requireAgentServer({ serverId: input.serverId, token: input.token })
52
+ const services = getPluginServices()
53
+ if (!services.certificates)
54
+ throw new AgentRequestError(404, 'Managed certificates are not configured.')
55
+ const server = await pluginContext.readServer(input.serverId)
56
+ if (
57
+ !server ||
58
+ server.status === 'deleted' ||
59
+ server.status === 'disabled' ||
60
+ server.teamStatus !== 'active'
61
+ )
62
+ throw new AgentRequestError(403, 'Server is not active.')
63
+ try {
64
+ await services.serverLifecycle?.reconcile(server)
65
+ const result = resultSchema.parse(
66
+ await services.certificates.request({
67
+ server,
68
+ csrPem: input.csrPem,
69
+ ...(input.installedCertificateSha256
70
+ ? { installedCertificateSha256: input.installedCertificateSha256 }
71
+ : {}),
72
+ }),
73
+ )
74
+ return Response.json(
75
+ result,
76
+ result.status === 'pending'
77
+ ? {
78
+ status: 202,
79
+ headers: {
80
+ 'Retry-After': String(result.retryAfter),
81
+ 'Cache-Control': 'no-store',
82
+ },
83
+ }
84
+ : { headers: { 'Cache-Control': 'no-store' } },
85
+ )
86
+ } catch (error) {
87
+ console.error('Agent certificate provider failed.', {
88
+ serverId: server.id,
89
+ error: error instanceof Error ? error.message : 'Unknown error',
90
+ })
91
+ throw new AgentRequestError(
92
+ 503,
93
+ 'Certificate service is temporarily unavailable.',
94
+ )
95
+ }
96
+ }
@@ -0,0 +1,116 @@
1
+ import { and, eq, gt, sql } from 'drizzle-orm'
2
+ import type { BaseSQLiteDatabase } from 'drizzle-orm/sqlite-core'
3
+ import { config } from '#devpush/server/config'
4
+ import * as schema from '#devpush/server/db/schema'
5
+ import { isUniqueConstraintError } from '#devpush/server/db/errors'
6
+ import type { AtomicStore } from '#devpush/server/db/atomic'
7
+
8
+ type Store = Pick<BaseSQLiteDatabase<'async', unknown, typeof schema>, 'update'>
9
+
10
+ export async function claimDeployment(
11
+ store: Store,
12
+ serverId: string,
13
+ now: Date,
14
+ ) {
15
+ const { deployments } = schema
16
+ const runtimeAvailable = sql`not exists(select 1 from ${deployments} as running
17
+ where running.runtime_id = ${deployments.runtimeId} and running.status = 'running')`
18
+ try {
19
+ const [claimed] = await store
20
+ .update(deployments)
21
+ .set({ status: 'running', phase: 'agent_connected', startedAt: now })
22
+ .where(
23
+ and(
24
+ eq(deployments.status, 'queued'),
25
+ gt(
26
+ deployments.createdAt,
27
+ new Date(
28
+ now.getTime() -
29
+ config.DEVPUSH_DEPLOYMENT_QUEUE_TTL_HOURS * 3600000,
30
+ ),
31
+ ),
32
+ runtimeAvailable,
33
+ sql`${deployments.id} = (select id from ${deployments}
34
+ where server_id = ${serverId} and status = 'queued' and created_at > ${now.getTime() - config.DEVPUSH_DEPLOYMENT_QUEUE_TTL_HOURS * 3600000} and ${runtimeAvailable}
35
+ order by created_at, id limit 1)`,
36
+ ),
37
+ )
38
+ .returning()
39
+ return claimed ?? null
40
+ } catch (error) {
41
+ if (isUniqueConstraintError(error)) return null
42
+ throw error
43
+ }
44
+ }
45
+
46
+ export async function claimCleanup(store: Store, serverId: string, now: Date) {
47
+ const { agentJobs } = schema
48
+ const [claimed] = await store
49
+ .update(agentJobs)
50
+ .set({
51
+ status: 'running',
52
+ attempt: sql`${agentJobs.attempt} + 1`,
53
+ claimedAt: now,
54
+ updatedAt: now,
55
+ })
56
+ .where(
57
+ and(
58
+ eq(agentJobs.status, 'queued'),
59
+ sql`${agentJobs.id} = (select id from ${agentJobs} where server_id = ${serverId}
60
+ and type in ('deprovision', 'purge') and status = 'queued' and available_at <= ${now.getTime()}
61
+ order by created_at, id limit 1)`,
62
+ ),
63
+ )
64
+ .returning()
65
+ return claimed ?? null
66
+ }
67
+
68
+ export async function claimBackupCandidate(
69
+ store: Store & Pick<AtomicStore, 'batch'>,
70
+ input: {
71
+ job: typeof schema.agentJobs.$inferSelect
72
+ payloadSnapshot: string
73
+ backupRunId: string | null
74
+ now: Date
75
+ },
76
+ ) {
77
+ const { agentJobs, backupRuns } = schema
78
+ const guard = and(
79
+ eq(agentJobs.id, input.job.id),
80
+ eq(agentJobs.serverId, input.job.serverId),
81
+ eq(agentJobs.status, 'queued'),
82
+ eq(agentJobs.type, input.job.type),
83
+ sql`${agentJobs.payload} = ${input.payloadSnapshot}`,
84
+ eq(agentJobs.attempt, input.job.attempt),
85
+ eq(agentJobs.updatedAt, input.job.updatedAt),
86
+ sql`${agentJobs.availableAt} <= ${input.now.getTime()}`,
87
+ )
88
+ const claim = store
89
+ .update(agentJobs)
90
+ .set({
91
+ status: 'running',
92
+ attempt: sql`${agentJobs.attempt} + 1`,
93
+ claimedAt: input.now,
94
+ updatedAt: input.now,
95
+ })
96
+ .where(guard)
97
+ .returning()
98
+ if (input.backupRunId) {
99
+ const [, [claimed]] = await store.batch([
100
+ store
101
+ .update(backupRuns)
102
+ .set({ status: 'running', startedAt: input.now, updatedAt: input.now })
103
+ .where(
104
+ and(
105
+ eq(backupRuns.id, input.backupRunId),
106
+ eq(backupRuns.status, 'queued'),
107
+ sql`exists(select 1 from ${agentJobs} where ${guard})`,
108
+ ),
109
+ ),
110
+ claim,
111
+ ])
112
+ return claimed ?? null
113
+ }
114
+ const [claimed] = await claim
115
+ return claimed ?? null
116
+ }