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,198 @@
1
+ import { notifyProject } from '#devpush/server/realtime/service'
2
+ import { notifyAfterProjectUpdate } from '#devpush/server/notifications/observe'
3
+ import { handleGitHubInstallationWebhook } from '#devpush/server/github/service'
4
+ import {
5
+ getBranchFromGitHubRef,
6
+ InvalidGitHubInstallationPayloadError,
7
+ parseGitHubInstallationWebhookPayload,
8
+ parseGitHubPushPayload,
9
+ requireGitHubWebhookConfiguration,
10
+ validateGitHubWebhookHeaders,
11
+ verifyGitHubWebhookSignature,
12
+ } from '#devpush/server/github/webhooks'
13
+ import { createDeploymentsForGitHubPush } from '#devpush/server/projects/deployments'
14
+ import {
15
+ RequestBodyTooLargeError,
16
+ readRequestText,
17
+ } from '#devpush/server/http/request-body'
18
+ import { enforceRateLimit } from '#devpush/server/http/rate-limit'
19
+
20
+ // GitHub permits larger payloads, but this endpoint buffers the body before
21
+ // signature verification and only consumes compact push/installation fields.
22
+ const maxGitHubWebhookBytes = 5 * 1024 * 1024
23
+
24
+ export async function handleGitHubWebhook(request: Request) {
25
+ const webhookHeaders = validateGitHubWebhookHeaders(request.headers)
26
+
27
+ if (!webhookHeaders.ok) {
28
+ if (webhookHeaders.status === 401) {
29
+ const limited = await enforceRateLimit({
30
+ limit: 120,
31
+ request,
32
+ scope: 'github-webhook',
33
+ windowMs: 60_000,
34
+ })
35
+ if (limited) return limited
36
+ }
37
+ return Response.json(
38
+ { error: webhookHeaders.error },
39
+ { status: webhookHeaders.status },
40
+ )
41
+ }
42
+
43
+ try {
44
+ requireGitHubWebhookConfiguration()
45
+ } catch (error) {
46
+ return Response.json(
47
+ {
48
+ error:
49
+ error instanceof Error
50
+ ? error.message
51
+ : 'GitHub webhook verification failed.',
52
+ },
53
+ { status: 503 },
54
+ )
55
+ }
56
+
57
+ let body: string
58
+
59
+ try {
60
+ body = await readRequestText(request, maxGitHubWebhookBytes)
61
+ } catch (error) {
62
+ if (error instanceof RequestBodyTooLargeError) {
63
+ return Response.json(
64
+ { error: 'Webhook payload is too large.' },
65
+ { status: 413 },
66
+ )
67
+ }
68
+
69
+ throw error
70
+ }
71
+
72
+ try {
73
+ const isValidSignature = verifyGitHubWebhookSignature({
74
+ body,
75
+ signature: webhookHeaders.signature,
76
+ })
77
+
78
+ if (!isValidSignature) {
79
+ const rateLimitResponse = await enforceRateLimit({
80
+ limit: 120,
81
+ request,
82
+ scope: 'github-webhook',
83
+ windowMs: 60_000,
84
+ })
85
+ if (rateLimitResponse) {
86
+ return rateLimitResponse
87
+ }
88
+
89
+ return Response.json({ error: 'Invalid signature.' }, { status: 401 })
90
+ }
91
+ } catch (error) {
92
+ return Response.json(
93
+ {
94
+ error:
95
+ error instanceof Error
96
+ ? error.message
97
+ : 'GitHub webhook verification failed.',
98
+ },
99
+ { status: 503 },
100
+ )
101
+ }
102
+
103
+ let payload: ReturnType<typeof parseGitHubPushPayload> | null = null
104
+ const { deliveryId, event } = webhookHeaders
105
+
106
+ if (
107
+ event === 'installation' ||
108
+ event === 'installation_repositories' ||
109
+ event === 'installation_target'
110
+ ) {
111
+ try {
112
+ const installationPayload = parseGitHubInstallationWebhookPayload(body)
113
+ await handleGitHubInstallationWebhook({
114
+ action: installationPayload.action,
115
+ event,
116
+ installationId: installationPayload.installationId,
117
+ })
118
+ } catch (error) {
119
+ if (error instanceof InvalidGitHubInstallationPayloadError) {
120
+ return Response.json({ error: error.message }, { status: 400 })
121
+ }
122
+
123
+ throw error
124
+ }
125
+
126
+ return Response.json({ ok: true })
127
+ }
128
+
129
+ if (event === 'push') {
130
+ try {
131
+ payload = parseGitHubPushPayload(body)
132
+ } catch (error) {
133
+ return Response.json(
134
+ {
135
+ error:
136
+ error instanceof Error
137
+ ? error.message
138
+ : 'Invalid GitHub webhook payload.',
139
+ },
140
+ { status: 400 },
141
+ )
142
+ }
143
+ }
144
+
145
+ if (event !== 'push') {
146
+ return Response.json({ ok: true, ignored: event })
147
+ }
148
+
149
+ if (!payload) {
150
+ return Response.json(
151
+ { error: 'Invalid GitHub webhook payload.' },
152
+ { status: 400 },
153
+ )
154
+ }
155
+
156
+ const branch = getBranchFromGitHubRef(payload.ref)
157
+ const installationId = payload.installation?.id
158
+ const repositoryId = payload.repository?.id
159
+ const repositoryFullName = payload.repository?.full_name
160
+ const gitSha = payload.head_commit?.id
161
+
162
+ if (
163
+ !branch ||
164
+ typeof installationId !== 'number' ||
165
+ !Number.isSafeInteger(installationId) ||
166
+ typeof repositoryId !== 'number' ||
167
+ !Number.isSafeInteger(repositoryId) ||
168
+ !repositoryFullName ||
169
+ !gitSha
170
+ ) {
171
+ return Response.json({ ok: true, ignored: 'unsupported_push' })
172
+ }
173
+
174
+ const result = await createDeploymentsForGitHubPush({
175
+ deliveryId,
176
+ installationId,
177
+ repositoryId,
178
+ repositoryFullName,
179
+ branch,
180
+ gitSha,
181
+ message: payload.head_commit?.message ?? null,
182
+ authorName: payload.head_commit?.author?.name ?? null,
183
+ authoredAt: payload.head_commit?.timestamp ?? null,
184
+ commitUrl: payload.head_commit?.url ?? null,
185
+ githubSenderId:
186
+ Number.isSafeInteger(payload.sender?.id) && (payload.sender?.id ?? 0) > 0
187
+ ? String(payload.sender!.id)
188
+ : undefined,
189
+ })
190
+
191
+ for (const projectId of new Set(
192
+ [...result.created, ...result.skipped].map((item) => item.projectId),
193
+ )) {
194
+ await notifyProject(projectId)
195
+ notifyAfterProjectUpdate(projectId)
196
+ }
197
+ return Response.json({ ok: true, ...result })
198
+ }
@@ -0,0 +1,205 @@
1
+ import { createHmac, timingSafeEqual } from 'node:crypto'
2
+
3
+ import { config } from '#devpush/server/config'
4
+
5
+ export type GitHubPushWebhookPayload = {
6
+ sender?: { id?: number } | null
7
+ ref?: string
8
+ head_commit?: {
9
+ id?: string
10
+ message?: string
11
+ author?: { name?: string }
12
+ timestamp?: string
13
+ url?: string
14
+ } | null
15
+ installation?: {
16
+ id?: number
17
+ } | null
18
+ repository?: {
19
+ id?: number
20
+ full_name?: string
21
+ } | null
22
+ }
23
+
24
+ export type GitHubInstallationWebhookPayload = {
25
+ action: string
26
+ installationId: number
27
+ }
28
+
29
+ export type GitHubInstallationWebhookEvent =
30
+ | 'installation'
31
+ | 'installation_repositories'
32
+ | 'installation_target'
33
+
34
+ export type GitHubInstallationChange =
35
+ | { type: 'refresh' }
36
+ | { type: 'status'; status: 'deleted' | 'suspended' }
37
+ | { type: 'ignore' }
38
+
39
+ type GitHubWebhookHeadersResult =
40
+ | {
41
+ ok: true
42
+ deliveryId: string
43
+ event: string
44
+ signature: string
45
+ }
46
+ | { ok: false; error: string; status: 400 | 401 }
47
+
48
+ export function validateGitHubWebhookHeaders(
49
+ headers: Headers,
50
+ ): GitHubWebhookHeadersResult {
51
+ const signature = headers.get('x-hub-signature-256')?.trim() ?? ''
52
+ if (!/^sha256=[a-f0-9]{64}$/i.test(signature)) {
53
+ return { ok: false, error: 'Invalid signature.', status: 401 }
54
+ }
55
+
56
+ const deliveryId = headers.get('x-github-delivery')?.trim() ?? ''
57
+ if (!/^[a-z0-9-]{1,255}$/i.test(deliveryId)) {
58
+ return {
59
+ ok: false,
60
+ error: 'Missing or invalid GitHub delivery ID.',
61
+ status: 400,
62
+ }
63
+ }
64
+
65
+ const event = headers.get('x-github-event')?.trim() ?? ''
66
+ if (!/^[a-z][a-z0-9_]{0,63}$/.test(event)) {
67
+ return {
68
+ ok: false,
69
+ error: 'Missing or invalid GitHub event.',
70
+ status: 400,
71
+ }
72
+ }
73
+
74
+ return { ok: true, deliveryId, event, signature }
75
+ }
76
+
77
+ export function requireGitHubWebhookConfiguration() {
78
+ if (!config.GITHUB_APP_WEBHOOK_SECRET) {
79
+ throw new Error('GitHub webhook secret is not configured.')
80
+ }
81
+
82
+ return config.GITHUB_APP_WEBHOOK_SECRET
83
+ }
84
+
85
+ export function verifyGitHubWebhookSignature({
86
+ body,
87
+ signature,
88
+ }: {
89
+ body: string
90
+ signature: string | null
91
+ }) {
92
+ const webhookSecret = requireGitHubWebhookConfiguration()
93
+
94
+ if (!signature?.startsWith('sha256=')) {
95
+ return false
96
+ }
97
+
98
+ const expected = createHmac('sha256', webhookSecret)
99
+ .update(body)
100
+ .digest('hex')
101
+ const actual = signature.slice('sha256='.length)
102
+
103
+ return safeEqualHex(actual, expected)
104
+ }
105
+
106
+ export function parseGitHubPushPayload(body: string): GitHubPushWebhookPayload {
107
+ const payload: unknown = JSON.parse(body)
108
+
109
+ if (!payload || typeof payload !== 'object') {
110
+ throw new Error('Invalid GitHub webhook payload.')
111
+ }
112
+
113
+ return payload
114
+ }
115
+
116
+ export class InvalidGitHubInstallationPayloadError extends Error {
117
+ constructor() {
118
+ super('Invalid GitHub installation webhook payload.')
119
+ }
120
+ }
121
+
122
+ export function parseGitHubInstallationWebhookPayload(
123
+ body: string,
124
+ ): GitHubInstallationWebhookPayload {
125
+ let payload: unknown
126
+ try {
127
+ payload = JSON.parse(body)
128
+ } catch {
129
+ throw new InvalidGitHubInstallationPayloadError()
130
+ }
131
+
132
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
133
+ throw new InvalidGitHubInstallationPayloadError()
134
+ }
135
+
136
+ const record = payload as Record<string, unknown>
137
+ const installation = record.installation
138
+
139
+ if (
140
+ typeof record.action !== 'string' ||
141
+ !installation ||
142
+ typeof installation !== 'object' ||
143
+ Array.isArray(installation)
144
+ ) {
145
+ throw new InvalidGitHubInstallationPayloadError()
146
+ }
147
+
148
+ const installationId = (installation as Record<string, unknown>).id
149
+
150
+ if (
151
+ typeof installationId !== 'number' ||
152
+ !Number.isSafeInteger(installationId)
153
+ ) {
154
+ throw new InvalidGitHubInstallationPayloadError()
155
+ }
156
+
157
+ return { action: record.action, installationId }
158
+ }
159
+
160
+ export function getGitHubInstallationChange(
161
+ event: GitHubInstallationWebhookEvent,
162
+ action: string,
163
+ ): GitHubInstallationChange {
164
+ if (event === 'installation') {
165
+ if (action === 'deleted') {
166
+ return { type: 'status', status: 'deleted' }
167
+ }
168
+
169
+ if (action === 'suspend') {
170
+ return { type: 'status', status: 'suspended' }
171
+ }
172
+
173
+ if (
174
+ !['created', 'new_permissions_accepted', 'unsuspend'].includes(action)
175
+ ) {
176
+ return { type: 'ignore' }
177
+ }
178
+ }
179
+
180
+ return { type: 'refresh' }
181
+ }
182
+
183
+ export function getBranchFromGitHubRef(ref: string | undefined) {
184
+ const prefix = 'refs/heads/'
185
+
186
+ if (!ref?.startsWith(prefix)) {
187
+ return null
188
+ }
189
+
190
+ return ref.slice(prefix.length)
191
+ }
192
+
193
+ function safeEqualHex(actualHex: string, expectedHex: string) {
194
+ if (
195
+ !/^[a-f0-9]+$/i.test(actualHex) ||
196
+ actualHex.length !== expectedHex.length
197
+ ) {
198
+ return false
199
+ }
200
+
201
+ return timingSafeEqual(
202
+ Buffer.from(actualHex, 'hex'),
203
+ Buffer.from(expectedHex, 'hex'),
204
+ )
205
+ }
@@ -0,0 +1,11 @@
1
+ import { timingSafeEqual } from 'node:crypto'
2
+
3
+ export function matchesBearerSecret(
4
+ authorization: string | null,
5
+ secret: string,
6
+ ) {
7
+ if (!authorization || !secret) return false
8
+ const actual = Buffer.from(authorization)
9
+ const expected = Buffer.from(`Bearer ${secret}`)
10
+ return actual.length === expected.length && timingSafeEqual(actual, expected)
11
+ }
@@ -0,0 +1,66 @@
1
+ // Keep source locations for investigation without retaining exception messages,
2
+ // query text, URLs, or serialized request values. This works on Node and Workers.
3
+ export function safeErrorDiagnostics(error: unknown) {
4
+ if (!(error instanceof Error))
5
+ return { errorName: 'unknown', stackFrames: [] as string[] }
6
+ try {
7
+ const errorName = /^[A-Za-z][A-Za-z0-9_.-]{0,79}$/.test(error.name)
8
+ ? error.name
9
+ : 'Error'
10
+ if (typeof error.stack !== 'string')
11
+ return { errorName, stackFrames: [] as string[] }
12
+ // A multiline exception message can itself contain frame-shaped text.
13
+ // Remove the complete message before examining actual stack frames.
14
+ const messagePosition = error.message
15
+ ? error.stack.indexOf(error.message)
16
+ : -1
17
+ const stack =
18
+ messagePosition >= 0
19
+ ? error.stack.slice(messagePosition + error.message.length)
20
+ : error.stack.slice(error.stack.indexOf('\n') + 1)
21
+ const stackFrames: string[] = []
22
+ for (const line of stack.slice(0, 32 * 1024).split('\n')) {
23
+ const match =
24
+ /^\s*at\s+(?:(.*?)\s+\()?([^()\n]+):(\d{1,8}):(\d{1,8})\)?$/.exec(line)
25
+ if (!match) continue
26
+ const source = safeSourceLocation(match[2]!)
27
+ if (!source) continue
28
+ const functionName =
29
+ match[1] &&
30
+ /^(?:(?:async|new) )?[A-Za-z_$][A-Za-z0-9_.$]{0,100}$/.test(match[1])
31
+ ? match[1]
32
+ : undefined
33
+ const location = `${source}:${match[3]}:${match[4]}`
34
+ stackFrames.push(
35
+ functionName ? `at ${functionName} (${location})` : `at ${location}`,
36
+ )
37
+ if (stackFrames.length === 8) break
38
+ }
39
+ return { errorName, stackFrames }
40
+ } catch {
41
+ // Nonstandard error accessors must not prevent the public error response.
42
+ return { errorName: 'Error', stackFrames: [] as string[] }
43
+ }
44
+ }
45
+
46
+ function safeSourceLocation(source: string) {
47
+ if (/^node:[A-Za-z0-9_./-]{1,180}$/.test(source)) return source
48
+ if (
49
+ /^[A-Za-z][A-Za-z0-9+.-]*:/.test(source) &&
50
+ !source.startsWith('file:///') &&
51
+ !/^[A-Za-z]:[\\/]/.test(source)
52
+ )
53
+ return null
54
+ const parts = source
55
+ .replace(/^file:\/\/\//, '/')
56
+ .replace(/\\/g, '/')
57
+ .split('/')
58
+ .slice(-4)
59
+ if (
60
+ !parts.length ||
61
+ parts.some((part) => !/^[A-Za-z0-9_$@.~-]{1,100}$/.test(part))
62
+ )
63
+ return null
64
+ if (!/\.(?:[cm]?js|tsx?|jsx)$/.test(parts.at(-1)!)) return null
65
+ return parts.join('/').slice(-200)
66
+ }
@@ -0,0 +1,15 @@
1
+ import { getAuthSession } from '#devpush/server/auth/session.server'
2
+ import { getPluginServices } from '#devpush/server/plugins/services.server'
3
+
4
+ export async function handlePluginRequest(request: Request) {
5
+ const session = await getAuthSession()
6
+ return getPluginServices().handleRequest!(
7
+ request,
8
+ session
9
+ ? {
10
+ userId: session.user.id,
11
+ email: session.user.email,
12
+ }
13
+ : null,
14
+ )
15
+ }
@@ -0,0 +1,53 @@
1
+ import { config } from '#devpush/server/config'
2
+ import { isCloudflareRuntime } from '#devpush/server/runtime/platform-env'
3
+
4
+ export function resolveProxyRequest(
5
+ request: Request,
6
+ options = {
7
+ trusted: config.DEVPUSH_TRUST_PROXY && !isCloudflareRuntime(),
8
+ origins: [
9
+ config.DEVPUSH_BASE_URL,
10
+ config.BETTER_AUTH_URL,
11
+ config.DEVPUSH_MCP_URL,
12
+ ],
13
+ },
14
+ ): Request {
15
+ if (!options.trusted) return request
16
+ const protocol = request.headers
17
+ .get('x-forwarded-proto')
18
+ ?.split(',')
19
+ .at(-1)
20
+ ?.trim()
21
+ .toLowerCase()
22
+ if (protocol !== 'http' && protocol !== 'https') return request
23
+ const current = new URL(request.url)
24
+ const host =
25
+ request.headers.get('x-forwarded-host')?.split(',').at(-1)?.trim() ??
26
+ current.host
27
+ let origin: URL
28
+ try {
29
+ origin = new URL(`${protocol}://${host}`)
30
+ } catch {
31
+ return request
32
+ }
33
+ if (
34
+ origin.username ||
35
+ origin.password ||
36
+ origin.pathname !== '/' ||
37
+ origin.search ||
38
+ origin.hash
39
+ )
40
+ return request
41
+ if (!options.origins.some((value) => new URL(value).origin === origin.origin))
42
+ return request
43
+ if (current.origin === origin.origin) return request
44
+ const url = new URL(current.pathname + current.search, origin)
45
+ return new Request(url, {
46
+ method: request.method,
47
+ headers: request.headers,
48
+ body: request.body,
49
+ signal: request.signal,
50
+ redirect: request.redirect,
51
+ duplex: 'half',
52
+ } as RequestInit)
53
+ }
@@ -0,0 +1,48 @@
1
+ import { createMiddleware } from '@tanstack/react-start'
2
+ import { PublicError } from '#devpush/lib/public-error'
3
+ import { ZodError } from 'zod'
4
+ import { safeErrorDiagnostics } from './error-diagnostics'
5
+
6
+ export const publicErrorsMiddleware = createMiddleware({
7
+ type: 'function',
8
+ }).server(async ({ next }) => {
9
+ try {
10
+ return await next()
11
+ } catch (error) {
12
+ throw publicErrorPayload(error)
13
+ }
14
+ })
15
+
16
+ export function toPublicError(error: unknown): PublicError {
17
+ if (error instanceof PublicError) return error
18
+ if (
19
+ error &&
20
+ typeof error === 'object' &&
21
+ (('status' in error && error.status === 429) ||
22
+ ('statusCode' in error && error.statusCode === 429))
23
+ )
24
+ return new PublicError('RATE_LIMITED')
25
+ if (error instanceof ZodError) return new PublicError('INVALID_INPUT')
26
+ if (error instanceof Error && error.message === 'Forbidden')
27
+ return new PublicError('FORBIDDEN')
28
+ if (
29
+ error instanceof Error &&
30
+ (error.message === 'Unauthorized' ||
31
+ error.message.startsWith('Sign in before '))
32
+ )
33
+ return new PublicError('UNAUTHORIZED')
34
+ console.error('Server action failed', safeErrorDiagnostics(error))
35
+ return new PublicError('INTERNAL_ERROR')
36
+ }
37
+
38
+ // Plain objects survive Start's default transport without per-window adapters.
39
+ export function publicErrorPayload(error: unknown) {
40
+ const safe = toPublicError(error)
41
+ return {
42
+ name: 'PublicError' as const,
43
+ code: safe.code,
44
+ message: safe.message,
45
+ ...(safe.policy ? { policy: safe.policy } : {}),
46
+ ...(safe.reloadUrl ? { reloadUrl: safe.reloadUrl } : {}),
47
+ }
48
+ }
@@ -0,0 +1,59 @@
1
+ import { createHash } from 'node:crypto'
2
+
3
+ import { lt, sql } from 'drizzle-orm'
4
+
5
+ import { db } from '#devpush/server/db'
6
+ import { rateLimits } from '#devpush/server/db/schema'
7
+ import { getRequestPublicAddress } from './request-ip'
8
+
9
+ export async function enforceRateLimit({
10
+ limit,
11
+ request,
12
+ identity: explicitIdentity,
13
+ scope,
14
+ windowMs,
15
+ }: {
16
+ limit: number
17
+ request?: Request
18
+ identity?: string
19
+ scope: string
20
+ windowMs: number
21
+ }) {
22
+ const identity =
23
+ explicitIdentity ??
24
+ (request ? getRequestPublicAddress(request) : null) ??
25
+ 'unknown-address'
26
+ const key = `${scope}:${createHash('sha256').update(identity).digest('hex')}`
27
+ const now = new Date()
28
+ const resetAt = new Date(now.getTime() + windowMs)
29
+ const rows = await db
30
+ .insert(rateLimits)
31
+ .values({ key, count: 1, resetAt })
32
+ .onConflictDoUpdate({
33
+ target: rateLimits.key,
34
+ set: {
35
+ count: sql<number>`case when ${rateLimits.resetAt} <= ${now} then 1 else ${rateLimits.count} + 1 end`,
36
+ resetAt: sql<Date>`case when ${rateLimits.resetAt} <= ${now} then ${resetAt} else ${rateLimits.resetAt} end`,
37
+ },
38
+ })
39
+ .returning({ count: rateLimits.count, resetAt: rateLimits.resetAt })
40
+ const state = rows[0]
41
+ if (!state) throw new Error('Rate limit update returned no state.')
42
+
43
+ if (state.count <= limit) {
44
+ return null
45
+ }
46
+
47
+ const retryAfter = Math.max(
48
+ 1,
49
+ Math.ceil((state.resetAt.getTime() - now.getTime()) / 1000),
50
+ )
51
+ return Response.json(
52
+ { error: 'Too many requests. Try again later.' },
53
+ { status: 429, headers: { 'retry-after': String(retryAfter) } },
54
+ )
55
+ }
56
+
57
+ export async function cleanupExpiredRateLimits() {
58
+ await db.delete(rateLimits).where(lt(rateLimits.resetAt, new Date()))
59
+ }