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,569 @@
1
+ import { withWorkspaceAccess } from '#devpush/server/plugins/workspace-access'
2
+ import { sql } from 'drizzle-orm'
3
+ import { z } from 'zod'
4
+ import type { SQL } from 'drizzle-orm'
5
+ import {
6
+ createApiKeySchema,
7
+ apiRoleRank,
8
+ apiScopeDefinitions,
9
+ apiKeySettingsSchema,
10
+ apiKeyTeamsSchema,
11
+ apiKeyProjectsSchema,
12
+ apiKeyMaxExpirationDays,
13
+ scopesForApiPreset,
14
+ } from '#devpush/lib/api-key-schema'
15
+ import { PublicError } from '#devpush/lib/public-error'
16
+ import type {
17
+ ApiKeySummary,
18
+ ApiKeyTeamOption,
19
+ ApiKeyProjectOption,
20
+ ApiScope,
21
+ } from '#devpush/lib/api-key-schema'
22
+ import type { TeamRole } from '#devpush/lib/team-role'
23
+ import { db } from '#devpush/server/db'
24
+ import type { AtomicStore } from '#devpush/server/db/atomic'
25
+ import {
26
+ apiGrants,
27
+ apiKeys,
28
+ projects,
29
+ teamMembers,
30
+ teams,
31
+ user,
32
+ } from '#devpush/server/db/schema'
33
+ import type { ApiPrincipal } from './authorization'
34
+ import { apiAuthorizationCondition } from './authorization'
35
+ import { getResourceColor } from '#devpush/server/resources/color'
36
+
37
+ type CredentialRow = {
38
+ id: string
39
+ userId: string
40
+ teamId: string
41
+ membershipId: string
42
+ roleCeiling: TeamRole
43
+ projectIds: string | null
44
+ scopes: string
45
+ expiresAt: number | null
46
+ revokedAt: number | null
47
+ createdAt: number
48
+ providerGrantId?: string | null
49
+ }
50
+ type KeyRow = CredentialRow & {
51
+ name: string
52
+ prefix: string
53
+ preset: ApiKeySummary['preset']
54
+ lastUsedAt: number | null
55
+ teamName: string
56
+ }
57
+ type Member = { membershipId: string; role: TeamRole }
58
+
59
+ const credentialColumnNames = [
60
+ 'id',
61
+ 'userId',
62
+ 'teamId',
63
+ 'membershipId',
64
+ 'roleCeiling',
65
+ 'projectIds',
66
+ 'scopes',
67
+ 'expiresAt',
68
+ 'revokedAt',
69
+ 'createdAt',
70
+ ] as const
71
+ const keyColumnNames = [
72
+ ...credentialColumnNames,
73
+ 'name',
74
+ 'prefix',
75
+ 'preset',
76
+ 'lastUsedAt',
77
+ 'teamName',
78
+ ] as const
79
+
80
+ async function readRows<T>(
81
+ store: AtomicStore,
82
+ query: SQL,
83
+ columns: readonly string[],
84
+ ): Promise<T[]> {
85
+ const rows = await store.all(query)
86
+ // sqlite-proxy preserves positional rows; D1's raw SQL API returns records.
87
+ return rows.map(
88
+ (row) =>
89
+ (Array.isArray(row)
90
+ ? Object.fromEntries(
91
+ columns.map((column, index) => [column, row[index]]),
92
+ )
93
+ : row) as T,
94
+ )
95
+ }
96
+
97
+ const credentialColumns = sql`credential.id, credential.user_id as userId, credential.team_id as teamId,
98
+ credential.membership_id as membershipId, credential.role_ceiling as roleCeiling,
99
+ credential.project_ids as projectIds, credential.scopes, credential.expires_at as expiresAt,
100
+ credential.revoked_at as revokedAt, credential.created_at as createdAt`
101
+
102
+ function activeMemberGuard(
103
+ userId: string,
104
+ teamId: string,
105
+ membershipId: string,
106
+ role?: TeamRole,
107
+ ) {
108
+ return sql`exists(select 1 from ${teamMembers} as member
109
+ join ${user} as actor on actor.id = member.user_id
110
+ join ${teams} as member_team on member_team.id = member.team_id
111
+ where member.id = ${membershipId} and member.user_id = ${userId} and member.team_id = ${teamId}
112
+ and actor.status = 'active' and member_team.status = 'active'
113
+ ${role ? sql`and member.role = ${role}` : sql``})`
114
+ }
115
+
116
+ async function requireMember(
117
+ store: AtomicStore,
118
+ userId: string,
119
+ teamId: string,
120
+ ): Promise<Member> {
121
+ const [member] = await readRows<Member>(
122
+ store,
123
+ sql`select member.id as membershipId, member.role
124
+ from ${teamMembers} as member join ${user} as actor on actor.id = member.user_id
125
+ join ${teams} as member_team on member_team.id = member.team_id
126
+ where member.user_id = ${userId} and member.team_id = ${teamId}
127
+ and actor.status = 'active' and member_team.status = 'active' limit 1`,
128
+ ['membershipId', 'role'],
129
+ )
130
+ if (!member) throw new PublicError('FORBIDDEN')
131
+ return member
132
+ }
133
+
134
+ async function requireActiveUser(store: AtomicStore, userId: string) {
135
+ const rows = await store.all(
136
+ sql`select id from ${user} where id = ${userId} and status = 'active' limit 1`,
137
+ )
138
+ if (rows.length !== 1) throw new PublicError('UNAUTHORIZED')
139
+ }
140
+
141
+ function projectSelectionGuard(teamId: string, projectIds: string[] | null) {
142
+ return projectIds === null
143
+ ? sql`1`
144
+ : sql`(select count(*) from ${projects} where team_id = ${teamId} and status = 'active'
145
+ and id in (${sql.join(
146
+ projectIds.map((id) => sql`${id}`),
147
+ sql`, `,
148
+ )})) = ${projectIds.length}`
149
+ }
150
+
151
+ function principalFrom(
152
+ row: CredentialRow,
153
+ currentRole: TeamRole,
154
+ credentialKind: 'api_key' | 'oauth',
155
+ ): ApiPrincipal {
156
+ const roleCeiling =
157
+ apiRoleRank[currentRole] < apiRoleRank[row.roleCeiling]
158
+ ? currentRole
159
+ : row.roleCeiling
160
+ const storedScopes = JSON.parse(row.scopes) as string[]
161
+ return {
162
+ userId: row.userId,
163
+ teamId: row.teamId,
164
+ membershipId: row.membershipId,
165
+ roleCeiling,
166
+ projectIds: row.projectIds === null ? null : JSON.parse(row.projectIds),
167
+ scopes: storedScopes.filter(
168
+ (scope): scope is ApiScope =>
169
+ scope in apiScopeDefinitions &&
170
+ apiRoleRank[roleCeiling] >=
171
+ apiRoleRank[apiScopeDefinitions[scope as ApiScope].minimumRole],
172
+ ),
173
+ credentialId: row.id,
174
+ credentialKind,
175
+ providerGrantId: row.providerGrantId,
176
+ }
177
+ }
178
+
179
+ async function resolveCredential(
180
+ store: AtomicStore,
181
+ row: CredentialRow | undefined,
182
+ credentialKind: 'api_key' | 'oauth',
183
+ ) {
184
+ if (
185
+ !row ||
186
+ row.revokedAt !== null ||
187
+ (row.expiresAt === null
188
+ ? credentialKind === 'oauth'
189
+ : row.expiresAt <= Date.now())
190
+ )
191
+ throw new PublicError('UNAUTHORIZED')
192
+ let member: Member
193
+ try {
194
+ member = await requireMember(store, row.userId, row.teamId)
195
+ } catch (error) {
196
+ if (error instanceof PublicError) throw new PublicError('UNAUTHORIZED')
197
+ throw error
198
+ }
199
+ if (member.membershipId !== row.membershipId)
200
+ throw new PublicError('UNAUTHORIZED')
201
+ return principalFrom(row, member.role, credentialKind)
202
+ }
203
+
204
+ export async function hashApiSecret(secret: string) {
205
+ const digest = await crypto.subtle.digest(
206
+ 'SHA-256',
207
+ new TextEncoder().encode(secret),
208
+ )
209
+ return Array.from(new Uint8Array(digest), (byte) =>
210
+ byte.toString(16).padStart(2, '0'),
211
+ ).join('')
212
+ }
213
+
214
+ function newSecret() {
215
+ const bytes = crypto.getRandomValues(new Uint8Array(32))
216
+ return `dp_${Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('')}`
217
+ }
218
+
219
+ /** Public bearer authentication intentionally ignores browser cookies. */
220
+ export async function authenticateApiKey(
221
+ request: Request,
222
+ store: AtomicStore = db,
223
+ ): Promise<ApiPrincipal> {
224
+ const match = request.headers
225
+ .get('authorization')
226
+ ?.match(/^Bearer (dp_[a-f0-9]{64})$/i)
227
+ if (!match) throw new PublicError('UNAUTHORIZED')
228
+ const keyHash = await hashApiSecret(match[1]!)
229
+ const [row] = await readRows<CredentialRow>(
230
+ store,
231
+ sql`select ${credentialColumns} from ${apiKeys} as credential where key_hash = ${keyHash} limit 1`,
232
+ credentialColumnNames,
233
+ )
234
+ const principal = await resolveCredential(store, row, 'api_key')
235
+ const rows =
236
+ await store.all(sql`update ${apiKeys} set last_used_at = ${Date.now()} where id = ${principal.credentialId}
237
+ and ${apiAuthorizationCondition(principal)} returning id`)
238
+ if (rows.length !== 1) throw new PublicError('UNAUTHORIZED')
239
+ return principal
240
+ }
241
+
242
+ export async function resolveApiGrant(
243
+ grantId: string,
244
+ store: AtomicStore = db,
245
+ ): Promise<ApiPrincipal> {
246
+ const [row] = await readRows<CredentialRow>(
247
+ store,
248
+ sql`select ${credentialColumns}, credential.provider_grant_id as providerGrantId from ${apiGrants} as credential where id = ${grantId} limit 1`,
249
+ [...credentialColumnNames, 'providerGrantId'],
250
+ )
251
+ const principal = await resolveCredential(store, row, 'oauth')
252
+ const valid = await store.all(
253
+ sql`select 1 where ${apiAuthorizationCondition(principal)}`,
254
+ )
255
+ if (valid.length !== 1) throw new PublicError('UNAUTHORIZED')
256
+ return principal
257
+ }
258
+
259
+ function keySummary(row: KeyRow, available: boolean): ApiKeySummary {
260
+ return {
261
+ id: row.id,
262
+ name: row.name,
263
+ prefix: row.prefix,
264
+ teamId: row.teamId,
265
+ teamName: row.teamName,
266
+ projectIds: row.projectIds === null ? null : JSON.parse(row.projectIds),
267
+ preset: row.preset,
268
+ scopes: JSON.parse(row.scopes),
269
+ status:
270
+ row.revokedAt !== null
271
+ ? 'revoked'
272
+ : row.expiresAt !== null && row.expiresAt <= Date.now()
273
+ ? 'expired'
274
+ : available
275
+ ? 'active'
276
+ : 'unavailable',
277
+ createdAt: new Date(row.createdAt).toISOString(),
278
+ expiresAt:
279
+ row.expiresAt === null ? null : new Date(row.expiresAt).toISOString(),
280
+ lastUsedAt:
281
+ row.lastUsedAt === null ? null : new Date(row.lastUsedAt).toISOString(),
282
+ revokedAt:
283
+ row.revokedAt === null ? null : new Date(row.revokedAt).toISOString(),
284
+ }
285
+ }
286
+
287
+ async function readKey(store: AtomicStore, userId: string, id: string) {
288
+ const [row] = await readRows<KeyRow>(
289
+ store,
290
+ sql`select ${credentialColumns}, credential.name, credential.prefix, credential.preset,
291
+ credential.last_used_at as lastUsedAt, member_team.name as teamName
292
+ from ${apiKeys} as credential join ${teams} as member_team on member_team.id = credential.team_id
293
+ where credential.id = ${id} and credential.user_id = ${userId} limit 1`,
294
+ keyColumnNames,
295
+ )
296
+ if (!row) throw new PublicError('FORBIDDEN')
297
+ return row
298
+ }
299
+
300
+ export async function getApiKeySettings(
301
+ userId: string,
302
+ store: AtomicStore = db,
303
+ options: { page?: number } = {},
304
+ ): Promise<{
305
+ keys: ApiKeySummary[]
306
+ teams: ApiKeyTeamOption[]
307
+ pagination: {
308
+ page: number
309
+ pageSize: number
310
+ total: number
311
+ totalPages: number
312
+ }
313
+ }> {
314
+ await requireActiveUser(store, userId)
315
+ const requested = apiKeySettingsSchema.parse(options)
316
+ const [count] = await readRows<{ total: number }>(
317
+ store,
318
+ sql`select count(*) as total from ${apiKeys} where user_id = ${userId}`,
319
+ ['total'],
320
+ )
321
+ const pageSize = 25
322
+ const total = Number(count?.total ?? 0)
323
+ const totalPages = Math.max(1, Math.ceil(total / pageSize))
324
+ const page = Math.min(requested.page, totalPages)
325
+ const teamOptions = await getApiKeyTeams(userId, {}, store)
326
+ const keyRows = await readRows<KeyRow>(
327
+ store,
328
+ sql`select ${credentialColumns}, credential.name, credential.prefix, credential.preset,
329
+ credential.last_used_at as lastUsedAt, member_team.name as teamName
330
+ from ${apiKeys} as credential join ${teams} as member_team on member_team.id = credential.team_id
331
+ where credential.user_id = ${userId}
332
+ order by credential.revoked_at is not null, (credential.expires_at is not null and credential.expires_at <= ${Date.now()}), credential.created_at desc, credential.id
333
+ limit ${pageSize} offset ${(page - 1) * pageSize}`,
334
+ keyColumnNames,
335
+ )
336
+ const keys = keyRows.map((row) => keySummary(row, true))
337
+ return {
338
+ keys,
339
+ pagination: { page, pageSize, total, totalPages },
340
+ teams: teamOptions.items,
341
+ }
342
+ }
343
+
344
+ function searchPattern(query: string) {
345
+ return `%${query.replace(/[\\%_]/g, '\\$&')}%`
346
+ }
347
+ function pageInfo(total: number, requested: number) {
348
+ const pageSize = 25
349
+ const totalPages = Math.max(1, Math.ceil(total / pageSize))
350
+ return { page: Math.min(requested, totalPages), pageSize, total, totalPages }
351
+ }
352
+
353
+ export async function getApiKeyTeams(
354
+ userId: string,
355
+ input: unknown = {},
356
+ store: AtomicStore = db,
357
+ ) {
358
+ await requireActiveUser(store, userId)
359
+ const data = apiKeyTeamsSchema.parse(input)
360
+ const filter = sql`member.user_id = ${userId} and member_team.status = 'active'
361
+ and (member_team.name like ${searchPattern(data.query)} escape '\\' or member_team.slug like ${searchPattern(data.query)} escape '\\')`
362
+ const [count] = await readRows<{ total: number }>(
363
+ store,
364
+ sql`select count(*) as total from ${teamMembers} as member
365
+ join ${teams} as member_team on member_team.id = member.team_id where ${filter}`,
366
+ ['total'],
367
+ )
368
+ const pagination = pageInfo(Number(count?.total ?? 0), data.page)
369
+ const rows = await readRows<Omit<ApiKeyTeamOption, 'projects' | 'color'>>(
370
+ store,
371
+ sql`select member_team.id, member_team.name, member_team.slug, member.role, member_team.avatar_path as image
372
+ from ${teamMembers} as member join ${teams} as member_team on member_team.id = member.team_id
373
+ where ${filter} order by member_team.name, member_team.id limit ${pagination.pageSize} offset ${(pagination.page - 1) * pagination.pageSize}`,
374
+ ['id', 'name', 'slug', 'role', 'image'],
375
+ )
376
+ return {
377
+ items: rows.map((row) => ({
378
+ ...row,
379
+ color: getResourceColor(row.id),
380
+ projects: [],
381
+ })),
382
+ pagination,
383
+ }
384
+ }
385
+
386
+ export async function getApiKeyProjects(
387
+ userId: string,
388
+ input: unknown,
389
+ store: AtomicStore = db,
390
+ ) {
391
+ const data = apiKeyProjectsSchema.parse(input)
392
+ await requireMember(store, userId, data.teamId)
393
+ const filter = sql`${projects.teamId} = ${data.teamId} and ${projects.status} = 'active'
394
+ and (${projects.name} like ${searchPattern(data.query)} escape '\\' or ${projects.slug} like ${searchPattern(data.query)} escape '\\')`
395
+ const [count] = await readRows<{ total: number }>(
396
+ store,
397
+ sql`select count(*) as total from ${projects} where ${filter}`,
398
+ ['total'],
399
+ )
400
+ const pagination = pageInfo(Number(count?.total ?? 0), data.page)
401
+ const rows = await readRows<Omit<ApiKeyProjectOption, 'color'>>(
402
+ store,
403
+ sql`select ${projects.id} as id,${projects.name} as name,${projects.slug} as slug,${projects.avatarPath} as avatarUrl
404
+ from ${projects} where ${filter} order by ${projects.name}, ${projects.id} limit ${pagination.pageSize} offset ${(pagination.page - 1) * pagination.pageSize}`,
405
+ ['id', 'name', 'slug', 'avatarUrl'],
406
+ )
407
+ return {
408
+ items: rows.map((row) => ({ ...row, color: getResourceColor(row.id) })),
409
+ pagination,
410
+ }
411
+ }
412
+
413
+ export async function createApiKey(
414
+ userId: string,
415
+ input: unknown,
416
+ store: AtomicStore = db,
417
+ ) {
418
+ const data = createApiKeySchema.parse(input)
419
+ const member = await requireMember(store, userId, data.teamId)
420
+ return withWorkspaceAccess(
421
+ data.teamId,
422
+ 'credential.create',
423
+ async () => {
424
+ const scopes = scopesForApiPreset(
425
+ data.preset,
426
+ member.role,
427
+ data.includeLogs,
428
+ )
429
+ const id = crypto.randomUUID()
430
+ const secret = newSecret()
431
+ const keyHash = await hashApiSecret(secret)
432
+ const now = Date.now()
433
+ const expiresAt =
434
+ data.expiresInDays === null
435
+ ? null
436
+ : now + data.expiresInDays * 86_400_000
437
+ const inserted =
438
+ await store.all(sql`insert into ${apiKeys} (id,user_id,team_id,membership_id,name,prefix,key_hash,role_ceiling,preset,project_ids,scopes,expires_at,created_at)
439
+ select ${id},${userId},${data.teamId},${member.membershipId},${data.name},${secret.slice(0, 11)},${keyHash},${member.role},${data.preset},${data.projectIds === null ? null : JSON.stringify(data.projectIds)},${JSON.stringify(scopes)},${expiresAt},${now}
440
+ where ${activeMemberGuard(userId, data.teamId, member.membershipId, member.role)}
441
+ and ${projectSelectionGuard(data.teamId, data.projectIds)}
442
+ and (select count(*) from ${apiKeys} where user_id = ${userId} and revoked_at is null
443
+ and (expires_at is null or expires_at > cast(unixepoch('subsec') * 1000 as integer))) < 100
444
+ returning id`)
445
+ if (inserted.length !== 1)
446
+ throw new PublicError('API_KEY_CREATION_UNAVAILABLE')
447
+ return { key: keySummary(await readKey(store, userId, id), true), secret }
448
+ },
449
+ { kind: 'user', userId: userId },
450
+ )
451
+ }
452
+
453
+ export async function revokeApiKey(
454
+ userId: string,
455
+ id: string,
456
+ store: AtomicStore = db,
457
+ ) {
458
+ await requireActiveUser(store, userId)
459
+ const result =
460
+ await store.all(sql`update ${apiKeys} set revoked_at = coalesce(revoked_at,${Date.now()})
461
+ where id = ${id} and user_id = ${userId} and exists(select 1 from ${user} where id = ${userId} and status = 'active') returning id`)
462
+ if (result.length !== 1) throw new PublicError('FORBIDDEN')
463
+ }
464
+
465
+ export async function rotateApiKey(
466
+ userId: string,
467
+ id: string,
468
+ store: AtomicStore = db,
469
+ ) {
470
+ const old = await readKey(store, userId, id)
471
+ if (old.revokedAt !== null) throw new PublicError('API_KEY_REVOKED')
472
+ if (old.expiresAt !== null && old.expiresAt <= Date.now())
473
+ throw new PublicError('API_KEY_EXPIRED')
474
+ const member = await requireMember(store, userId, old.teamId)
475
+ return withWorkspaceAccess(
476
+ old.teamId,
477
+ 'credential.rotate',
478
+ async () => {
479
+ if (member.membershipId !== old.membershipId)
480
+ throw new PublicError('FORBIDDEN')
481
+ const principal = principalFrom(old, member.role, 'api_key')
482
+ const newId = crypto.randomUUID()
483
+ const secret = newSecret()
484
+ const keyHash = await hashApiSecret(secret)
485
+ const now = Date.now()
486
+ const expiresAt =
487
+ old.expiresAt === null
488
+ ? null
489
+ : now +
490
+ Math.min(
491
+ apiKeyMaxExpirationDays * 86_400_000,
492
+ Math.max(86_400_000, old.expiresAt - old.createdAt),
493
+ )
494
+ const result = await store.batch([
495
+ store.all(sql`insert into ${apiKeys} (id,user_id,team_id,membership_id,name,prefix,key_hash,role_ceiling,preset,project_ids,scopes,expires_at,created_at)
496
+ select ${newId},user_id,team_id,membership_id,name,${secret.slice(0, 11)},${keyHash},${principal.roleCeiling},preset,project_ids,${JSON.stringify(principal.scopes)},${expiresAt},${now}
497
+ from ${apiKeys} where id = ${id} and user_id = ${userId} and revoked_at is null
498
+ and (expires_at is null or expires_at > cast(unixepoch('subsec') * 1000 as integer))
499
+ and (select count(*) from ${apiKeys} where user_id = ${userId} and revoked_at is null
500
+ and (expires_at is null or expires_at > cast(unixepoch('subsec') * 1000 as integer))) <= 100
501
+ and ${activeMemberGuard(userId, old.teamId, old.membershipId, member.role)}
502
+ and ${projectSelectionGuard(old.teamId, principal.projectIds)} returning id`),
503
+ store.run(
504
+ sql`update ${apiKeys} set revoked_at = ${now} where id = ${id} and exists(select 1 from ${apiKeys} where id = ${newId})`,
505
+ ),
506
+ ])
507
+ if (result[0].length !== 1) throw new PublicError('FORBIDDEN')
508
+ return {
509
+ key: keySummary(await readKey(store, userId, newId), true),
510
+ secret,
511
+ }
512
+ },
513
+ { kind: 'user', userId: userId },
514
+ )
515
+ }
516
+
517
+ export async function createApiGrant(
518
+ input: {
519
+ userId: string
520
+ teamId: string
521
+ projectIds: string[] | null
522
+ preset: 'read' | 'deploy' | 'manage'
523
+ expiresInDays: number
524
+ includeLogs?: boolean
525
+ providerGrantId?: string
526
+ },
527
+ store: AtomicStore = db,
528
+ ) {
529
+ const data = createApiKeySchema
530
+ .safeExtend({ expiresInDays: z.number().int().min(1).max(90) })
531
+ .parse({
532
+ name: 'MCP connection',
533
+ teamId: input.teamId,
534
+ projectIds: input.projectIds,
535
+ preset: input.preset,
536
+ expiresInDays: input.expiresInDays,
537
+ includeLogs: input.includeLogs ?? false,
538
+ })
539
+ const member = await requireMember(store, input.userId, data.teamId)
540
+ return withWorkspaceAccess(
541
+ data.teamId,
542
+ 'credential.create',
543
+ async () => {
544
+ const id = crypto.randomUUID()
545
+ const now = Date.now()
546
+ const rows =
547
+ await store.all(sql`insert into ${apiGrants} (id,user_id,team_id,membership_id,role_ceiling,project_ids,scopes,provider_grant_id,expires_at,created_at)
548
+ select ${id},${input.userId},${data.teamId},${member.membershipId},${member.role},${data.projectIds === null ? null : JSON.stringify(data.projectIds)},${JSON.stringify(scopesForApiPreset(data.preset, member.role, data.includeLogs))},${input.providerGrantId ?? null},${now + data.expiresInDays * 86_400_000},${now}
549
+ where ${activeMemberGuard(input.userId, data.teamId, member.membershipId, member.role)} and ${projectSelectionGuard(data.teamId, data.projectIds)}
550
+ and (select count(*) from ${apiGrants} where user_id = ${input.userId} and revoked_at is null and expires_at > ${now}) < 100 returning id`)
551
+ if (rows.length !== 1)
552
+ throw new PublicError('MCP_CONNECTION_CREATION_UNAVAILABLE')
553
+ return resolveApiGrant(id, store)
554
+ },
555
+ { kind: 'user', userId: input.userId },
556
+ )
557
+ }
558
+
559
+ export async function revokeApiGrant(
560
+ userId: string,
561
+ grantId: string,
562
+ store: AtomicStore = db,
563
+ ) {
564
+ await requireActiveUser(store, userId)
565
+ const rows =
566
+ await store.all(sql`update ${apiGrants} set revoked_at = coalesce(revoked_at,${Date.now()}) where id = ${grantId} and user_id = ${userId}
567
+ and exists(select 1 from ${user} where id = ${userId} and status = 'active') returning id`)
568
+ if (rows.length !== 1) throw new PublicError('FORBIDDEN')
569
+ }
@@ -0,0 +1,72 @@
1
+ import { createHmac, timingSafeEqual } from 'node:crypto'
2
+ import { z } from 'zod'
3
+ import { config } from '#devpush/server/config'
4
+ import { ApiError } from './errors'
5
+
6
+ const cursorSchema = z.strictObject({
7
+ version: z.literal(1),
8
+ context: z.string(),
9
+ position: z.number().int().nonnegative(),
10
+ id: z.string().max(256),
11
+ })
12
+
13
+ export function encodeApiCursor(context: string, position: number, id = '') {
14
+ const payload = Buffer.from(
15
+ JSON.stringify({ version: 1, context, position, id }),
16
+ ).toString('base64url')
17
+ return `${payload}.${signature(payload)}`
18
+ }
19
+
20
+ export function decodeApiCursor(cursor: string | undefined, context: string) {
21
+ if (!cursor) return null
22
+ try {
23
+ if (cursor.length > 2048) throw new Error('length')
24
+ const [payload, signed, extra] = cursor.split('.')
25
+ if (!payload || !signed || extra) throw new Error('format')
26
+ const expected = Buffer.from(signature(payload))
27
+ const actual = Buffer.from(signed)
28
+ if (actual.length !== expected.length || !timingSafeEqual(actual, expected))
29
+ throw new Error('signature')
30
+ const value = cursorSchema.parse(
31
+ JSON.parse(Buffer.from(payload, 'base64url').toString('utf8')),
32
+ )
33
+ if (value.context !== context) throw new Error('context')
34
+ return value
35
+ } catch {
36
+ throw new ApiError('INVALID_CURSOR', 400)
37
+ }
38
+ }
39
+
40
+ export function apiFingerprint(value: unknown) {
41
+ return createHmac('sha256', fingerprintKey())
42
+ .update('devpush-api-fingerprint-v1\0')
43
+ .update(JSON.stringify(canonical(value)))
44
+ .digest('hex')
45
+ }
46
+
47
+ function signature(value: string) {
48
+ return createHmac('sha256', fingerprintKey())
49
+ .update('devpush-api-cursor-v1\0')
50
+ .update(value)
51
+ .digest('base64url')
52
+ }
53
+
54
+ function fingerprintKey() {
55
+ const value = config.DEVPUSH_SECRET_ENCRYPTION_KEY
56
+ if (!value)
57
+ throw new Error(
58
+ 'DEVPUSH_SECRET_ENCRYPTION_KEY is required for API cursor and replay protection.',
59
+ )
60
+ return value
61
+ }
62
+
63
+ function canonical(value: unknown): unknown {
64
+ if (Array.isArray(value)) return value.map(canonical)
65
+ if (value && typeof value === 'object')
66
+ return Object.fromEntries(
67
+ Object.entries(value)
68
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
69
+ .map(([key, item]) => [key, canonical(item)]),
70
+ )
71
+ return value
72
+ }
@@ -0,0 +1,24 @@
1
+ export const apiErrorMessages = {
2
+ NOT_FOUND: 'The resource was not found.',
3
+ IDEMPOTENCY_REQUIRED: 'Provide an Idempotency-Key for this operation.',
4
+ IDEMPOTENCY_CONFLICT:
5
+ 'This idempotency key was already used with different input.',
6
+ SETTINGS_CONFLICT:
7
+ 'The resource settings changed. Read its current settings version and try again.',
8
+ OPERATION_CONFLICT:
9
+ 'The operation could not be admitted in the current resource state.',
10
+ METHOD_NOT_ALLOWED: 'This HTTP method is not supported for this resource.',
11
+ INVALID_CURSOR: 'The cursor is invalid or belongs to a different query.',
12
+ UNSUPPORTED_MEDIA_TYPE: 'Use application/json for the request body.',
13
+ REQUEST_TOO_LARGE: 'The request body is too large.',
14
+ } as const
15
+
16
+ export class ApiError extends Error {
17
+ constructor(
18
+ readonly code: keyof typeof apiErrorMessages,
19
+ readonly status: number,
20
+ ) {
21
+ super(apiErrorMessages[code])
22
+ this.name = 'ApiError'
23
+ }
24
+ }