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,52 @@
1
+ import { sql } from 'drizzle-orm'
2
+ import type { SQL } from 'drizzle-orm'
3
+
4
+ export type DeprovisionJobPayload = {
5
+ schemaVersion: 1
6
+ runtimeIds: string[]
7
+ removeVolumes: boolean
8
+ reason: string
9
+ }
10
+
11
+ export const cleanupVolumeRetentionMs = 7 * 24 * 60 * 60 * 1000
12
+
13
+ // Derive targets inside the caller's deletion batch. One noisy project cannot
14
+ // produce an unbounded agent payload; every target is kept in a chunk of 200.
15
+ export function runtimeCleanupInsert(input: {
16
+ projectIds: SQL
17
+ guard: SQL
18
+ projectId?: string
19
+ reason: string
20
+ now: Date
21
+ }) {
22
+ const now = input.now.getTime()
23
+ return sql`with targets as materialized (
24
+ select p.team_id, trim(coalesce(e.server_id, p.default_server_id)) as server_id,
25
+ trim(e.runtime_id) as runtime_id
26
+ from environments e join projects p on p.id = e.project_id
27
+ where p.id in (${input.projectIds}) and ${input.guard}
28
+ union
29
+ select p.team_id, trim(d.server_id), trim(d.runtime_id)
30
+ from deployments d join environments e on e.id = d.environment_id
31
+ join projects p on p.id = e.project_id
32
+ where p.id in (${input.projectIds}) and ${input.guard}
33
+ ), numbered as (
34
+ select *, (row_number() over (partition by team_id, server_id order by runtime_id) - 1) / 200 as chunk
35
+ from targets where server_id is not null and server_id <> '' and runtime_id <> ''
36
+ ), groups as (
37
+ select team_id, server_id, chunk, json_group_array(runtime_id) as runtime_ids
38
+ from (select * from numbered order by team_id, server_id, chunk, runtime_id)
39
+ group by team_id, server_id, chunk
40
+ ), kinds as (select 'deprovision' as type union all select 'purge')
41
+ insert into agent_jobs
42
+ (id, type, status, server_id, team_id, project_id, payload, max_attempts, available_at, created_at, updated_at)
43
+ select lower(hex(randomblob(16))), kinds.type, 'queued', groups.server_id, groups.team_id,
44
+ ${input.projectId ?? null},
45
+ json_object('schemaVersion', 1, 'runtimeIds', json(groups.runtime_ids),
46
+ 'removeVolumes', json(case when kinds.type = 'purge' then 'true' else 'false' end),
47
+ 'reason', case when kinds.type = 'purge'
48
+ then ${`${input.reason} Purge retained volumes after the retention window.`} else ${input.reason} end),
49
+ 10, case when kinds.type = 'purge' then ${now + cleanupVolumeRetentionMs} else ${now} end,
50
+ ${now}, ${now}
51
+ from groups cross join kinds`
52
+ }
@@ -0,0 +1,116 @@
1
+ export const currentAgentProtocolVersion = 1
2
+
3
+ export const currentAgentCapabilities = [
4
+ 'jobs.compose.deploy.v1',
5
+ 'jobs.compose.cleanup.v2',
6
+ 'jobs.backup.v1',
7
+ 'jobs.backup.archive.v1',
8
+ 'jobs.backup.cleanup.v1',
9
+ 'jobs.restore.v1',
10
+ 'runtime.logs.v1',
11
+ 'metrics.history.v1',
12
+ 'ingress.certificates.v1',
13
+ ] as const
14
+
15
+ export type AgentCapability = (typeof currentAgentCapabilities)[number]
16
+
17
+ export type AgentUpdate = {
18
+ status: 'current' | 'available' | 'recommended' | 'required'
19
+ latestVersion: string | null
20
+ reason: string | null
21
+ }
22
+
23
+ export function getAgentUpdate({
24
+ agentVersion,
25
+ capabilities,
26
+ latestVersion,
27
+ protocolVersion,
28
+ recommendedVersion,
29
+ }: {
30
+ agentVersion: string | null
31
+ capabilities: Array<string> | null
32
+ latestVersion: string | null
33
+ protocolVersion: number | null
34
+ recommendedVersion: string | null
35
+ }): AgentUpdate {
36
+ if (!canAgentRunDeployments({ capabilities, protocolVersion })) {
37
+ return {
38
+ status: 'required',
39
+ latestVersion,
40
+ reason:
41
+ 'This agent can connect, but it cannot run the current deployment protocol.',
42
+ }
43
+ }
44
+
45
+ if (!capabilities?.includes('jobs.compose.cleanup.v2')) {
46
+ return {
47
+ status: 'required',
48
+ latestVersion,
49
+ reason: 'Update this agent to run cleanup jobs with attempt protection.',
50
+ }
51
+ }
52
+
53
+ if (isOlderVersion(agentVersion, recommendedVersion) && recommendedVersion) {
54
+ return {
55
+ status: 'recommended',
56
+ latestVersion,
57
+ reason: `Agent ${recommendedVersion} or newer is recommended.`,
58
+ }
59
+ }
60
+
61
+ if (isOlderVersion(agentVersion, latestVersion)) {
62
+ return {
63
+ status: 'available',
64
+ latestVersion,
65
+ reason: latestVersion ? `Agent ${latestVersion} is available.` : null,
66
+ }
67
+ }
68
+
69
+ return { status: 'current', latestVersion, reason: null }
70
+ }
71
+
72
+ export function canAgentRunDeployments({
73
+ capabilities,
74
+ protocolVersion,
75
+ }: {
76
+ capabilities: Array<string> | null
77
+ protocolVersion: number | null
78
+ }) {
79
+ return (
80
+ protocolVersion === currentAgentProtocolVersion &&
81
+ (capabilities?.includes('jobs.compose.deploy.v1') ?? false)
82
+ )
83
+ }
84
+
85
+ export function hasAgentCapability(
86
+ capabilities: Array<string> | undefined,
87
+ capability: AgentCapability,
88
+ ) {
89
+ return capabilities?.includes(capability) ?? false
90
+ }
91
+
92
+ function isOlderVersion(current: string | null, target: string | null) {
93
+ const currentParts = parseVersion(current)
94
+ const targetParts = parseVersion(target)
95
+
96
+ if (!currentParts || !targetParts) {
97
+ return false
98
+ }
99
+
100
+ for (let index = 0; index < targetParts.length; index += 1) {
101
+ const currentPart = currentParts[index]
102
+ const targetPart = targetParts[index]
103
+ if (currentPart === undefined || targetPart === undefined) return false
104
+ const difference = currentPart - targetPart
105
+ if (difference !== 0) {
106
+ return difference < 0
107
+ }
108
+ }
109
+
110
+ return false
111
+ }
112
+
113
+ function parseVersion(value: string | null) {
114
+ const match = value?.match(/^v?(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/)
115
+ return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : null
116
+ }
@@ -0,0 +1,83 @@
1
+ import { notifyAfterAgentCompletion } from '#devpush/server/notifications/observe'
2
+ import {
3
+ completeBackupCleanupJob,
4
+ completeBackupJob,
5
+ completeCleanupJob,
6
+ completeDeploymentJob,
7
+ completeRestoreJob,
8
+ } from '#devpush/server/agents/jobs'
9
+ import type { CompleteAgentJobPayload } from './protocol'
10
+
11
+ export async function completeAgentJob(payload: CompleteAgentJobPayload) {
12
+ const result = await completeJob(payload)
13
+ await notifyAfterAgentCompletion(payload.serverId)
14
+ return result
15
+ }
16
+
17
+ async function completeJob(payload: CompleteAgentJobPayload) {
18
+ if (payload.type === 'deploy') {
19
+ return completeDeploymentJob({
20
+ serverId: payload.serverId,
21
+ token: payload.token,
22
+ completion: {
23
+ deploymentId: payload.deploymentId,
24
+ status: payload.status,
25
+ gitSha: payload.gitSha,
26
+ logStartSeq: payload.logStartSeq,
27
+ logs: payload.logs,
28
+ events: payload.events,
29
+ },
30
+ })
31
+ }
32
+
33
+ if (payload.type === 'backup') {
34
+ return completeBackupJob({
35
+ serverId: payload.serverId,
36
+ token: payload.token,
37
+ completion: {
38
+ jobId: payload.jobId,
39
+ backupRunId: payload.backupRunId,
40
+ status: payload.status,
41
+ message: payload.message,
42
+ manifest: payload.manifest,
43
+ artifacts: payload.artifacts,
44
+ },
45
+ })
46
+ }
47
+
48
+ if (payload.type === 'backup_cleanup') {
49
+ return completeBackupCleanupJob({
50
+ serverId: payload.serverId,
51
+ token: payload.token,
52
+ completion: {
53
+ jobId: payload.jobId,
54
+ backupRunIds: payload.backupRunIds,
55
+ status: payload.status,
56
+ message: payload.message,
57
+ },
58
+ })
59
+ }
60
+
61
+ if (payload.type === 'restore') {
62
+ return completeRestoreJob({
63
+ serverId: payload.serverId,
64
+ token: payload.token,
65
+ completion: {
66
+ jobId: payload.jobId,
67
+ status: payload.status,
68
+ message: payload.message,
69
+ },
70
+ })
71
+ }
72
+
73
+ return completeCleanupJob({
74
+ serverId: payload.serverId,
75
+ token: payload.token,
76
+ completion: {
77
+ jobId: payload.jobId,
78
+ attempt: payload.attempt,
79
+ status: payload.status,
80
+ message: payload.message,
81
+ },
82
+ })
83
+ }
@@ -0,0 +1,51 @@
1
+ import { sql } from 'drizzle-orm'
2
+ import type { AtomicStore } from '#devpush/server/db/atomic'
3
+ import type { agentJobs } from '#devpush/server/db/schema'
4
+ import type { AgentCleanupCompletion } from './protocol'
5
+
6
+ export async function recoverStaleCleanupJobs(
7
+ store: AtomicStore,
8
+ input: {
9
+ serverId: string
10
+ cleanupJobId?: string
11
+ now: Date
12
+ },
13
+ ) {
14
+ const now = input.now.getTime()
15
+ const stale = sql`server_id = ${input.serverId} and type in ('deprovision','purge') and status = 'running'
16
+ and (claimed_at is null or claimed_at < ${now - 60 * 60 * 1000})
17
+ ${input.cleanupJobId ? sql`and id <> ${input.cleanupJobId}` : sql``}`
18
+ await store.batch([
19
+ store.run(sql`update agent_jobs set status = 'abandoned', error_message = 'Cleanup stopped reporting after its final attempt.',
20
+ finished_at = ${now},updated_at = ${now} where ${stale} and attempt >= max_attempts`),
21
+ store.run(sql`update agent_jobs set status = 'queued', error_message = 'Cleanup stopped reporting and will be retried.',
22
+ claimed_at = null,available_at = ${now},finished_at = null,updated_at = ${now} where ${stale} and attempt < max_attempts`),
23
+ ])
24
+ }
25
+
26
+ export async function finishContainerCleanup(
27
+ store: AtomicStore,
28
+ input: {
29
+ job: typeof agentJobs.$inferSelect
30
+ completion: AgentCleanupCompletion
31
+ now: Date
32
+ },
33
+ ) {
34
+ const job = input.job,
35
+ now = input.now.getTime()
36
+ const retry =
37
+ input.completion.status === 'failed' && job.attempt < job.maxAttempts
38
+ const status = retry
39
+ ? 'queued'
40
+ : input.completion.status === 'failed'
41
+ ? 'abandoned'
42
+ : input.completion.status
43
+ const rows = await store.all(sql`update agent_jobs set status = ${status},
44
+ error_message = ${input.completion.status === 'failed' ? (input.completion.message ?? 'Cleanup failed.') : null},
45
+ claimed_at = ${retry ? null : (job.claimedAt?.getTime() ?? null)},
46
+ available_at = ${retry ? now + 60_000 : job.availableAt.getTime()},finished_at = ${retry ? null : now},updated_at = ${now}
47
+ where id = ${job.id} and server_id = ${job.serverId} and type = ${job.type} and type in ('deprovision','purge')
48
+ and status = 'running' and attempt = ${input.completion.attempt} and attempt = ${job.attempt} and claimed_at is ${job.claimedAt?.getTime() ?? null}
49
+ and updated_at = ${job.updatedAt.getTime()} returning id`)
50
+ return rows.length === 1
51
+ }
@@ -0,0 +1,111 @@
1
+ import { and, eq, or, sql } from 'drizzle-orm'
2
+ import type { BaseSQLiteDatabase } from 'drizzle-orm/sqlite-core'
3
+ import type { AtomicStore } from '#devpush/server/db/atomic'
4
+ import { deployments } from '#devpush/server/db/schema'
5
+ import * as schema from '#devpush/server/db/schema'
6
+
7
+ export async function finishDeployment(
8
+ store: Pick<BaseSQLiteDatabase<'async', unknown, typeof schema>, 'update'> &
9
+ Pick<AtomicStore, 'batch'>,
10
+ input: {
11
+ deployment: typeof deployments.$inferSelect
12
+ projectId: string
13
+ status: 'succeeded' | 'failed' | 'canceled'
14
+ phase: string
15
+ gitSha: string
16
+ errorMessage: string | null
17
+ now: Date
18
+ },
19
+ ) {
20
+ const { aliases } = schema
21
+ const scope = and(
22
+ eq(deployments.id, input.deployment.id),
23
+ eq(deployments.serverId, input.deployment.serverId),
24
+ eq(deployments.status, 'running'),
25
+ )
26
+ const finish = store
27
+ .update(deployments)
28
+ .set({
29
+ status: input.status,
30
+ phase: input.phase,
31
+ gitSha: input.gitSha,
32
+ errorMessage: input.errorMessage,
33
+ finishedAt: input.now,
34
+ })
35
+ .where(scope)
36
+ .returning({ id: deployments.id })
37
+ if (input.status !== 'succeeded') return (await finish).length === 1
38
+ // Move aliases while the guarded deployment is still running, then finish
39
+ // it in the same batch. A terminal retry cannot move an alias a second time.
40
+ const [, completed] = await store.batch([
41
+ store
42
+ .update(aliases)
43
+ .set({
44
+ previousDeploymentId: sql`${aliases.deploymentId}`,
45
+ deploymentId: input.deployment.id,
46
+ updatedAt: input.now,
47
+ })
48
+ .where(
49
+ and(
50
+ eq(aliases.projectId, input.projectId),
51
+ eq(aliases.environmentId, input.deployment.environmentId),
52
+ or(
53
+ eq(aliases.type, 'environment'),
54
+ and(
55
+ eq(aliases.type, 'branch'),
56
+ eq(aliases.branch, input.deployment.gitBranch),
57
+ ),
58
+ ),
59
+ sql`exists(select 1 from ${deployments} where ${scope})`,
60
+ ),
61
+ ),
62
+ finish,
63
+ ])
64
+ return completed.length === 1
65
+ }
66
+
67
+ export async function failRunningDeployments(
68
+ store: AtomicStore,
69
+ input: {
70
+ ids: string[]
71
+ message: string
72
+ phase: string
73
+ now: Date
74
+ startedBefore?: Date
75
+ heartbeatBefore?: Date
76
+ },
77
+ ) {
78
+ const failedIds: string[] = []
79
+ for (let offset = 0; offset < input.ids.length; offset += 80) {
80
+ const ids = input.ids.slice(offset, offset + 80)
81
+ const guard = sql`${deployments.id} in (${sql.join(
82
+ ids.map((id) => sql`${id}`),
83
+ sql`, `,
84
+ )})
85
+ and ${deployments.status} = 'running'
86
+ ${input.startedBefore ? sql`and ${deployments.startedAt} < ${input.startedBefore.getTime()}` : sql``}
87
+ ${
88
+ input.heartbeatBefore
89
+ ? sql`and exists(select 1 from servers where servers.id = ${deployments.serverId}
90
+ and (servers.last_seen_at is null or servers.last_seen_at < ${input.heartbeatBefore.getTime()}))`
91
+ : sql``
92
+ }`
93
+ const [changed] = await store.batch([
94
+ store.all(sql`update ${deployments} set status = 'failed', phase = ${input.phase}, error_message = ${input.message},
95
+ finished_at = ${input.now.getTime()} where ${guard} returning id`),
96
+ ])
97
+ // Raw SQLite proxy results are arrays; D1 returns named objects. Normalize
98
+ // only this raw SQL boundary, without relying on either driver's shape.
99
+ for (const row of changed) {
100
+ const id = Array.isArray(row)
101
+ ? row[0]
102
+ : typeof row === 'object' && row !== null && 'id' in row
103
+ ? row.id
104
+ : undefined
105
+ if (typeof id !== 'string')
106
+ throw new Error('Invalid deployment mutation result.')
107
+ failedIds.push(id)
108
+ }
109
+ }
110
+ return failedIds
111
+ }
@@ -0,0 +1,159 @@
1
+ import type {
2
+ BackupCleanupJobPayload,
3
+ BackupJobPayload,
4
+ BackupRestoreJobPayload,
5
+ } from '#devpush/server/backups/service'
6
+ import type { DeprovisionJobPayload } from './cleanup'
7
+
8
+ export function parseDeprovisionPayload(
9
+ value: unknown,
10
+ ): DeprovisionJobPayload | null {
11
+ if (
12
+ typeof value !== 'object' ||
13
+ value === null ||
14
+ !('schemaVersion' in value) ||
15
+ !('runtimeIds' in value) ||
16
+ !('removeVolumes' in value) ||
17
+ !('reason' in value)
18
+ ) {
19
+ return null
20
+ }
21
+
22
+ const payload = value as Record<string, unknown>
23
+
24
+ if (
25
+ payload.schemaVersion !== 1 ||
26
+ !Array.isArray(payload.runtimeIds) ||
27
+ typeof payload.removeVolumes !== 'boolean' ||
28
+ typeof payload.reason !== 'string'
29
+ ) {
30
+ return null
31
+ }
32
+
33
+ const runtimeIds = payload.runtimeIds.filter(
34
+ (runtimeId): runtimeId is string =>
35
+ typeof runtimeId === 'string' && runtimeId.trim() !== '',
36
+ )
37
+
38
+ if (runtimeIds.length === 0) {
39
+ return null
40
+ }
41
+
42
+ return {
43
+ schemaVersion: 1,
44
+ runtimeIds: Array.from(new Set(runtimeIds)).sort(),
45
+ removeVolumes: payload.removeVolumes,
46
+ reason: payload.reason,
47
+ }
48
+ }
49
+
50
+ export function parseBackupPayload(value: unknown): BackupJobPayload | null {
51
+ if (typeof value !== 'object' || value === null) {
52
+ return null
53
+ }
54
+
55
+ const payload = value as Record<string, unknown>
56
+
57
+ if (
58
+ payload.schemaVersion !== 1 ||
59
+ typeof payload.backupRunId !== 'string' ||
60
+ typeof payload.artifactPrefix !== 'string' ||
61
+ typeof payload.localBasePath !== 'string' ||
62
+ typeof payload.projectId !== 'string' ||
63
+ typeof payload.environmentId !== 'string' ||
64
+ typeof payload.runtimeId !== 'string' ||
65
+ !isRecord(payload.destination) ||
66
+ !isRecord(payload.retention) ||
67
+ !isRecord(payload.volumes) ||
68
+ !Array.isArray(payload.databaseDumps)
69
+ ) {
70
+ return null
71
+ }
72
+
73
+ return payload as BackupJobPayload
74
+ }
75
+
76
+ export function parseBackupCleanupPayload(
77
+ value: unknown,
78
+ ): BackupCleanupJobPayload | null {
79
+ if (typeof value !== 'object' || value === null) {
80
+ return null
81
+ }
82
+
83
+ const payload = value as Record<string, unknown>
84
+
85
+ if (
86
+ payload.schemaVersion !== 1 ||
87
+ !Array.isArray(payload.backupRunIds) ||
88
+ !Array.isArray(payload.localObjectKeys) ||
89
+ !Array.isArray(payload.remoteObjectKeys) ||
90
+ typeof payload.reason !== 'string'
91
+ ) {
92
+ return null
93
+ }
94
+
95
+ if (payload.remoteObjectKeys.length > 0 && !payload.destination) {
96
+ return null
97
+ }
98
+
99
+ return payload as BackupCleanupJobPayload
100
+ }
101
+
102
+ export function parseRestorePayload(
103
+ value: unknown,
104
+ ): BackupRestoreJobPayload | null {
105
+ if (typeof value !== 'object' || value === null) {
106
+ return null
107
+ }
108
+
109
+ const payload = value as Record<string, unknown>
110
+
111
+ if (
112
+ payload.schemaVersion !== 1 ||
113
+ typeof payload.backupRunId !== 'string' ||
114
+ typeof payload.projectId !== 'string' ||
115
+ typeof payload.environmentId !== 'string' ||
116
+ typeof payload.runtimeId !== 'string' ||
117
+ !Array.isArray(payload.artifacts) ||
118
+ payload.artifacts.length === 0
119
+ ) {
120
+ return null
121
+ }
122
+
123
+ for (const artifact of payload.artifacts) {
124
+ if (
125
+ !isRecord(artifact) ||
126
+ typeof artifact.id !== 'string' ||
127
+ typeof artifact.objectKey !== 'string' ||
128
+ typeof artifact.checksumSha256 !== 'string' ||
129
+ (artifact.storage !== 'local' && artifact.storage !== 'remote')
130
+ ) {
131
+ return null
132
+ }
133
+
134
+ if (artifact.type === 'volume') {
135
+ if (typeof artifact.targetVolume !== 'string') {
136
+ return null
137
+ }
138
+ } else if (artifact.type === 'database') {
139
+ if (
140
+ typeof artifact.targetService !== 'string' ||
141
+ typeof artifact.restoreCommand !== 'string'
142
+ ) {
143
+ return null
144
+ }
145
+ } else {
146
+ return null
147
+ }
148
+
149
+ if (artifact.storage === 'remote' && !payload.destination) {
150
+ return null
151
+ }
152
+ }
153
+
154
+ return payload as BackupRestoreJobPayload
155
+ }
156
+
157
+ function isRecord(value: unknown): value is Record<string, unknown> {
158
+ return typeof value === 'object' && value !== null
159
+ }