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,1211 @@
1
+ import {
2
+ withWorkspaceAccess,
3
+ checkWorkspaceAccess,
4
+ } from '#devpush/server/plugins/workspace-access'
5
+ import { pluginContext } from '#devpush/server/plugins/server-context'
6
+ import { getPluginServices } from '#devpush/server/plugins/services.server'
7
+ import { recoverStaleCleanupJobs } from '#devpush/server/agents/container-cleanup-state'
8
+ import { teamSettingsAccess } from '#devpush/server/settings-version'
9
+ import { PublicError } from '#devpush/lib/public-error'
10
+ import { recordAgentMetricSamples } from './metric-history'
11
+ import type { AgentMetricSample } from './metric-history-schema'
12
+ import { deleteServerState, changeServerEnabled } from './lifecycle'
13
+ import { createHash, randomBytes, randomUUID } from 'node:crypto'
14
+
15
+ import { and, desc, eq, gt, inArray, ne, or, sql } from 'drizzle-orm'
16
+
17
+ import { reconcileReportedRuntimes } from '#devpush/server/agents/reconciliation'
18
+ import type { ReportedRuntime } from '#devpush/server/agents/reconciliation'
19
+ import { getAgentUpdate } from '#devpush/server/agents/compatibility'
20
+ import type { AgentUpdate } from '#devpush/server/agents/compatibility'
21
+ import { AgentRequestError } from '#devpush/server/agents/protocol'
22
+ import { config, currentAgentVersion } from '#devpush/server/config'
23
+ import { db } from '#devpush/server/db'
24
+ import {
25
+ agentJobs,
26
+ aliases,
27
+ deployments,
28
+ environments,
29
+ projects,
30
+ servers,
31
+ } from '#devpush/server/db/schema'
32
+ import {
33
+ listServerMetricSeries,
34
+ downsampleServerMetricSeries,
35
+ listServerMonitoringSummaries,
36
+ recordServerHeartbeat,
37
+ } from '#devpush/server/servers/monitoring'
38
+ import type { ServerMonitoringSummary } from '#devpush/server/servers/monitoring'
39
+ import { requireTeam, requireTeamManager } from '#devpush/server/teams/service'
40
+ import { getEffectiveServerStatus } from '#devpush/server/servers/status'
41
+ import type { ServerStatus } from '#devpush/server/servers/status'
42
+
43
+ export type CreatedServer = {
44
+ id: string
45
+ name: string
46
+ status: 'pending'
47
+ installCommand: string
48
+ }
49
+
50
+ export type ServerInstallCommand = {
51
+ serverId: string
52
+ installCommand: string
53
+ }
54
+
55
+ export type ServerDetail = {
56
+ settingsVersion: number
57
+ id: string
58
+ name: string
59
+ status: ServerStatus
60
+ agentVersion: string | null
61
+ agentProtocolVersion: number | null
62
+ agentCapabilities: Array<string> | null
63
+ agentUpdate: AgentUpdate
64
+ publicAddress: string | null
65
+ publicIpv4?: string | null
66
+ publicIpv6?: string | null
67
+ lastSeenAt: string | null
68
+ hostname: string | null
69
+ operatingSystem: string | null
70
+ kernelVersion: string | null
71
+ architecture: string | null
72
+ cpuCoreCount: number | null
73
+ hostUptimeSeconds: number | null
74
+ monitoring: ServerMonitoringSummary
75
+ createdAt: string
76
+ updatedAt: string
77
+ }
78
+
79
+ export type ServerDeleteDiagnostic = {
80
+ serverId: string
81
+ serverName: string
82
+ projectCount: number
83
+ environmentCount: number
84
+ }
85
+
86
+ const serverMachineNamePattern = /^[A-Za-z0-9][A-Za-z0-9._-]*[A-Za-z0-9]$/
87
+
88
+ export type AgentRegistration = {
89
+ inactiveRedirects?: Array<{ id: string; host: string; https: boolean }>
90
+ managedCertificates?: boolean
91
+ metricsAcceptedThrough?: string
92
+ serverId: string
93
+ status: 'connected'
94
+ credential?: string
95
+ runtimeLogTargets?: Array<{
96
+ runtimeId: string
97
+ }>
98
+ }
99
+
100
+ export type HostDetailsInput = {
101
+ hostname?: string
102
+ operatingSystem?: string
103
+ kernelVersion?: string
104
+ architecture?: string
105
+ cpuCoreCount?: number
106
+ uptimeSeconds?: number
107
+ }
108
+
109
+ export async function requireTeamServer({
110
+ userId,
111
+ teamSlug,
112
+ serverId,
113
+ }: {
114
+ userId: string
115
+ teamSlug: string
116
+ serverId: string
117
+ }): Promise<ServerDetail> {
118
+ const team = await requireTeam(userId, teamSlug)
119
+ return requireServerForTeam({ teamId: team.id, serverId })
120
+ }
121
+
122
+ export async function requireServerForTeam({
123
+ teamId,
124
+ serverId,
125
+ }: {
126
+ teamId: string
127
+ serverId: string
128
+ }): Promise<ServerDetail> {
129
+ const rows = await db
130
+ .select({
131
+ settingsVersion: servers.settingsVersion,
132
+ id: servers.id,
133
+ name: servers.name,
134
+ status: servers.status,
135
+ agentVersion: servers.agentVersion,
136
+ agentProtocolVersion: servers.agentProtocolVersion,
137
+ agentCapabilities: servers.agentCapabilities,
138
+ publicAddress: servers.publicAddress,
139
+ publicIpv4: servers.publicIpv4,
140
+ publicIpv6: servers.publicIpv6,
141
+ lastSeenAt: servers.lastSeenAt,
142
+ hostname: servers.hostname,
143
+ operatingSystem: servers.operatingSystem,
144
+ kernelVersion: servers.kernelVersion,
145
+ architecture: servers.architecture,
146
+ cpuCoreCount: servers.cpuCoreCount,
147
+ hostUptimeSeconds: servers.hostUptimeSeconds,
148
+ createdAt: servers.createdAt,
149
+ updatedAt: servers.updatedAt,
150
+ })
151
+ .from(servers)
152
+ .where(
153
+ and(
154
+ eq(servers.id, serverId),
155
+ eq(servers.teamId, teamId),
156
+ ne(servers.status, 'deleted'),
157
+ ),
158
+ )
159
+ .limit(1)
160
+ const server = rows.at(0)
161
+
162
+ if (!server) {
163
+ throw new PublicError('SERVER_NOT_FOUND')
164
+ }
165
+
166
+ const now = new Date()
167
+ const monitoring = await listServerMonitoringSummaries(
168
+ [{ id: server.id, createdAt: server.createdAt, status: server.status }],
169
+ now,
170
+ )
171
+
172
+ return {
173
+ ...server,
174
+ status: getEffectiveServerStatus({
175
+ status: server.status,
176
+ lastSeenAt: server.lastSeenAt,
177
+ now,
178
+ }),
179
+ agentUpdate: server.lastSeenAt
180
+ ? getAgentUpdate({
181
+ agentVersion: server.agentVersion,
182
+ protocolVersion: server.agentProtocolVersion,
183
+ capabilities: server.agentCapabilities,
184
+ latestVersion: currentAgentVersion,
185
+ recommendedVersion: config.DEVPUSH_RECOMMENDED_AGENT_VERSION ?? null,
186
+ })
187
+ : { status: 'current', latestVersion: null, reason: null },
188
+ lastSeenAt: server.lastSeenAt?.toISOString() ?? null,
189
+ monitoring: monitoring.get(server.id) ?? {
190
+ availabilityPercent: null,
191
+ availabilityWindowMs: 0,
192
+ latestMetrics: null,
193
+ },
194
+ createdAt: server.createdAt.toISOString(),
195
+ updatedAt: server.updatedAt.toISOString(),
196
+ }
197
+ }
198
+
199
+ export async function requireServerMetricHistoryForTeam({
200
+ teamId,
201
+ serverId,
202
+ }: {
203
+ teamId: string
204
+ serverId: string
205
+ }) {
206
+ const [server] = await db
207
+ .select({ createdAt: servers.createdAt })
208
+ .from(servers)
209
+ .where(
210
+ and(
211
+ eq(servers.id, serverId),
212
+ eq(servers.teamId, teamId),
213
+ ne(servers.status, 'deleted'),
214
+ ),
215
+ )
216
+ .limit(1)
217
+ if (!server) throw new PublicError('SERVER_NOT_FOUND')
218
+ const now = new Date()
219
+ const startAt = new Date(
220
+ Math.max(server.createdAt.getTime(), now.getTime() - 24 * 60 * 60 * 1000),
221
+ )
222
+ return {
223
+ points: downsampleServerMetricSeries(
224
+ await listServerMetricSeries({ serverId, now, startAt }),
225
+ ),
226
+ window: { startedAt: startAt.toISOString(), endedAt: now.toISOString() },
227
+ }
228
+ }
229
+
230
+ export async function updateTeamServer({
231
+ settingsVersion,
232
+ userId,
233
+ teamSlug,
234
+ serverId,
235
+ name,
236
+ }: {
237
+ settingsVersion?: number
238
+ userId: string
239
+ teamSlug: string
240
+ serverId: string
241
+ name: string
242
+ }) {
243
+ const nextName = name.trim()
244
+ validateServerMachineName(nextName)
245
+ const team = await requireTeamManager(userId, teamSlug)
246
+ return withWorkspaceAccess(
247
+ team.id,
248
+ 'server.update',
249
+ async () => {
250
+ const existingRows = await db
251
+ .select({ id: servers.id, settingsVersion: servers.settingsVersion })
252
+ .from(servers)
253
+ .where(
254
+ and(
255
+ eq(servers.id, serverId),
256
+ eq(servers.teamId, team.id),
257
+ ne(servers.status, 'deleted'),
258
+ ),
259
+ )
260
+ .limit(1)
261
+
262
+ if (!existingRows.at(0)) {
263
+ throw new PublicError('SERVER_NOT_FOUND')
264
+ }
265
+
266
+ const expectedVersion =
267
+ settingsVersion ?? existingRows[0]!.settingsVersion
268
+ if (expectedVersion !== existingRows[0]!.settingsVersion)
269
+ throw new PublicError('SETTINGS_CONFLICT')
270
+
271
+ const duplicateRows = await db
272
+ .select({ id: servers.id })
273
+ .from(servers)
274
+ .where(
275
+ and(
276
+ eq(servers.teamId, team.id),
277
+ sql`lower(${servers.handle}) = lower(${nextName})`,
278
+ ne(servers.id, serverId),
279
+ ne(servers.status, 'deleted'),
280
+ ),
281
+ )
282
+ .limit(1)
283
+
284
+ if (duplicateRows.at(0)) {
285
+ throw new PublicError('SERVER_NAME_CONFLICT')
286
+ }
287
+
288
+ const changed = await db
289
+ .update(servers)
290
+ .set({
291
+ name: nextName,
292
+ handle: nextName,
293
+ settingsVersion: sql`${servers.settingsVersion} + 1`,
294
+ updatedAt: new Date(),
295
+ })
296
+ .where(
297
+ and(
298
+ eq(servers.id, serverId),
299
+ eq(servers.settingsVersion, expectedVersion),
300
+ ne(servers.status, 'deleted'),
301
+ teamSettingsAccess(team.id, userId),
302
+ ),
303
+ )
304
+ .returning({ settingsVersion: servers.settingsVersion })
305
+ if (!changed.length) throw new PublicError('SETTINGS_CONFLICT')
306
+ return changed[0]!
307
+ },
308
+ { kind: 'user', userId },
309
+ )
310
+ }
311
+
312
+ export async function disableTeamServer({
313
+ confirmationName,
314
+ userId,
315
+ teamSlug,
316
+ serverId,
317
+ }: {
318
+ confirmationName: string
319
+ userId: string
320
+ teamSlug: string
321
+ serverId: string
322
+ }) {
323
+ const team = await requireTeamManager(userId, teamSlug)
324
+ return withWorkspaceAccess(
325
+ team.id,
326
+ 'server.disable',
327
+ async () => {
328
+ const existingRows = await db
329
+ .select({ id: servers.id, name: servers.name })
330
+ .from(servers)
331
+ .where(
332
+ and(
333
+ eq(servers.id, serverId),
334
+ eq(servers.teamId, team.id),
335
+ ne(servers.status, 'deleted'),
336
+ ),
337
+ )
338
+ .limit(1)
339
+
340
+ const server = existingRows.at(0)
341
+
342
+ if (!server) {
343
+ throw new PublicError('SERVER_NOT_FOUND')
344
+ }
345
+
346
+ if (confirmationName !== server.name) {
347
+ throw new PublicError('SERVER_DISABLE_CONFIRMATION')
348
+ }
349
+
350
+ const now = new Date()
351
+ if (
352
+ !(await changeServerEnabled(db, {
353
+ userId,
354
+ teamId: team.id,
355
+ serverId,
356
+ enabled: false,
357
+ now,
358
+ }))
359
+ ) {
360
+ throw new PublicError('SERVER_STATE_CHANGED')
361
+ }
362
+ },
363
+ { kind: 'user', userId },
364
+ )
365
+ }
366
+
367
+ export async function enableTeamServer({
368
+ userId,
369
+ teamSlug,
370
+ serverId,
371
+ }: {
372
+ userId: string
373
+ teamSlug: string
374
+ serverId: string
375
+ }) {
376
+ const team = await requireTeamManager(userId, teamSlug)
377
+ return withWorkspaceAccess(
378
+ team.id,
379
+ 'server.enable',
380
+ async () => {
381
+ const existingRows = await db
382
+ .select({ id: servers.id, status: servers.status })
383
+ .from(servers)
384
+ .where(
385
+ and(
386
+ eq(servers.id, serverId),
387
+ eq(servers.teamId, team.id),
388
+ ne(servers.status, 'deleted'),
389
+ ),
390
+ )
391
+ .limit(1)
392
+
393
+ const server = existingRows.at(0)
394
+ if (!server) {
395
+ throw new PublicError('SERVER_NOT_FOUND')
396
+ }
397
+
398
+ if (server.status !== 'disabled') {
399
+ throw new PublicError('SERVER_ENABLE_DISABLED_ONLY')
400
+ }
401
+
402
+ const now = new Date()
403
+ if (
404
+ !(await changeServerEnabled(db, {
405
+ userId,
406
+ teamId: team.id,
407
+ serverId,
408
+ enabled: true,
409
+ now,
410
+ }))
411
+ ) {
412
+ throw new PublicError('SERVER_STATE_CHANGED')
413
+ }
414
+ },
415
+ { kind: 'user', userId },
416
+ )
417
+ }
418
+
419
+ export async function createPendingServer({
420
+ userId,
421
+ teamSlug,
422
+ name,
423
+ }: {
424
+ userId: string
425
+ teamSlug: string
426
+ name: string
427
+ }): Promise<CreatedServer> {
428
+ const nextName = name.trim()
429
+ validateServerMachineName(nextName)
430
+ const team = await requireTeamManager(userId, teamSlug)
431
+ const duplicateRows = await db
432
+ .select({ id: servers.id })
433
+ .from(servers)
434
+ .where(
435
+ and(
436
+ eq(servers.teamId, team.id),
437
+ sql`lower(${servers.handle}) = lower(${nextName})`,
438
+ ne(servers.status, 'deleted'),
439
+ ),
440
+ )
441
+ .limit(1)
442
+
443
+ if (duplicateRows.at(0)) {
444
+ throw new PublicError('SERVER_NAME_CONFLICT')
445
+ }
446
+
447
+ const serverId = randomUUID()
448
+ const enrollmentToken = `dps_${randomBytes(32).toString('base64url')}`
449
+ const enrollmentTokenHash = hashToken(enrollmentToken)
450
+ const enrollmentTokenExpiresAt = new Date(Date.now() + 60 * 60 * 1000)
451
+
452
+ const now = new Date()
453
+ await withWorkspaceAccess(
454
+ team.id,
455
+ 'server.create',
456
+ async () => {
457
+ const [created] = await db.batch([
458
+ db.all(sql`insert into servers (id,team_id,name,handle,status,enrollment_token_hash,enrollment_token_expires_at,created_at,updated_at)
459
+ select ${serverId},${team.id},${nextName},${nextName},'pending',${enrollmentTokenHash},${enrollmentTokenExpiresAt.getTime()},${now.getTime()},${now.getTime()}
460
+ where ${teamSettingsAccess(team.id, userId)} and exists(select 1 from team_members where team_id=${team.id} and user_id=${userId} and role=${team.role}) returning id`),
461
+ db.run(sql`insert into server_availability_windows (id,server_id,started_at)
462
+ select ${randomUUID()},${serverId},${now.getTime()} where changes()=1`),
463
+ ])
464
+ if (!created.length) throw new PublicError('FORBIDDEN')
465
+ },
466
+ { kind: 'user', userId },
467
+ )
468
+
469
+ return {
470
+ id: serverId,
471
+ name: nextName,
472
+ status: 'pending',
473
+ installCommand: buildInstallCommand({
474
+ baseUrl: config.DEVPUSH_BASE_URL,
475
+ token: enrollmentToken,
476
+ }),
477
+ }
478
+ }
479
+
480
+ export async function createServerInstallCommand({
481
+ userId,
482
+ teamSlug,
483
+ serverId,
484
+ }: {
485
+ userId: string
486
+ teamSlug: string
487
+ serverId: string
488
+ }): Promise<ServerInstallCommand> {
489
+ const team = await requireTeamManager(userId, teamSlug)
490
+ return withWorkspaceAccess(
491
+ team.id,
492
+ 'server.enroll',
493
+ async () => {
494
+ const serverRows = await db
495
+ .select({
496
+ settingsVersion: servers.settingsVersion,
497
+ id: servers.id,
498
+ status: servers.status,
499
+ lastSeenAt: servers.lastSeenAt,
500
+ })
501
+ .from(servers)
502
+ .where(
503
+ and(
504
+ eq(servers.id, serverId),
505
+ eq(servers.teamId, team.id),
506
+ ne(servers.status, 'deleted'),
507
+ ),
508
+ )
509
+ .limit(1)
510
+ const server = serverRows.at(0)
511
+
512
+ if (!server) {
513
+ throw new PublicError('SERVER_NOT_FOUND')
514
+ }
515
+
516
+ if (server.status === 'disabled') {
517
+ throw new PublicError('SERVER_DISABLED_ENROLLMENT')
518
+ }
519
+
520
+ if (server.status === 'deleted') {
521
+ throw new PublicError('SERVER_DELETED_ENROLLMENT')
522
+ }
523
+
524
+ if (
525
+ getEffectiveServerStatus({
526
+ status: server.status,
527
+ lastSeenAt: server.lastSeenAt,
528
+ }) === 'connected'
529
+ ) {
530
+ throw new PublicError('SERVER_CONNECTED_ENROLLMENT')
531
+ }
532
+
533
+ const enrollmentToken = `dps_${randomBytes(32).toString('base64url')}`
534
+ const enrollmentTokenHash = hashToken(enrollmentToken)
535
+ const enrollmentTokenExpiresAt = new Date(Date.now() + 60 * 60 * 1000)
536
+ const now = new Date()
537
+
538
+ const changed = await db
539
+ .update(servers)
540
+ .set({
541
+ enrollmentTokenHash,
542
+ enrollmentTokenExpiresAt,
543
+ updatedAt: now,
544
+ })
545
+ .where(
546
+ and(
547
+ eq(servers.id, server.id),
548
+ eq(servers.teamId, team.id),
549
+ ne(servers.status, 'deleted'),
550
+ ne(servers.status, 'disabled'),
551
+ teamSettingsAccess(team.id, userId),
552
+ eq(servers.settingsVersion, server.settingsVersion),
553
+ sql`(${servers.agentCredentialHash} is null or ${servers.status} <> 'connected')`,
554
+ ),
555
+ )
556
+ .returning({ id: servers.id })
557
+ if (!changed.length) throw new PublicError('SERVER_STATE_CHANGED')
558
+
559
+ return {
560
+ serverId: server.id,
561
+ installCommand: buildInstallCommand({
562
+ baseUrl: config.DEVPUSH_BASE_URL,
563
+ token: enrollmentToken,
564
+ }),
565
+ }
566
+ },
567
+ { kind: 'user', userId },
568
+ )
569
+ }
570
+
571
+ export async function registerAgent({
572
+ token,
573
+ agentVersion,
574
+ protocolVersion,
575
+ capabilities,
576
+ publicAddress,
577
+ publicIpv4,
578
+ publicIpv6,
579
+ routingAddress,
580
+ }: {
581
+ token: string
582
+ agentVersion?: string | null
583
+ protocolVersion?: number
584
+ capabilities?: Array<string>
585
+ publicAddress: string | null
586
+ routingAddress?: string
587
+ publicIpv4?: string | null
588
+ publicIpv6?: string | null
589
+ }): Promise<AgentRegistration> {
590
+ const auth = await findServerForCredential(token)
591
+ const server = auth.server
592
+
593
+ if (server.status === 'deleted') {
594
+ throw new AgentRequestError(403, 'Server was deleted.')
595
+ }
596
+
597
+ if (auth.credentialType === 'enrollment')
598
+ await checkWorkspaceAccess(server.teamId, 'server.enroll', {
599
+ kind: 'system',
600
+ source: 'agent',
601
+ })
602
+
603
+ const now = new Date()
604
+ const credential =
605
+ auth.credentialType === 'enrollment'
606
+ ? `dpa_${randomBytes(32).toString('base64url')}`
607
+ : undefined
608
+ const updates = {
609
+ status: 'connected' as const,
610
+ ...(agentVersion ? { agentVersion } : {}),
611
+ ...(protocolVersion ? { agentProtocolVersion: protocolVersion } : {}),
612
+ ...(capabilities ? { agentCapabilities: capabilities } : {}),
613
+ ...mergePublicAddresses(server, { publicIpv4, publicIpv6, routingAddress }),
614
+ lastSeenAt: now,
615
+ ...(credential
616
+ ? { pendingAgentCredentialHash: hashToken(credential) }
617
+ : {}),
618
+ updatedAt: now,
619
+ }
620
+
621
+ const updated = await db
622
+ .update(servers)
623
+ .set(updates)
624
+ .where(
625
+ and(
626
+ eq(servers.id, server.id),
627
+ ne(servers.status, 'disabled'),
628
+ ne(servers.status, 'deleted'),
629
+ auth.credentialType === 'enrollment'
630
+ ? and(
631
+ eq(servers.enrollmentTokenHash, hashToken(token)),
632
+ gt(servers.enrollmentTokenExpiresAt, now),
633
+ )
634
+ : eq(
635
+ auth.credentialType === 'pending'
636
+ ? servers.pendingAgentCredentialHash
637
+ : servers.agentCredentialHash,
638
+ hashToken(token),
639
+ ),
640
+ ),
641
+ )
642
+ .returning({ id: servers.id })
643
+ if (!updated.length)
644
+ throw new AgentRequestError(403, 'Server state or credential changed.')
645
+ await recordServerHeartbeat({
646
+ serverId: server.id,
647
+ observedAt: now,
648
+ agentVersion: agentVersion ?? null,
649
+ publicAddress,
650
+ })
651
+
652
+ const inactiveRedirects = capabilities?.includes(
653
+ 'ingress.inactive-redirects.v1',
654
+ )
655
+ ? await db
656
+ .select({
657
+ id: deployments.id,
658
+ host: aliases.hostname,
659
+ https: aliases.https,
660
+ })
661
+ .from(deployments)
662
+ .innerJoin(
663
+ aliases,
664
+ and(
665
+ eq(aliases.deploymentId, deployments.id),
666
+ eq(aliases.type, 'deployment'),
667
+ ),
668
+ )
669
+ .innerJoin(environments, eq(environments.id, deployments.environmentId))
670
+ .innerJoin(
671
+ projects,
672
+ and(
673
+ eq(projects.id, environments.projectId),
674
+ eq(projects.status, 'active'),
675
+ ),
676
+ )
677
+ .where(
678
+ and(
679
+ eq(deployments.serverId, server.id),
680
+ eq(deployments.status, 'succeeded'),
681
+ ),
682
+ )
683
+ .orderBy(desc(deployments.createdAt))
684
+ .limit(1000)
685
+ : undefined
686
+ return {
687
+ inactiveRedirects,
688
+ serverId: server.id,
689
+ status: 'connected',
690
+ ...(credential ? { credential } : {}),
691
+ }
692
+ }
693
+
694
+ export async function recordAgentHeartbeat({
695
+ serverId,
696
+ token,
697
+ agentVersion,
698
+ protocolVersion,
699
+ capabilities,
700
+ publicAddress,
701
+ publicIpv4,
702
+ publicIpv6,
703
+ routingAddress,
704
+ runtimeIds,
705
+ runtimes,
706
+ system,
707
+ metricSamples,
708
+ }: {
709
+ serverId: string
710
+ token: string
711
+ agentVersion?: string | null
712
+ protocolVersion?: number
713
+ capabilities?: Array<string>
714
+ publicAddress: string | null
715
+ routingAddress?: string
716
+ publicIpv4?: string | null
717
+ publicIpv6?: string | null
718
+ runtimeIds?: Array<string>
719
+ runtimes?: Array<ReportedRuntime>
720
+ system?: HostDetailsInput
721
+ metricSamples?: Array<AgentMetricSample>
722
+ }): Promise<AgentRegistration> {
723
+ const auth = await requireAgentCredential(token)
724
+ const server = auth.server
725
+
726
+ if (server.id !== serverId) {
727
+ throw new AgentRequestError(401, 'Agent token does not match this server.')
728
+ }
729
+
730
+ const now = new Date()
731
+ if (server.status !== 'deleted') {
732
+ const updates = {
733
+ status: 'connected' as const,
734
+ ...(auth.credentialType === 'pending'
735
+ ? {
736
+ agentCredentialHash: server.pendingAgentCredentialHash,
737
+ pendingAgentCredentialHash: null,
738
+ enrollmentTokenHash: null,
739
+ enrollmentTokenExpiresAt: null,
740
+ agentCredentialRotatedAt: now,
741
+ }
742
+ : {}),
743
+ ...(agentVersion ? { agentVersion } : {}),
744
+ ...(protocolVersion ? { agentProtocolVersion: protocolVersion } : {}),
745
+ ...(capabilities ? { agentCapabilities: capabilities } : {}),
746
+ ...mergePublicAddresses(server, {
747
+ publicIpv4,
748
+ publicIpv6,
749
+ routingAddress,
750
+ }),
751
+ ...(system?.hostname ? { hostname: system.hostname } : {}),
752
+ ...(system?.operatingSystem
753
+ ? { operatingSystem: system.operatingSystem }
754
+ : {}),
755
+ ...(system?.kernelVersion ? { kernelVersion: system.kernelVersion } : {}),
756
+ ...(system?.architecture ? { architecture: system.architecture } : {}),
757
+ ...(system?.cpuCoreCount ? { cpuCoreCount: system.cpuCoreCount } : {}),
758
+ ...(system?.uptimeSeconds !== undefined
759
+ ? { hostUptimeSeconds: system.uptimeSeconds }
760
+ : {}),
761
+ lastSeenAt: now,
762
+ updatedAt: sql`case when ${servers.status} <> 'connected' or ${Number(auth.credentialType === 'pending')} = 1 then ${now.getTime()} else ${servers.updatedAt} end`,
763
+ }
764
+
765
+ const updated = await db
766
+ .update(servers)
767
+ .set(updates)
768
+ .where(
769
+ and(
770
+ eq(servers.id, server.id),
771
+ ne(servers.status, 'disabled'),
772
+ ne(servers.status, 'deleted'),
773
+ eq(
774
+ auth.credentialType === 'pending'
775
+ ? servers.pendingAgentCredentialHash
776
+ : servers.agentCredentialHash,
777
+ hashToken(token),
778
+ ),
779
+ ),
780
+ )
781
+ .returning({ id: servers.id })
782
+ if (!updated.length)
783
+ throw new AgentRequestError(403, 'Server state or credential changed.')
784
+ await recordServerHeartbeat({
785
+ serverId: server.id,
786
+ observedAt: now,
787
+ agentVersion: agentVersion ?? null,
788
+ publicAddress,
789
+ })
790
+ }
791
+
792
+ const observedRuntimeIds =
793
+ runtimeIds ?? runtimes?.map((runtime) => runtime.runtimeId)
794
+ if (observedRuntimeIds && server.status !== 'deleted') {
795
+ await reconcileReportedRuntimes({
796
+ serverId: server.id,
797
+ runtimeIds: observedRuntimeIds,
798
+ runtimes,
799
+ })
800
+ }
801
+
802
+ const metricsAcceptedThrough =
803
+ server.status === 'deleted'
804
+ ? undefined
805
+ : await recordAgentMetricSamples(server.id, metricSamples ?? [], now)
806
+
807
+ const deploymentServices = getPluginServices()
808
+ if (deploymentServices.serverLifecycle) {
809
+ const current = await pluginContext.readServer(server.id)
810
+ if (current) {
811
+ try {
812
+ await deploymentServices.serverLifecycle.reconcile(current)
813
+ } catch {
814
+ console.warn('Server address reconciliation is pending.', {
815
+ serverId: server.id,
816
+ })
817
+ }
818
+ }
819
+ }
820
+ return {
821
+ metricsAcceptedThrough,
822
+ ...(server.status !== 'deleted' && deploymentServices.certificates
823
+ ? { managedCertificates: true }
824
+ : {}),
825
+ serverId: server.id,
826
+ status: 'connected',
827
+ runtimeLogTargets:
828
+ server.status === 'deleted' ? [] : await listRuntimeLogTargets(server.id),
829
+ }
830
+ }
831
+
832
+ export async function rotateAgentCredential({
833
+ serverId,
834
+ token,
835
+ }: {
836
+ serverId: string
837
+ token: string
838
+ }): Promise<AgentRegistration> {
839
+ const auth = await requireAgentCredential(token)
840
+ const server = auth.server
841
+
842
+ if (server.id !== serverId) {
843
+ throw new AgentRequestError(
844
+ 401,
845
+ 'Agent credential does not match this server.',
846
+ )
847
+ }
848
+ if (server.status === 'deleted') {
849
+ throw new AgentRequestError(403, 'Server was deleted.')
850
+ }
851
+ const credential = `dpa_${randomBytes(32).toString('base64url')}`
852
+ const now = new Date()
853
+ const rotated = await db
854
+ .update(servers)
855
+ .set({
856
+ ...(auth.credentialType === 'pending'
857
+ ? {
858
+ agentCredentialHash: server.pendingAgentCredentialHash,
859
+ enrollmentTokenHash: null,
860
+ agentCredentialRotatedAt: now,
861
+ }
862
+ : {}),
863
+ pendingAgentCredentialHash: hashToken(credential),
864
+ updatedAt: now,
865
+ })
866
+ .where(
867
+ and(
868
+ eq(servers.id, server.id),
869
+ ne(servers.status, 'deleted'),
870
+ sql`agent_credential_hash is ${server.agentCredentialHash}`,
871
+ sql`pending_agent_credential_hash is ${server.pendingAgentCredentialHash}`,
872
+ ),
873
+ )
874
+ .returning({ id: servers.id })
875
+ if (rotated.length !== 1)
876
+ throw new AgentRequestError(
877
+ 409,
878
+ 'Agent credential changed. Retry rotation.',
879
+ )
880
+
881
+ return { serverId: server.id, status: 'connected', credential }
882
+ }
883
+
884
+ export async function getServerDeleteDiagnostic({
885
+ userId,
886
+ teamSlug,
887
+ serverId,
888
+ }: {
889
+ userId: string
890
+ teamSlug: string
891
+ serverId: string
892
+ }): Promise<ServerDeleteDiagnostic> {
893
+ const team = await requireTeamManager(userId, teamSlug)
894
+ const serverRows = await db
895
+ .select({
896
+ settingsVersion: servers.settingsVersion,
897
+ id: servers.id,
898
+ name: servers.name,
899
+ })
900
+ .from(servers)
901
+ .where(
902
+ and(
903
+ eq(servers.id, serverId),
904
+ eq(servers.teamId, team.id),
905
+ ne(servers.status, 'deleted'),
906
+ ),
907
+ )
908
+ .limit(1)
909
+ const server = serverRows.at(0)
910
+
911
+ if (!server) {
912
+ throw new PublicError('SERVER_NOT_FOUND')
913
+ }
914
+
915
+ const assignmentRows = await db
916
+ .select({
917
+ projectCount: sql<number>`count(distinct ${projects.id})`,
918
+ environmentCount: sql<number>`count(*)`,
919
+ })
920
+ .from(environments)
921
+ .innerJoin(projects, eq(projects.id, environments.projectId))
922
+ .where(
923
+ and(
924
+ eq(
925
+ sql`coalesce(${environments.serverId}, ${projects.defaultServerId})`,
926
+ server.id,
927
+ ),
928
+ eq(environments.status, 'active'),
929
+ eq(projects.teamId, team.id),
930
+ eq(projects.status, 'active'),
931
+ ),
932
+ )
933
+ const assignments = assignmentRows.at(0)
934
+
935
+ return {
936
+ serverId: server.id,
937
+ serverName: server.name,
938
+ projectCount: Number(assignments?.projectCount ?? 0),
939
+ environmentCount: Number(assignments?.environmentCount ?? 0),
940
+ }
941
+ }
942
+
943
+ export async function deleteTeamServer({
944
+ cancelOngoingDeployments,
945
+ confirmationName,
946
+ userId,
947
+ teamSlug,
948
+ serverId,
949
+ }: {
950
+ cancelOngoingDeployments: boolean
951
+ confirmationName: string
952
+ userId: string
953
+ teamSlug: string
954
+ serverId: string
955
+ }) {
956
+ const team = await requireTeamManager(userId, teamSlug)
957
+ const serverRows = await db
958
+ .select({
959
+ settingsVersion: servers.settingsVersion,
960
+ id: servers.id,
961
+ name: servers.name,
962
+ })
963
+ .from(servers)
964
+ .where(
965
+ and(
966
+ eq(servers.id, serverId),
967
+ eq(servers.teamId, team.id),
968
+ ne(servers.status, 'deleted'),
969
+ ),
970
+ )
971
+ .limit(1)
972
+ const server = serverRows.at(0)
973
+
974
+ if (!server) {
975
+ throw new PublicError('SERVER_NOT_FOUND')
976
+ }
977
+
978
+ if (confirmationName !== server.name) {
979
+ throw new PublicError('SERVER_DELETE_CONFIRMATION')
980
+ }
981
+
982
+ const now = new Date()
983
+
984
+ const changed = await withWorkspaceAccess(
985
+ team.id,
986
+ 'server.delete',
987
+ () =>
988
+ deleteServerState(db, {
989
+ userId,
990
+ teamId: team.id,
991
+ serverId: server.id,
992
+ cancelOngoingDeployments,
993
+ now,
994
+ }),
995
+ { kind: 'user', userId: userId },
996
+ )
997
+ if (!changed) throw new PublicError('SERVER_ACCESS_CHANGED')
998
+ }
999
+
1000
+ export async function requireAgentServer({
1001
+ cleanupJobId,
1002
+ serverId,
1003
+ token,
1004
+ }: {
1005
+ cleanupJobId?: string
1006
+ serverId: string
1007
+ token: string
1008
+ }) {
1009
+ const { server } = await requireAgentCredential(token, { cleanupJobId })
1010
+
1011
+ if (server.id !== serverId) {
1012
+ throw new AgentRequestError(401, 'Agent token does not match this server.')
1013
+ }
1014
+
1015
+ return server
1016
+ }
1017
+
1018
+ async function listRuntimeLogTargets(serverId: string) {
1019
+ const rows = await db
1020
+ .select({ runtimeId: environments.runtimeId })
1021
+ .from(environments)
1022
+ .innerJoin(projects, eq(projects.id, environments.projectId))
1023
+ .where(
1024
+ and(
1025
+ eq(
1026
+ sql`coalesce(${environments.serverId}, ${projects.defaultServerId})`,
1027
+ serverId,
1028
+ ),
1029
+ eq(environments.status, 'active'),
1030
+ eq(projects.status, 'active'),
1031
+ ),
1032
+ )
1033
+
1034
+ return Array.from(new Set(rows.map((row) => row.runtimeId)))
1035
+ .sort()
1036
+ .map((runtimeId) => ({ runtimeId }))
1037
+ }
1038
+
1039
+ function hashToken(token: string) {
1040
+ return createHash('sha256').update(token).digest('hex')
1041
+ }
1042
+
1043
+ function validateServerMachineName(name: string) {
1044
+ if (
1045
+ name.length === 0 ||
1046
+ name.length > 100 ||
1047
+ !serverMachineNamePattern.test(name)
1048
+ ) {
1049
+ throw new PublicError('SERVER_NAME_INVALID')
1050
+ }
1051
+ }
1052
+
1053
+ async function findServerForCredential(token: string) {
1054
+ const tokenHash = hashToken(token)
1055
+ const rows = await db
1056
+ .select({
1057
+ settingsVersion: servers.settingsVersion,
1058
+ id: servers.id,
1059
+ teamId: servers.teamId,
1060
+ status: servers.status,
1061
+ publicAddress: servers.publicAddress,
1062
+ publicIpv4: servers.publicIpv4,
1063
+ publicIpv6: servers.publicIpv6,
1064
+ enrollmentTokenHash: servers.enrollmentTokenHash,
1065
+ enrollmentTokenExpiresAt: servers.enrollmentTokenExpiresAt,
1066
+ agentCredentialHash: servers.agentCredentialHash,
1067
+ pendingAgentCredentialHash: servers.pendingAgentCredentialHash,
1068
+ })
1069
+ .from(servers)
1070
+ .where(
1071
+ or(
1072
+ and(
1073
+ eq(servers.enrollmentTokenHash, tokenHash),
1074
+ gt(servers.enrollmentTokenExpiresAt, new Date()),
1075
+ ),
1076
+ eq(servers.agentCredentialHash, tokenHash),
1077
+ eq(servers.pendingAgentCredentialHash, tokenHash),
1078
+ ),
1079
+ )
1080
+ .limit(1)
1081
+ const server = rows.at(0)
1082
+
1083
+ if (!server) {
1084
+ throw new AgentRequestError(401, 'Invalid or expired agent credential.')
1085
+ }
1086
+
1087
+ if (server.status === 'disabled') {
1088
+ throw new AgentRequestError(403, 'Server is disabled.')
1089
+ }
1090
+
1091
+ return {
1092
+ server,
1093
+ credentialType:
1094
+ server.enrollmentTokenHash === tokenHash
1095
+ ? ('enrollment' as const)
1096
+ : server.pendingAgentCredentialHash === tokenHash
1097
+ ? ('pending' as const)
1098
+ : ('active' as const),
1099
+ }
1100
+ }
1101
+
1102
+ async function requireAgentCredential(
1103
+ token: string,
1104
+ { cleanupJobId }: { cleanupJobId?: string } = {},
1105
+ ) {
1106
+ const auth = await findServerForCredential(token)
1107
+
1108
+ if (auth.credentialType === 'enrollment') {
1109
+ throw new AgentRequestError(
1110
+ 401,
1111
+ 'Enrollment tokens cannot authenticate agent requests.',
1112
+ )
1113
+ }
1114
+
1115
+ if (auth.server.status === 'deleted') {
1116
+ await recoverStaleCleanupJobs(db, {
1117
+ serverId: auth.server.id,
1118
+ cleanupJobId,
1119
+ now: new Date(),
1120
+ })
1121
+
1122
+ if (!(await hasPermittedDeletedServerWork(auth.server.id, cleanupJobId))) {
1123
+ throw new AgentRequestError(403, 'Server was deleted.')
1124
+ }
1125
+ }
1126
+
1127
+ return auth
1128
+ }
1129
+
1130
+ async function hasPermittedDeletedServerWork(
1131
+ serverId: string,
1132
+ cleanupJobId?: string,
1133
+ ) {
1134
+ const [deploymentRows, jobRows] = await Promise.all([
1135
+ db
1136
+ .select({ id: deployments.id })
1137
+ .from(deployments)
1138
+ .where(
1139
+ and(
1140
+ eq(deployments.serverId, serverId),
1141
+ inArray(deployments.status, ['queued', 'running']),
1142
+ ),
1143
+ )
1144
+ .limit(1),
1145
+ db
1146
+ .select({ id: agentJobs.id })
1147
+ .from(agentJobs)
1148
+ .where(
1149
+ and(
1150
+ eq(agentJobs.serverId, serverId),
1151
+ inArray(agentJobs.type, ['deprovision', 'purge']),
1152
+ or(
1153
+ inArray(agentJobs.status, ['queued', 'running']),
1154
+ cleanupJobId ? eq(agentJobs.id, cleanupJobId) : sql`0`,
1155
+ ),
1156
+ ),
1157
+ )
1158
+ .limit(1),
1159
+ ])
1160
+
1161
+ return deploymentRows.length > 0 || jobRows.length > 0
1162
+ }
1163
+
1164
+ function buildInstallCommand({
1165
+ baseUrl,
1166
+ token,
1167
+ }: {
1168
+ baseUrl: string
1169
+ token: string
1170
+ }) {
1171
+ const installUrl = `${baseUrl.replace(/\/$/, '')}/api/install/agent`
1172
+ const tokenInstallUrl = `${installUrl}/${encodeURIComponent(token)}`
1173
+
1174
+ return `(tmp="$(mktemp)" && curl -fsSL ${shellQuote(tokenInstallUrl)} -o "$tmp" && sudo bash "$tmp"; status=$?; test -z "\${tmp:-}" || rm -f "$tmp"; exit "$status")`
1175
+ }
1176
+
1177
+ function shellQuote(value: string) {
1178
+ return `'${value.replaceAll("'", "'\\''")}'`
1179
+ }
1180
+
1181
+ export function mergePublicAddresses(
1182
+ server: {
1183
+ publicAddress: string | null
1184
+ publicIpv4: string | null
1185
+ publicIpv6: string | null
1186
+ },
1187
+ reported: {
1188
+ publicIpv4?: string | null
1189
+ publicIpv6?: string | null
1190
+ routingAddress?: string
1191
+ },
1192
+ ) {
1193
+ if (reported.routingAddress) {
1194
+ return {
1195
+ publicAddress: reported.routingAddress,
1196
+ publicIpv4: reported.routingAddress.includes(':')
1197
+ ? null
1198
+ : reported.routingAddress,
1199
+ publicIpv6: reported.routingAddress.includes(':')
1200
+ ? reported.routingAddress
1201
+ : null,
1202
+ }
1203
+ }
1204
+ const publicIpv4 = reported.publicIpv4 ?? server.publicIpv4
1205
+ const publicIpv6 = reported.publicIpv6 ?? server.publicIpv6
1206
+ return {
1207
+ publicIpv4,
1208
+ publicIpv6,
1209
+ publicAddress: publicIpv4 ?? publicIpv6 ?? server.publicAddress,
1210
+ }
1211
+ }