devpush 0.0.0-stage → 1.0.0-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (456) hide show
  1. package/README.md +22 -2
  2. package/agent-release.json +37 -0
  3. package/dist/package/LICENSE.md +21 -0
  4. package/dist/package/migrate.mjs +182 -0
  5. package/dist/package/plugins.mjs +125 -0
  6. package/dist/package/vite.mjs +439 -0
  7. package/package.json +168 -3
  8. package/public/apple-touch-icon.png +0 -0
  9. package/public/favicon.svg +1 -0
  10. package/public/logo-email.png +0 -0
  11. package/public/robots.txt +3 -0
  12. package/public/social.png +0 -0
  13. package/src/cloudflare-runtime.d.ts +78 -0
  14. package/src/components/about-dialog.tsx +70 -0
  15. package/src/components/copy-button.tsx +74 -0
  16. package/src/components/logo-mark.tsx +20 -0
  17. package/src/components/pending-collection.tsx +103 -0
  18. package/src/components/resource-avatar.tsx +65 -0
  19. package/src/components/route-error-state.tsx +95 -0
  20. package/src/components/section-navigation.tsx +39 -0
  21. package/src/components/settings-layout.tsx +111 -0
  22. package/src/components/storage-capabilities.tsx +11 -0
  23. package/src/components/table-pager.tsx +101 -0
  24. package/src/components/ui/alert-dialog.tsx +180 -0
  25. package/src/components/ui/alert.tsx +78 -0
  26. package/src/components/ui/avatar.tsx +58 -0
  27. package/src/components/ui/badge.tsx +49 -0
  28. package/src/components/ui/button-group.tsx +38 -0
  29. package/src/components/ui/button.tsx +67 -0
  30. package/src/components/ui/card.tsx +82 -0
  31. package/src/components/ui/chart.tsx +307 -0
  32. package/src/components/ui/checkbox.tsx +30 -0
  33. package/src/components/ui/combobox.tsx +256 -0
  34. package/src/components/ui/dialog.tsx +163 -0
  35. package/src/components/ui/dropdown-menu.tsx +110 -0
  36. package/src/components/ui/empty.tsx +65 -0
  37. package/src/components/ui/field.tsx +192 -0
  38. package/src/components/ui/input-group.tsx +118 -0
  39. package/src/components/ui/input-otp.tsx +70 -0
  40. package/src/components/ui/input.tsx +19 -0
  41. package/src/components/ui/label.tsx +24 -0
  42. package/src/components/ui/pagination.tsx +34 -0
  43. package/src/components/ui/popover.tsx +86 -0
  44. package/src/components/ui/select.tsx +145 -0
  45. package/src/components/ui/skeleton.tsx +19 -0
  46. package/src/components/ui/sonner.tsx +47 -0
  47. package/src/components/ui/switch.tsx +31 -0
  48. package/src/components/ui/table.tsx +81 -0
  49. package/src/components/ui/textarea.tsx +18 -0
  50. package/src/components/ui/tooltip.tsx +55 -0
  51. package/src/features/admin/admin-settings.tsx +1048 -0
  52. package/src/features/auth/current-user-context-query.ts +11 -0
  53. package/src/features/auth/login-form.tsx +442 -0
  54. package/src/features/auth/sign-out-menu-item.tsx +42 -0
  55. package/src/features/deployments/deployment-actions.tsx +336 -0
  56. package/src/features/deployments/deployment-alias-badge.tsx +35 -0
  57. package/src/features/deployments/deployment-detail.tsx +596 -0
  58. package/src/features/deployments/deployment-filters.ts +66 -0
  59. package/src/features/deployments/deployment-list.tsx +465 -0
  60. package/src/features/deployments/deployment-visit-links.tsx +104 -0
  61. package/src/features/deployments/deployments-page.tsx +376 -0
  62. package/src/features/logs/log-scroll-area.tsx +69 -0
  63. package/src/features/notifications/email-preferences.tsx +232 -0
  64. package/src/features/onboarding/profile-onboarding.tsx +162 -0
  65. package/src/features/plugins/contributions.tsx +118 -0
  66. package/src/features/projects/backup-job-logs.tsx +143 -0
  67. package/src/features/projects/backup-policy-delete-dialog.tsx +82 -0
  68. package/src/features/projects/backup-restore-actions.tsx +573 -0
  69. package/src/features/projects/backup-status-icon.tsx +33 -0
  70. package/src/features/projects/backup-target-list.tsx +222 -0
  71. package/src/features/projects/backup-timestamp.tsx +32 -0
  72. package/src/features/projects/backup-volume-picker.tsx +158 -0
  73. package/src/features/projects/compose-service-input.tsx +64 -0
  74. package/src/features/projects/deploy-readiness.ts +28 -0
  75. package/src/features/projects/environment-dot.tsx +91 -0
  76. package/src/features/projects/log-keyword-filter.tsx +48 -0
  77. package/src/features/projects/manual-deploy-dialog.tsx +563 -0
  78. package/src/features/projects/pending-section.ts +26 -0
  79. package/src/features/projects/project-backup-details-pending.tsx +305 -0
  80. package/src/features/projects/project-backup-details.tsx +936 -0
  81. package/src/features/projects/project-backup-policy.tsx +644 -0
  82. package/src/features/projects/project-backup-utils.ts +190 -0
  83. package/src/features/projects/project-backups-pending.tsx +136 -0
  84. package/src/features/projects/project-backups.tsx +876 -0
  85. package/src/features/projects/project-environment-settings.tsx +1128 -0
  86. package/src/features/projects/project-heading.tsx +68 -0
  87. package/src/features/projects/project-layout.tsx +188 -0
  88. package/src/features/projects/project-logs.tsx +461 -0
  89. package/src/features/projects/project-overview.tsx +280 -0
  90. package/src/features/projects/project-route-pending.tsx +1064 -0
  91. package/src/features/projects/project-routing-settings.tsx +663 -0
  92. package/src/features/projects/project-settings-section.tsx +64 -0
  93. package/src/features/projects/project-settings-utils.ts +101 -0
  94. package/src/features/projects/project-settings.tsx +1472 -0
  95. package/src/features/projects/repository-picker-dialog.tsx +404 -0
  96. package/src/features/repositories/new-project-dialog-launcher.tsx +44 -0
  97. package/src/features/repositories/new-project-dialog.tsx +1204 -0
  98. package/src/features/repositories/new-project-pending.tsx +28 -0
  99. package/src/features/repositories/repository-list-pending.tsx +59 -0
  100. package/src/features/servers/server-availability-alert.tsx +44 -0
  101. package/src/features/servers/server-availability.ts +29 -0
  102. package/src/features/servers/server-avatar.tsx +52 -0
  103. package/src/features/servers/server-detail-pending.tsx +315 -0
  104. package/src/features/servers/server-detail.tsx +855 -0
  105. package/src/features/servers/server-metric-chart-pending.tsx +38 -0
  106. package/src/features/servers/server-metric-chart.tsx +182 -0
  107. package/src/features/servers/server-metric-format.ts +50 -0
  108. package/src/features/servers/server-metrics-pending.tsx +75 -0
  109. package/src/features/servers/server-metrics.tsx +136 -0
  110. package/src/features/servers/team-servers.tsx +432 -0
  111. package/src/features/settings/settings-route-pending.tsx +406 -0
  112. package/src/features/teams/backup-storage-settings.tsx +483 -0
  113. package/src/features/teams/navigation.ts +51 -0
  114. package/src/features/teams/new-team-dialog.tsx +132 -0
  115. package/src/features/teams/pending-section.ts +19 -0
  116. package/src/features/teams/team-header.tsx +1286 -0
  117. package/src/features/teams/team-heading.tsx +57 -0
  118. package/src/features/teams/team-home.tsx +391 -0
  119. package/src/features/teams/team-invite.tsx +155 -0
  120. package/src/features/teams/team-route-pending.tsx +505 -0
  121. package/src/features/teams/team-settings.tsx +1555 -0
  122. package/src/features/teams/use-workspace-updates.ts +126 -0
  123. package/src/features/users/api-keys-settings.tsx +544 -0
  124. package/src/features/users/api-scope-selection.tsx +269 -0
  125. package/src/features/users/connected-apps-settings.tsx +449 -0
  126. package/src/features/users/mcp-consent.tsx +220 -0
  127. package/src/features/users/profile-picture.tsx +171 -0
  128. package/src/features/users/user-settings.tsx +1064 -0
  129. package/src/lib/account-settings-schema.ts +78 -0
  130. package/src/lib/action-feedback.ts +81 -0
  131. package/src/lib/admin-search-schema.ts +8 -0
  132. package/src/lib/agent-release.d.mts +21 -0
  133. package/src/lib/agent-release.mjs +90 -0
  134. package/src/lib/allowlist-schema.ts +79 -0
  135. package/src/lib/api-key-schema.ts +176 -0
  136. package/src/lib/auth/client.ts +20 -0
  137. package/src/lib/avatar-schema.ts +11 -0
  138. package/src/lib/backup-cron.ts +118 -0
  139. package/src/lib/backup-database.ts +41 -0
  140. package/src/lib/backup-feature.ts +2 -0
  141. package/src/lib/backup-paths.ts +7 -0
  142. package/src/lib/backup-policy-schema.ts +85 -0
  143. package/src/lib/backup-resource-schema.ts +12 -0
  144. package/src/lib/backup-restore-schema.ts +15 -0
  145. package/src/lib/backup-storage-schema.ts +55 -0
  146. package/src/lib/deferred-module.ts +38 -0
  147. package/src/lib/deployment-logs-query.ts +63 -0
  148. package/src/lib/display-name.ts +7 -0
  149. package/src/lib/error-message.ts +119 -0
  150. package/src/lib/form-errors.ts +10 -0
  151. package/src/lib/internal-redirect.ts +25 -0
  152. package/src/lib/manual-deploy-options-query.ts +21 -0
  153. package/src/lib/notification-preferences.ts +43 -0
  154. package/src/lib/onboarding.ts +11 -0
  155. package/src/lib/operation-logs-query.ts +51 -0
  156. package/src/lib/page-head.ts +14 -0
  157. package/src/lib/project-context-query.ts +257 -0
  158. package/src/lib/project-creation-schema.ts +64 -0
  159. package/src/lib/project-env-schema.ts +23 -0
  160. package/src/lib/project-environments-schema.ts +56 -0
  161. package/src/lib/project-routing-schema.ts +94 -0
  162. package/src/lib/project-runtime-config.ts +131 -0
  163. package/src/lib/project-settings-schema.ts +75 -0
  164. package/src/lib/public-api-schema.ts +128 -0
  165. package/src/lib/public-error.ts +337 -0
  166. package/src/lib/query.ts +13 -0
  167. package/src/lib/repository-selection-query.ts +11 -0
  168. package/src/lib/resource-cache.ts +30 -0
  169. package/src/lib/runtime-id.ts +19 -0
  170. package/src/lib/search.ts +14 -0
  171. package/src/lib/server-context-query.ts +43 -0
  172. package/src/lib/server-name-schema.ts +11 -0
  173. package/src/lib/settings-conflict.ts +45 -0
  174. package/src/lib/settings-version.ts +7 -0
  175. package/src/lib/team-context-query.ts +126 -0
  176. package/src/lib/team-context-schema.ts +8 -0
  177. package/src/lib/team-forms-schema.ts +45 -0
  178. package/src/lib/team-role.ts +13 -0
  179. package/src/lib/team-settings-schema.ts +48 -0
  180. package/src/lib/urls.ts +3 -0
  181. package/src/lib/use-avatar-input.ts +61 -0
  182. package/src/lib/use-refresh-error.ts +18 -0
  183. package/src/lib/use-settings-version.ts +23 -0
  184. package/src/lib/utils.ts +6 -0
  185. package/src/lib/validation.ts +5 -0
  186. package/src/lib/workspace-updates.ts +53 -0
  187. package/src/package/config.ts +10 -0
  188. package/src/package/env.mjs +54 -0
  189. package/src/package/migrate-api.d.ts +5 -0
  190. package/src/package/migrate.ts +56 -0
  191. package/src/package/plugins.ts +141 -0
  192. package/src/package/sqlite.mjs +2 -0
  193. package/src/package/vite-api.d.ts +5 -0
  194. package/src/package/vite.ts +324 -0
  195. package/src/package/watch-guard.cjs +26 -0
  196. package/src/plugins/client.ts +57 -0
  197. package/src/plugins/contract.ts +188 -0
  198. package/src/plugins/default.server.ts +4 -0
  199. package/src/plugins/default.ui.ts +4 -0
  200. package/src/plugins/operations.ts +45 -0
  201. package/src/plugins/server.ts +197 -0
  202. package/src/plugins/version.ts +1 -0
  203. package/src/routeTree.gen.ts +1275 -0
  204. package/src/router.tsx +46 -0
  205. package/src/routes/$teamSlug.index.tsx +53 -0
  206. package/src/routes/$teamSlug.new-project.tsx +13 -0
  207. package/src/routes/$teamSlug.projects.$projectName.backups.$backupId.tsx +40 -0
  208. package/src/routes/$teamSlug.projects.$projectName.backups.tsx +82 -0
  209. package/src/routes/$teamSlug.projects.$projectName.deployments.$deploymentId.tsx +52 -0
  210. package/src/routes/$teamSlug.projects.$projectName.deployments.tsx +86 -0
  211. package/src/routes/$teamSlug.projects.$projectName.logs.tsx +55 -0
  212. package/src/routes/$teamSlug.projects.$projectName.settings.tsx +51 -0
  213. package/src/routes/$teamSlug.projects.$projectName.tsx +106 -0
  214. package/src/routes/$teamSlug.projects.tsx +40 -0
  215. package/src/routes/$teamSlug.servers.$serverId.tsx +31 -0
  216. package/src/routes/$teamSlug.servers.index.tsx +59 -0
  217. package/src/routes/$teamSlug.servers.tsx +7 -0
  218. package/src/routes/$teamSlug.settings.tsx +56 -0
  219. package/src/routes/$teamSlug.tsx +86 -0
  220. package/src/routes/[.]well-known.oauth-authorization-server.api.auth.ts +13 -0
  221. package/src/routes/[.]well-known.oauth-protected-resource.mcp.ts +6 -0
  222. package/src/routes/__root.tsx +155 -0
  223. package/src/routes/admin.tsx +55 -0
  224. package/src/routes/api/agents/certificates.ts +21 -0
  225. package/src/routes/api/agents/credentials/rotate.ts +22 -0
  226. package/src/routes/api/agents/heartbeat.ts +42 -0
  227. package/src/routes/api/agents/job-logs.ts +22 -0
  228. package/src/routes/api/agents/jobs/complete.ts +22 -0
  229. package/src/routes/api/agents/jobs/next.ts +29 -0
  230. package/src/routes/api/agents/jobs/progress.ts +32 -0
  231. package/src/routes/api/agents/jobs/status.ts +22 -0
  232. package/src/routes/api/agents/register.ts +45 -0
  233. package/src/routes/api/agents/runtime-logs.ts +22 -0
  234. package/src/routes/api/agents/switch.ts +25 -0
  235. package/src/routes/api/auth/$.ts +38 -0
  236. package/src/routes/api/cron/backups.ts +33 -0
  237. package/src/routes/api/cron/maintenance.ts +30 -0
  238. package/src/routes/api/github/install.callback.ts +51 -0
  239. package/src/routes/api/github/install.ts +41 -0
  240. package/src/routes/api/github/webhook.ts +11 -0
  241. package/src/routes/api/health.ts +19 -0
  242. package/src/routes/api/install/agent.$token.ts +50 -0
  243. package/src/routes/api/install/agent.binary.ts +83 -0
  244. package/src/routes/api/install/agent.checksum.ts +45 -0
  245. package/src/routes/api/install/agent.ts +26 -0
  246. package/src/routes/api/plugins/$.ts +11 -0
  247. package/src/routes/api/v1/$.ts +15 -0
  248. package/src/routes/api/workspaces/updates.ts +8 -0
  249. package/src/routes/auth.login.tsx +50 -0
  250. package/src/routes/deployments.$deploymentId.unavailable.tsx +21 -0
  251. package/src/routes/index.tsx +23 -0
  252. package/src/routes/invites.$token.tsx +35 -0
  253. package/src/routes/mcp.ts +13 -0
  254. package/src/routes/oauth.consent.tsx +54 -0
  255. package/src/routes/onboarding.tsx +37 -0
  256. package/src/routes/onboarding.workspace.tsx +44 -0
  257. package/src/routes/settings.tsx +30 -0
  258. package/src/routes/uploads/avatars/$fileName.ts +44 -0
  259. package/src/server/admin/access.ts +67 -0
  260. package/src/server/admin/functions.ts +122 -0
  261. package/src/server/admin/service.ts +260 -0
  262. package/src/server/agents/backup-state.ts +210 -0
  263. package/src/server/agents/certificates.ts +96 -0
  264. package/src/server/agents/claims.ts +116 -0
  265. package/src/server/agents/cleanup.ts +52 -0
  266. package/src/server/agents/compatibility.ts +116 -0
  267. package/src/server/agents/completion.ts +83 -0
  268. package/src/server/agents/container-cleanup-state.ts +51 -0
  269. package/src/server/agents/deployment-state.ts +111 -0
  270. package/src/server/agents/job-payloads.ts +159 -0
  271. package/src/server/agents/jobs.ts +1293 -0
  272. package/src/server/agents/protocol.ts +400 -0
  273. package/src/server/agents/reconciliation.ts +43 -0
  274. package/src/server/agents/runtime-logs.ts +337 -0
  275. package/src/server/agents/runtime-state.ts +125 -0
  276. package/src/server/api/admission.ts +12 -0
  277. package/src/server/api/authorization.ts +115 -0
  278. package/src/server/api/credentials.ts +569 -0
  279. package/src/server/api/cursors.ts +72 -0
  280. package/src/server/api/errors.ts +24 -0
  281. package/src/server/api/functions.ts +60 -0
  282. package/src/server/api/handler.ts +274 -0
  283. package/src/server/api/mutation-operations.ts +913 -0
  284. package/src/server/api/operation-context.ts +139 -0
  285. package/src/server/api/operation-contract.ts +1099 -0
  286. package/src/server/api/operations.ts +64 -0
  287. package/src/server/api/read-operations.ts +891 -0
  288. package/src/server/api/retention.ts +33 -0
  289. package/src/server/auth/access.ts +109 -0
  290. package/src/server/auth/auth-methods.ts +28 -0
  291. package/src/server/auth/better-auth.ts +518 -0
  292. package/src/server/auth/context.ts +28 -0
  293. package/src/server/auth/email-change.ts +47 -0
  294. package/src/server/auth/email-delivery.ts +51 -0
  295. package/src/server/auth/provider-identity.ts +19 -0
  296. package/src/server/auth/provider-tokens.ts +27 -0
  297. package/src/server/auth/session.server.ts +19 -0
  298. package/src/server/backups/database-commands.ts +40 -0
  299. package/src/server/backups/destination.ts +12 -0
  300. package/src/server/backups/feature.ts +6 -0
  301. package/src/server/backups/policy-validation.ts +51 -0
  302. package/src/server/backups/queue-state.ts +137 -0
  303. package/src/server/backups/service.ts +918 -0
  304. package/src/server/backups/storage-functions.ts +42 -0
  305. package/src/server/backups/storage-state.ts +67 -0
  306. package/src/server/backups/storage.ts +237 -0
  307. package/src/server/config.ts +274 -0
  308. package/src/server/db/atomic.ts +16 -0
  309. package/src/server/db/d1-connection.ts +83 -0
  310. package/src/server/db/errors.ts +17 -0
  311. package/src/server/db/index.cloudflare.ts +15 -0
  312. package/src/server/db/index.ts +35 -0
  313. package/src/server/db/libsql-connection.ts +72 -0
  314. package/src/server/db/log-schema.ts +102 -0
  315. package/src/server/db/migrate-libsql.ts +29 -0
  316. package/src/server/db/migrate-sqlite.ts +47 -0
  317. package/src/server/db/migration-history.ts +12 -0
  318. package/src/server/db/migrations-logs/0000_initial.sql +48 -0
  319. package/src/server/db/migrations-logs/0001_runtime_log_order.sql +2 -0
  320. package/src/server/db/migrations-logs/meta/0000_snapshot.json +327 -0
  321. package/src/server/db/migrations-logs/meta/0001_snapshot.json +329 -0
  322. package/src/server/db/migrations-logs/meta/_journal.json +20 -0
  323. package/src/server/db/migrations-sqlite/0000_initial.sql +894 -0
  324. package/src/server/db/migrations-sqlite/0001_agent_indexes.sql +5 -0
  325. package/src/server/db/migrations-sqlite/meta/0000_snapshot.json +5816 -0
  326. package/src/server/db/migrations-sqlite/meta/0001_snapshot.json +5841 -0
  327. package/src/server/db/migrations-sqlite/meta/_journal.json +20 -0
  328. package/src/server/db/oauth-schema.ts +216 -0
  329. package/src/server/db/remote-config.ts +47 -0
  330. package/src/server/db/schema.ts +1218 -0
  331. package/src/server/db/sqlite-connection.ts +94 -0
  332. package/src/server/db/sqlite-files.ts +27 -0
  333. package/src/server/db/sqlite-params.ts +19 -0
  334. package/src/server/db/worker-connection.ts +47 -0
  335. package/src/server/deleted-resources.ts +133 -0
  336. package/src/server/deletion-recovery.ts +272 -0
  337. package/src/server/email/layout.tsx +85 -0
  338. package/src/server/email/notification-template.tsx +62 -0
  339. package/src/server/email/otp-template.tsx +48 -0
  340. package/src/server/email/service.ts +251 -0
  341. package/src/server/email/team-invite-template.tsx +59 -0
  342. package/src/server/email/theme.ts +10 -0
  343. package/src/server/github/http.ts +14 -0
  344. package/src/server/github/install-state.ts +48 -0
  345. package/src/server/github/service.ts +831 -0
  346. package/src/server/github/webhook-handler.ts +198 -0
  347. package/src/server/github/webhooks.ts +205 -0
  348. package/src/server/http/auth.ts +11 -0
  349. package/src/server/http/error-diagnostics.ts +66 -0
  350. package/src/server/http/plugin-request.ts +15 -0
  351. package/src/server/http/proxy-request.ts +53 -0
  352. package/src/server/http/public-errors.ts +48 -0
  353. package/src/server/http/rate-limit.ts +59 -0
  354. package/src/server/http/request-body.ts +54 -0
  355. package/src/server/http/request-ip.ts +54 -0
  356. package/src/server/http/security-headers.ts +36 -0
  357. package/src/server/install/agent-binary.ts +230 -0
  358. package/src/server/install/agent-script.ts +565 -0
  359. package/src/server/install/release-binary.ts +65 -0
  360. package/src/server/logs/budget.ts +72 -0
  361. package/src/server/logs/cleanup.ts +64 -0
  362. package/src/server/logs/deployments.ts +98 -0
  363. package/src/server/logs/operations.ts +145 -0
  364. package/src/server/logs/retention.ts +39 -0
  365. package/src/server/maintenance.ts +61 -0
  366. package/src/server/mcp/connections.ts +358 -0
  367. package/src/server/mcp/functions.ts +63 -0
  368. package/src/server/mcp/handler.ts +295 -0
  369. package/src/server/mcp/oauth-discovery.cloudflare.ts +6 -0
  370. package/src/server/mcp/oauth-discovery.ts +8 -0
  371. package/src/server/mcp/oauth-provider.ts +220 -0
  372. package/src/server/mcp/urls.ts +20 -0
  373. package/src/server/notifications/functions.ts +51 -0
  374. package/src/server/notifications/observe.ts +303 -0
  375. package/src/server/notifications/outbox.ts +335 -0
  376. package/src/server/notifications/preferences.ts +131 -0
  377. package/src/server/notifications/query.ts +16 -0
  378. package/src/server/plugins/scope.ts +17 -0
  379. package/src/server/plugins/server-context.ts +157 -0
  380. package/src/server/plugins/services.server.ts +10 -0
  381. package/src/server/plugins/workspace-access.ts +74 -0
  382. package/src/server/projects/backup-policy-state.ts +68 -0
  383. package/src/server/projects/backups.ts +219 -0
  384. package/src/server/projects/compose-metadata.ts +174 -0
  385. package/src/server/projects/configuration-state.ts +90 -0
  386. package/src/server/projects/context.ts +909 -0
  387. package/src/server/projects/creation-state.ts +46 -0
  388. package/src/server/projects/deletion-state.ts +69 -0
  389. package/src/server/projects/deployment-config.ts +139 -0
  390. package/src/server/projects/deployment-logs.ts +97 -0
  391. package/src/server/projects/deployments.ts +1123 -0
  392. package/src/server/projects/domain-state.ts +71 -0
  393. package/src/server/projects/environment-branches.ts +75 -0
  394. package/src/server/projects/environment-state.ts +158 -0
  395. package/src/server/projects/environments.ts +424 -0
  396. package/src/server/projects/functions.ts +748 -0
  397. package/src/server/projects/generated-hostname.ts +77 -0
  398. package/src/server/projects/hostnames.ts +244 -0
  399. package/src/server/projects/mappers.ts +457 -0
  400. package/src/server/projects/names.ts +41 -0
  401. package/src/server/projects/project-env-vars.ts +28 -0
  402. package/src/server/projects/queue-state.ts +118 -0
  403. package/src/server/projects/repository.ts +368 -0
  404. package/src/server/projects/route-state.ts +37 -0
  405. package/src/server/projects/runtime-logs.ts +97 -0
  406. package/src/server/projects/settings-context.ts +58 -0
  407. package/src/server/projects/settings.ts +738 -0
  408. package/src/server/projects/types.ts +355 -0
  409. package/src/server/realtime/agent-events.ts +32 -0
  410. package/src/server/realtime/node-broadcaster.ts +71 -0
  411. package/src/server/realtime/node-handler.ts +55 -0
  412. package/src/server/realtime/service.ts +125 -0
  413. package/src/server/realtime/workspace-object.cloudflare.ts +62 -0
  414. package/src/server/realtime/workspace-object.ts +3 -0
  415. package/src/server/resources/color.ts +13 -0
  416. package/src/server/runtime/background.ts +21 -0
  417. package/src/server/runtime/node-asset-cache-plugin.ts +12 -0
  418. package/src/server/runtime/node-scheduler-plugin.ts +17 -0
  419. package/src/server/runtime/node-scheduler.ts +51 -0
  420. package/src/server/runtime/platform-env.cloudflare.ts +17 -0
  421. package/src/server/runtime/platform-env.ts +19 -0
  422. package/src/server/runtime/scheduled-task-error.ts +9 -0
  423. package/src/server/scheduled-tasks.ts +28 -0
  424. package/src/server/secrets/crypto.ts +56 -0
  425. package/src/server/servers/functions.ts +233 -0
  426. package/src/server/servers/lifecycle.ts +122 -0
  427. package/src/server/servers/metric-history-schema.ts +24 -0
  428. package/src/server/servers/metric-history.ts +55 -0
  429. package/src/server/servers/monitoring.ts +592 -0
  430. package/src/server/servers/service.ts +1211 -0
  431. package/src/server/servers/status.ts +32 -0
  432. package/src/server/servers/switch.ts +96 -0
  433. package/src/server/settings-version.ts +43 -0
  434. package/src/server/storage/capabilities.ts +6 -0
  435. package/src/server/storage/local-object-store.cloudflare.ts +7 -0
  436. package/src/server/storage/local-object-store.ts +50 -0
  437. package/src/server/storage/object-store.ts +160 -0
  438. package/src/server/teams/creation.ts +51 -0
  439. package/src/server/teams/delete-policy.ts +13 -0
  440. package/src/server/teams/deletion-state.ts +80 -0
  441. package/src/server/teams/functions.ts +324 -0
  442. package/src/server/teams/invitations.ts +38 -0
  443. package/src/server/teams/membership.ts +86 -0
  444. package/src/server/teams/queries.ts +261 -0
  445. package/src/server/teams/service.ts +1127 -0
  446. package/src/server/teams/types.ts +101 -0
  447. package/src/server/uploads/avatar.ts +133 -0
  448. package/src/server/users/deletion-state.ts +93 -0
  449. package/src/server/users/functions.ts +160 -0
  450. package/src/server/users/onboarding.ts +17 -0
  451. package/src/server/users/service.ts +644 -0
  452. package/src/server.ts +55 -0
  453. package/src/styles/shadcn.css +653 -0
  454. package/src/styles/theme.css +76 -0
  455. package/src/styles.css +177 -0
  456. package/wrangler.template.jsonc +37 -0
@@ -0,0 +1,33 @@
1
+ import { sql } from 'drizzle-orm'
2
+ import { db } from '#devpush/server/db'
3
+ import type { AtomicStore } from '#devpush/server/db/atomic'
4
+
5
+ export const API_AUDIT_RETENTION_MS = 30 * 86400_000
6
+ const batchSize = 500
7
+
8
+ // Expired idempotency records may remain longer than the guaranteed replay
9
+ // window until maintenance runs. A maintenance run always does bounded work.
10
+ export async function pruneApiHistory(
11
+ store: AtomicStore = db,
12
+ now = Date.now(),
13
+ ) {
14
+ const auditBefore = now - API_AUDIT_RETENTION_MS
15
+ const [idempotency, audit, accessTokens, refreshTokens] = await store.batch([
16
+ store.all<{ id: string }>(sql`delete from api_idempotency where id in
17
+ (select id from api_idempotency where expires_at <= ${now} order by expires_at limit ${batchSize}) returning id`),
18
+ store.all<{ id: string }>(sql`delete from api_audit where id in
19
+ (select id from api_audit where created_at < ${auditBefore} order by created_at limit ${batchSize}) returning id`),
20
+ store.all<{ id: string }>(sql`delete from oauth_access_token where id in
21
+ (select id from oauth_access_token where expires_at <= ${now} order by expires_at limit ${batchSize}) returning id`),
22
+ // Preserve revoked/rotated refresh rows until expiry: their families still
23
+ // need reuse detection while any token in that family can be presented.
24
+ store.all<{ id: string }>(sql`delete from oauth_refresh_token where id in
25
+ (select id from oauth_refresh_token where expires_at <= ${now} order by expires_at limit ${batchSize}) returning id`),
26
+ ])
27
+ return {
28
+ idempotency: idempotency.length,
29
+ audit: audit.length,
30
+ accessTokens: accessTokens.length,
31
+ refreshTokens: refreshTokens.length,
32
+ }
33
+ }
@@ -0,0 +1,109 @@
1
+ import { eq, sql } from 'drizzle-orm'
2
+ import { isValidAllowlistPattern } from '#devpush/lib/allowlist-schema'
3
+
4
+ import { db } from '#devpush/server/db'
5
+ import { allowlist, user } from '#devpush/server/db/schema'
6
+
7
+ export { isValidAllowlistPattern } from '#devpush/lib/allowlist-schema'
8
+
9
+ export async function assertEmailCanAuthenticate(
10
+ email: string | null | undefined,
11
+ ) {
12
+ const normalizedEmail = normalizeEmail(email)
13
+
14
+ if (!normalizedEmail) {
15
+ throw new Error('Email address is required.')
16
+ }
17
+
18
+ const [existingUsers, allowlisted] = await Promise.all([
19
+ db
20
+ .select({ status: user.status })
21
+ .from(user)
22
+ .where(sql`lower(${user.email}) = ${normalizedEmail}`)
23
+ .limit(1),
24
+ isEmailAllowlisted(normalizedEmail),
25
+ ])
26
+ const existingUser = existingUsers.at(0)
27
+
28
+ if (existingUser?.status === 'banned') {
29
+ throw new Error('This account has been banned.')
30
+ }
31
+
32
+ if (existingUser?.status === 'deleted') {
33
+ throw new Error('This account has been deleted.')
34
+ }
35
+
36
+ if (!allowlisted) {
37
+ throw new Error('This email address is not allowed to sign in.')
38
+ }
39
+ }
40
+
41
+ export async function assertUserCanCreateSession(userId: string) {
42
+ const [currentUsers, entries] = await Promise.all([
43
+ db
44
+ .select({ email: user.email, status: user.status })
45
+ .from(user)
46
+ .where(eq(user.id, userId))
47
+ .limit(1),
48
+ listAllowlistEntries(),
49
+ ])
50
+ const currentUser = currentUsers.at(0)
51
+
52
+ if (!currentUser || currentUser.status !== 'active') {
53
+ throw new Error('This account cannot sign in.')
54
+ }
55
+
56
+ if (!isEmailAllowedByEntries(currentUser.email, entries)) {
57
+ throw new Error('This email address is not allowed to sign in.')
58
+ }
59
+ }
60
+
61
+ export async function isEmailAllowlisted(email: string) {
62
+ const normalizedEmail = normalizeEmail(email)
63
+ if (!normalizedEmail) return false
64
+
65
+ return isEmailAllowedByEntries(normalizedEmail, await listAllowlistEntries())
66
+ }
67
+
68
+ function listAllowlistEntries() {
69
+ return db
70
+ .select({ type: allowlist.type, value: allowlist.value })
71
+ .from(allowlist)
72
+ }
73
+
74
+ function isEmailAllowedByEntries(
75
+ email: string,
76
+ entries: Array<{ type: 'domain' | 'email' | 'pattern'; value: string }>,
77
+ ) {
78
+ const normalizedEmail = normalizeEmail(email)
79
+ if (!normalizedEmail) return false
80
+
81
+ if (entries.length === 0) {
82
+ return true
83
+ }
84
+
85
+ const domain = normalizedEmail.split('@').at(1) ?? ''
86
+
87
+ return entries.some((entry) => {
88
+ const value = entry.value.trim().toLowerCase()
89
+
90
+ if (entry.type === 'email') {
91
+ return value === normalizedEmail
92
+ }
93
+
94
+ if (entry.type === 'domain') {
95
+ return value.replace(/^@/, '') === domain
96
+ }
97
+
98
+ try {
99
+ if (!isValidAllowlistPattern(entry.value)) return false
100
+ return new RegExp(`^(?:${entry.value})$`, 'i').test(normalizedEmail)
101
+ } catch {
102
+ return false
103
+ }
104
+ })
105
+ }
106
+
107
+ export function normalizeEmail(email: string | null | undefined) {
108
+ return email?.trim().toLowerCase() || null
109
+ }
@@ -0,0 +1,28 @@
1
+ import { config } from '#devpush/server/config'
2
+
3
+ export type AuthMethods = {
4
+ email: boolean
5
+ github: boolean
6
+ google: boolean
7
+ }
8
+
9
+ export function getGitHubOAuthCredentials() {
10
+ const clientId = config.GITHUB_APP_CLIENT_ID
11
+ const clientSecret = config.GITHUB_APP_CLIENT_SECRET
12
+
13
+ if (!clientId || !clientSecret) {
14
+ return null
15
+ }
16
+
17
+ return { clientId, clientSecret }
18
+ }
19
+
20
+ export function getAuthMethods(): AuthMethods {
21
+ const githubCredentials = getGitHubOAuthCredentials()
22
+
23
+ return {
24
+ email: Boolean(config.EMAIL_FROM && config.RESEND_API_KEY),
25
+ github: Boolean(githubCredentials),
26
+ google: Boolean(config.GOOGLE_CLIENT_ID && config.GOOGLE_CLIENT_SECRET),
27
+ }
28
+ }
@@ -0,0 +1,518 @@
1
+ import { fetchGitHub, readGitHubJson } from '#devpush/server/github/http'
2
+ import { readProviderToken, storeProviderToken } from './provider-tokens'
3
+ import { drizzleAdapter } from '@better-auth/drizzle-adapter'
4
+ import { betterAuth } from 'better-auth'
5
+ import { emailOTP } from 'better-auth/plugins/email-otp'
6
+ import { tanstackStartCookies } from 'better-auth/tanstack-start'
7
+ import { and, eq, gt, sql } from 'drizzle-orm'
8
+ import { decodeJwt } from 'jose'
9
+ import type { GithubProfile, GoogleProfile } from 'better-auth/social-providers'
10
+
11
+ import {
12
+ getAuthMethods,
13
+ getGitHubOAuthCredentials,
14
+ } from '#devpush/server/auth/auth-methods'
15
+ import { db } from '#devpush/server/db'
16
+ import { replacePendingIdentity } from '#devpush/server/auth/provider-identity'
17
+ import * as schema from '#devpush/server/db/schema'
18
+ import {
19
+ assertEmailCanAuthenticate,
20
+ assertUserCanCreateSession,
21
+ } from '#devpush/server/auth/access'
22
+ import {
23
+ sendEmailChangeEmail,
24
+ sendEmailVerificationEmail,
25
+ sendOtpEmail,
26
+ } from '#devpush/server/email/service'
27
+ import { config } from '#devpush/server/config'
28
+ import { requireAuthEmailDelivery } from '#devpush/server/auth/email-delivery'
29
+ import {
30
+ assertAllowedAuthEmailChange,
31
+ requireAllowedEmailChanges,
32
+ } from '#devpush/server/auth/email-change'
33
+ import { ensureUserBootstrap } from '#devpush/server/teams/service'
34
+ import { decryptSecret, encryptSecret } from '#devpush/server/secrets/crypto'
35
+ import { mcpOAuthPlugins } from '#devpush/server/mcp/oauth-provider'
36
+
37
+ export const authMethods = getAuthMethods()
38
+ const githubCredentials = getGitHubOAuthCredentials()
39
+
40
+ type ProviderIdentity = {
41
+ name?: string | null
42
+ email?: string | null
43
+ username?: string | null
44
+ image?: string | null
45
+ }
46
+
47
+ type OAuthTokens = {
48
+ accessToken?: string
49
+ idToken?: string
50
+ }
51
+
52
+ type GitHubProfile = GithubProfile
53
+
54
+ type GitHubEmail = {
55
+ email: string
56
+ primary: boolean
57
+ verified: boolean
58
+ }
59
+
60
+ const pendingProviderIdentityTtlMs = 5 * 60 * 1000
61
+
62
+ function createAuth() {
63
+ return betterAuth({
64
+ appName: 'devpush',
65
+ secret: config.BETTER_AUTH_SECRET,
66
+ baseURL: config.BETTER_AUTH_URL,
67
+ basePath: '/api/auth',
68
+ // This server-only bridge is called directly by the MCP handler. OAuth
69
+ // protocol endpoints remain served by Better Auth.
70
+ disabledPaths: ['/mcp/verify', '/token', '/update-user'],
71
+ advanced: {
72
+ cookiePrefix: 'devpush',
73
+ },
74
+ database: drizzleAdapter(db, {
75
+ provider: 'sqlite',
76
+ schema,
77
+ }),
78
+ session: {
79
+ cookieCache: {
80
+ enabled: true,
81
+ maxAge: 60,
82
+ },
83
+ },
84
+ user: {
85
+ additionalFields: {
86
+ defaultTeamId: {
87
+ type: 'string',
88
+ required: false,
89
+ input: false,
90
+ },
91
+ username: {
92
+ type: 'string',
93
+ required: false,
94
+ input: false,
95
+ },
96
+ },
97
+ changeEmail: {
98
+ enabled: true,
99
+ sendChangeEmailConfirmation: async ({ newEmail, url, user }) => {
100
+ await sendEmailChangeEmail({
101
+ currentEmail: user.email,
102
+ newEmail,
103
+ verifyUrl: url,
104
+ })
105
+ },
106
+ },
107
+ },
108
+ emailVerification: {
109
+ sendVerificationEmail: async ({ url, user }) => {
110
+ await sendEmailVerificationEmail({
111
+ email: user.email,
112
+ verifyUrl: url,
113
+ })
114
+ },
115
+ },
116
+ databaseHooks: {
117
+ account: {
118
+ update: {
119
+ before: async (updatedAccount) => ({
120
+ data: {
121
+ ...updatedAccount,
122
+ ...(updatedAccount.idToken
123
+ ? { idToken: await storeProviderToken(updatedAccount.idToken) }
124
+ : {}),
125
+ },
126
+ }),
127
+ },
128
+ create: {
129
+ before: async (createdAccount) => {
130
+ const identity = await readPendingProviderIdentity(
131
+ getProviderIdentityKey(
132
+ createdAccount.providerId,
133
+ createdAccount.accountId,
134
+ ),
135
+ )
136
+
137
+ const idToken = createdAccount.idToken
138
+ ? await storeProviderToken(createdAccount.idToken)
139
+ : createdAccount.idToken
140
+ if (!identity) return { data: { idToken } }
141
+
142
+ return {
143
+ data: {
144
+ idToken,
145
+ providerAccountName: identity.name ?? undefined,
146
+ providerAccountEmail: identity.email ?? undefined,
147
+ providerAccountUsername: identity.username ?? undefined,
148
+ providerAccountImage: identity.image ?? undefined,
149
+ },
150
+ }
151
+ },
152
+ after: async (createdAccount) => {
153
+ await clearPendingProviderIdentity(
154
+ getProviderIdentityKey(
155
+ createdAccount.providerId,
156
+ createdAccount.accountId,
157
+ ),
158
+ )
159
+ },
160
+ },
161
+ },
162
+ session: {
163
+ create: {
164
+ before: async (createdSession) => {
165
+ await assertUserCanCreateSession(createdSession.userId)
166
+ },
167
+ },
168
+ },
169
+ user: {
170
+ update: {
171
+ before: async (updatedUser) => {
172
+ if (updatedUser.email)
173
+ await assertAllowedAuthEmailChange(updatedUser.email)
174
+ },
175
+ },
176
+ create: {
177
+ before: async (createdUser) => {
178
+ await assertEmailCanAuthenticate(createdUser.email)
179
+
180
+ return {
181
+ data: {
182
+ username: await getAvailableUsername(
183
+ createdUser.name || createdUser.email,
184
+ ),
185
+ },
186
+ }
187
+ },
188
+ after: async (createdUser) => {
189
+ try {
190
+ await ensureUserBootstrap(createdUser.id)
191
+ } catch (error) {
192
+ console.error('Failed to bootstrap user', error)
193
+ }
194
+ },
195
+ },
196
+ },
197
+ },
198
+ socialProviders: {
199
+ ...(githubCredentials
200
+ ? {
201
+ github: {
202
+ clientId: githubCredentials.clientId,
203
+ clientSecret: githubCredentials.clientSecret,
204
+ getUserInfo: getGitHubUserInfo,
205
+ },
206
+ }
207
+ : {}),
208
+ ...(authMethods.google
209
+ ? {
210
+ google: {
211
+ clientId: config.GOOGLE_CLIENT_ID as string,
212
+ clientSecret: config.GOOGLE_CLIENT_SECRET as string,
213
+ getUserInfo: getGoogleUserInfo,
214
+ },
215
+ }
216
+ : {}),
217
+ },
218
+ account: {
219
+ encryptOAuthTokens: true,
220
+ additionalFields: {
221
+ providerAccountName: {
222
+ type: 'string',
223
+ required: false,
224
+ input: false,
225
+ },
226
+ providerAccountEmail: {
227
+ type: 'string',
228
+ required: false,
229
+ input: false,
230
+ },
231
+ providerAccountUsername: {
232
+ type: 'string',
233
+ required: false,
234
+ input: false,
235
+ },
236
+ providerAccountImage: {
237
+ type: 'string',
238
+ required: false,
239
+ input: false,
240
+ },
241
+ },
242
+ accountLinking: {
243
+ allowDifferentEmails: false,
244
+ enabled: true,
245
+ trustedProviders: ['github', 'google'],
246
+ },
247
+ },
248
+ plugins: [
249
+ ...mcpOAuthPlugins(),
250
+ requireAllowedEmailChanges(),
251
+ requireAuthEmailDelivery(),
252
+ ...(authMethods.email
253
+ ? [
254
+ emailOTP({
255
+ allowedAttempts: 5,
256
+ expiresIn: 300,
257
+ resendStrategy: 'reuse',
258
+ storeOTP: 'encrypted',
259
+ sendVerificationOTP: async ({ email, otp, type }) => {
260
+ if (type !== 'sign-in') {
261
+ return
262
+ }
263
+
264
+ await assertEmailCanAuthenticate(email)
265
+ await sendOtpEmail(email, otp)
266
+ },
267
+ }),
268
+ ]
269
+ : []),
270
+ tanstackStartCookies(),
271
+ ],
272
+ })
273
+ }
274
+
275
+ type Auth = ReturnType<typeof createAuth>
276
+ let initializedAuth: Auth | undefined
277
+ // Better Auth plugin initialization may seed provider resources. Defer that I/O
278
+ // until a request uses the auth API; Workers cannot perform it at module scope.
279
+ export const auth = new Proxy({} as Auth, {
280
+ get(_target, property) {
281
+ const instance = (initializedAuth ??= createAuth())
282
+ const value: unknown = Reflect.get(instance, property, instance)
283
+ return typeof value === 'function' ? value.bind(instance) : value
284
+ },
285
+ })
286
+
287
+ function getProviderIdentityKey(providerId: string, accountId: string) {
288
+ return `${providerId}:${accountId}`
289
+ }
290
+
291
+ async function rememberProviderIdentity({
292
+ accountId,
293
+ providerId,
294
+ identity,
295
+ }: {
296
+ accountId: string
297
+ providerId: string
298
+ identity: ProviderIdentity
299
+ }) {
300
+ const key = getProviderIdentityKey(providerId, accountId)
301
+ const identifier = pendingProviderIdentityIdentifier(key)
302
+
303
+ await replacePendingIdentity(db, {
304
+ identifier,
305
+ value: encryptSecret(JSON.stringify(identity)),
306
+ expiresAt: new Date(Date.now() + pendingProviderIdentityTtlMs),
307
+ })
308
+ }
309
+
310
+ async function readPendingProviderIdentity(key: string) {
311
+ const identifier = pendingProviderIdentityIdentifier(key)
312
+ const rows = await db
313
+ .select({ value: schema.verification.value })
314
+ .from(schema.verification)
315
+ .where(
316
+ and(
317
+ eq(schema.verification.identifier, identifier),
318
+ gt(schema.verification.expiresAt, new Date()),
319
+ ),
320
+ )
321
+ .limit(1)
322
+ const value = rows[0]?.value
323
+
324
+ if (!value) {
325
+ return null
326
+ }
327
+
328
+ try {
329
+ const identity = JSON.parse(decryptSecret(value)) as unknown
330
+ return isProviderIdentity(identity) ? identity : null
331
+ } catch {
332
+ await clearPendingProviderIdentity(key)
333
+ return null
334
+ }
335
+ }
336
+
337
+ async function clearPendingProviderIdentity(key: string) {
338
+ await db
339
+ .delete(schema.verification)
340
+ .where(
341
+ eq(
342
+ schema.verification.identifier,
343
+ pendingProviderIdentityIdentifier(key),
344
+ ),
345
+ )
346
+ }
347
+
348
+ function pendingProviderIdentityIdentifier(key: string) {
349
+ return `devpush:provider-identity:${key}`
350
+ }
351
+
352
+ function isProviderIdentity(value: unknown): value is ProviderIdentity {
353
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
354
+ return false
355
+ }
356
+
357
+ return ['name', 'email', 'username', 'image'].every((field) => {
358
+ const item = (value as Record<string, unknown>)[field]
359
+ return item === undefined || item === null || typeof item === 'string'
360
+ })
361
+ }
362
+
363
+ export async function getGitHubUserInfo(token: OAuthTokens) {
364
+ if (!token.accessToken) {
365
+ return null
366
+ }
367
+
368
+ const headers = {
369
+ Accept: 'application/vnd.github+json',
370
+ Authorization: `Bearer ${token.accessToken}`,
371
+ 'User-Agent': 'devpush',
372
+ }
373
+ const [userResponse, emailsResponse] = await Promise.all([
374
+ fetchGitHub('https://api.github.com/user', { headers }),
375
+ fetchGitHub('https://api.github.com/user/emails', { headers }),
376
+ ])
377
+
378
+ if (!userResponse.ok) {
379
+ return null
380
+ }
381
+
382
+ const profile = await readGitHubJson<GitHubProfile>(userResponse)
383
+ if (!emailsResponse.ok) return null
384
+ const emails = await readGitHubJson<Array<GitHubEmail | null>>(emailsResponse)
385
+ if (!Array.isArray(emails)) return null
386
+ const verified = emails.filter(
387
+ (candidate): candidate is GitHubEmail =>
388
+ candidate?.verified === true && typeof candidate.email === 'string',
389
+ )
390
+ const email = (
391
+ verified.find((candidate) => candidate.primary === true) ??
392
+ verified.find((candidate) => candidate.email === profile.email) ??
393
+ verified.at(0)
394
+ )?.email
395
+ if (!email) return null
396
+
397
+ await assertEmailCanAuthenticate(email)
398
+
399
+ const accountId = String(profile.id)
400
+
401
+ await rememberProviderIdentity({
402
+ accountId,
403
+ providerId: 'github',
404
+ identity: {
405
+ name: profile.name,
406
+ email,
407
+ username: profile.login,
408
+ image: profile.avatar_url,
409
+ },
410
+ })
411
+
412
+ return {
413
+ user: {
414
+ name: profile.name || profile.login || '',
415
+ email,
416
+ image: profile.avatar_url,
417
+ emailVerified: true,
418
+ },
419
+ data: profile,
420
+ }
421
+ }
422
+
423
+ export async function getGoogleUserInfo(token: OAuthTokens) {
424
+ if (!token.idToken) {
425
+ return null
426
+ }
427
+
428
+ const claims = decodeJwt(await readProviderToken(token.idToken))
429
+ const accountId = getStringClaim(claims.sub)
430
+ const email = getStringClaim(claims.email)
431
+ const name = getStringClaim(claims.name)
432
+ const image = getStringClaim(claims.picture)
433
+
434
+ if (!accountId || !email || claims.email_verified !== true) {
435
+ return null
436
+ }
437
+
438
+ await assertEmailCanAuthenticate(email)
439
+
440
+ await rememberProviderIdentity({
441
+ accountId,
442
+ providerId: 'google',
443
+ identity: {
444
+ name,
445
+ email,
446
+ username: email,
447
+ image,
448
+ },
449
+ })
450
+
451
+ return {
452
+ user: {
453
+ name,
454
+ email,
455
+ image,
456
+ emailVerified: true,
457
+ },
458
+ data: claims as unknown as GoogleProfile,
459
+ }
460
+ }
461
+
462
+ function getStringClaim(value: unknown) {
463
+ return typeof value === 'string' && value.length > 0 ? value : undefined
464
+ }
465
+
466
+ async function getAvailableUsername(value: string) {
467
+ const base = normalizeUsername(value)
468
+ const existingRows = await db
469
+ .select({ username: schema.user.username })
470
+ .from(schema.user)
471
+ .where(
472
+ and(
473
+ sql`lower(${schema.user.username}) = ${base}`,
474
+ eq(schema.user.status, 'active'),
475
+ ),
476
+ )
477
+ .limit(1)
478
+
479
+ if (existingRows.length === 0) {
480
+ return base
481
+ }
482
+
483
+ for (let attempt = 0; attempt < 10; attempt += 1) {
484
+ const suffix = crypto.randomUUID().slice(0, 8)
485
+ const candidate = `${base.slice(0, 41)}-${suffix}`
486
+ const rows = await db
487
+ .select({ id: schema.user.id })
488
+ .from(schema.user)
489
+ .where(
490
+ and(
491
+ eq(schema.user.username, candidate),
492
+ eq(schema.user.status, 'active'),
493
+ ),
494
+ )
495
+ .limit(1)
496
+
497
+ if (rows.length === 0) {
498
+ return candidate
499
+ }
500
+ }
501
+
502
+ return `user-${crypto.randomUUID().slice(0, 12)}`
503
+ }
504
+
505
+ function normalizeUsername(value: string) {
506
+ const username = value
507
+ .split('@')[0]
508
+ ?.toLowerCase()
509
+ .replace(/[\s]+/g, '-')
510
+ .replace(/[^a-z0-9._-]/g, '')
511
+ .replace(/^[._-]+|[._-]+$/g, '')
512
+ .slice(0, 50)
513
+ .replace(/[._-]+$/g, '')
514
+
515
+ return username && username.length >= 2
516
+ ? username
517
+ : `user-${crypto.randomUUID().slice(0, 12)}`
518
+ }
@@ -0,0 +1,28 @@
1
+ import { createServerFn } from '@tanstack/react-start'
2
+ import { getAuthMethods } from '#devpush/server/auth/auth-methods'
3
+ import { getAuthSession } from '#devpush/server/auth/session.server'
4
+ import { getUserContext } from '#devpush/server/teams/service'
5
+
6
+ export type AuthUserContext = Awaited<ReturnType<typeof getUserContext>>
7
+ export const getLoginContext = createServerFn({ method: 'GET' }).handler(
8
+ async () => {
9
+ const session = await getAuthSession()
10
+
11
+ return {
12
+ authMethods: getAuthMethods(),
13
+ userContext: session ? await getUserContext(session.user.id) : null,
14
+ }
15
+ },
16
+ )
17
+
18
+ export const getCurrentUserContext = createServerFn({ method: 'GET' }).handler(
19
+ async () => {
20
+ const session = await getAuthSession()
21
+
22
+ if (!session) {
23
+ return null
24
+ }
25
+
26
+ return getUserContext(session.user.id)
27
+ },
28
+ )