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,831 @@
1
+ import { fetchGitHub, readGitHubJson } from '#devpush/server/github/http'
2
+ import { readProviderToken } from '#devpush/server/auth/provider-tokens'
3
+ import { and, desc, eq } from 'drizzle-orm'
4
+ import { createPrivateKey } from 'node:crypto'
5
+ import { SignJWT } from 'jose'
6
+ import { isAPIError } from 'better-auth/api'
7
+ import { PublicError } from '#devpush/lib/public-error'
8
+
9
+ import { config } from '#devpush/server/config'
10
+ import { db } from '#devpush/server/db'
11
+ import {
12
+ account,
13
+ githubInstallations,
14
+ projects,
15
+ } from '#devpush/server/db/schema'
16
+ import { getGitHubInstallationChange } from '#devpush/server/github/webhooks'
17
+ import type { GitHubInstallationWebhookEvent } from '#devpush/server/github/webhooks'
18
+
19
+ const githubApiBaseUrl = 'https://api.github.com'
20
+ const composeCandidates = [
21
+ 'compose.yml',
22
+ 'compose.yaml',
23
+ 'docker-compose.yml',
24
+ 'docker-compose.yaml',
25
+ ]
26
+
27
+ export type GitHubAccount = {
28
+ login: string
29
+ accessToken: string
30
+ }
31
+
32
+ export type GitHubInstallation = {
33
+ id: number
34
+ accountId: number
35
+ accountLogin: string
36
+ accountType: string
37
+ repositorySelection: string
38
+ status: 'active' | 'suspended'
39
+ }
40
+
41
+ export type GitHubRepository = {
42
+ id: number
43
+ installationId: number
44
+ name: string
45
+ fullName: string
46
+ owner: string
47
+ private: boolean
48
+ defaultBranch: string
49
+ updatedAt: string
50
+ permissions: {
51
+ push: boolean
52
+ }
53
+ }
54
+
55
+ type GitHubUserResponse = {
56
+ login: string
57
+ }
58
+
59
+ type GitHubInstallationResponse = {
60
+ id: number
61
+ account: {
62
+ id: number
63
+ login: string
64
+ type: string
65
+ }
66
+ repository_selection: string
67
+ suspended_at?: string | null
68
+ }
69
+
70
+ type GitHubInstallationsResponse = {
71
+ installations: Array<GitHubInstallationResponse>
72
+ total_count: number
73
+ }
74
+
75
+ type GitHubRepositoryResponse = {
76
+ id: number
77
+ name: string
78
+ full_name: string
79
+ private: boolean
80
+ default_branch: string
81
+ updated_at: string
82
+ owner: {
83
+ login: string
84
+ }
85
+ permissions?: {
86
+ push?: boolean
87
+ }
88
+ }
89
+
90
+ type GitHubBranchResponse = {
91
+ commit: {
92
+ sha: string
93
+ commit?: {
94
+ message?: string
95
+ }
96
+ }
97
+ }
98
+
99
+ type GitHubBranchListResponse = Array<{
100
+ name: string
101
+ }>
102
+
103
+ type GitHubCommitResponse = {
104
+ sha: string
105
+ commit?: {
106
+ message?: string
107
+ author?: {
108
+ date?: string
109
+ name?: string
110
+ }
111
+ }
112
+ html_url?: string
113
+ }
114
+
115
+ type GitHubRepositoriesResponse = {
116
+ repositories: Array<GitHubRepositoryResponse>
117
+ total_count: number
118
+ }
119
+
120
+ type GitHubContentResponse =
121
+ | {
122
+ type: 'file'
123
+ content: string
124
+ encoding: string
125
+ }
126
+ | {
127
+ type: string
128
+ content?: string
129
+ encoding?: string
130
+ }
131
+
132
+ let cachedJwt: { token: string; expiresAt: number } | null = null
133
+
134
+ export function isGitHubAppConfigured() {
135
+ return Boolean(
136
+ config.GITHUB_APP_ID &&
137
+ config.GITHUB_APP_NAME &&
138
+ config.GITHUB_APP_PRIVATE_KEY,
139
+ )
140
+ }
141
+
142
+ export function getGitHubAppInstallUrl() {
143
+ if (!config.GITHUB_APP_NAME) {
144
+ return null
145
+ }
146
+
147
+ return `https://github.com/apps/${config.GITHUB_APP_NAME}/installations/new`
148
+ }
149
+
150
+ export async function getGitHubAccount(
151
+ userId: string,
152
+ ): Promise<GitHubAccount | null> {
153
+ const accountRows = await db
154
+ .select({
155
+ id: account.id,
156
+ accessToken: account.accessToken,
157
+ accessTokenExpiresAt: account.accessTokenExpiresAt,
158
+ login: account.providerAccountUsername,
159
+ })
160
+ .from(account)
161
+ .where(and(eq(account.userId, userId), eq(account.providerId, 'github')))
162
+ .orderBy(desc(account.updatedAt))
163
+ .limit(1)
164
+ const githubAccount = accountRows.at(0)
165
+
166
+ if (!githubAccount?.accessToken) {
167
+ return null
168
+ }
169
+
170
+ let accessToken = await readProviderToken(githubAccount.accessToken)
171
+ if (
172
+ githubAccount.accessTokenExpiresAt &&
173
+ githubAccount.accessTokenExpiresAt.getTime() - Date.now() < 5_000
174
+ ) {
175
+ const { auth } = await import('#devpush/server/auth/better-auth')
176
+ try {
177
+ const refreshed = await auth.api.getAccessToken({
178
+ body: { accountId: githubAccount.id, userId },
179
+ })
180
+ if (
181
+ !refreshed.accessToken ||
182
+ (refreshed.accessTokenExpiresAt &&
183
+ new Date(refreshed.accessTokenExpiresAt).getTime() <= Date.now())
184
+ ) {
185
+ throw new PublicError('GITHUB_ACCESS_REFRESH_FAILED')
186
+ }
187
+ accessToken = refreshed.accessToken
188
+ } catch (error) {
189
+ if (
190
+ isAPIError(error) &&
191
+ [
192
+ 'FAILED_TO_GET_ACCESS_TOKEN',
193
+ 'PROVIDER_NOT_SUPPORTED',
194
+ 'ACCOUNT_NOT_FOUND',
195
+ ].includes(error.body?.code ?? '')
196
+ ) {
197
+ throw new PublicError('GITHUB_ACCESS_REFRESH_FAILED')
198
+ }
199
+ throw error
200
+ }
201
+ }
202
+
203
+ const login =
204
+ githubAccount.login ??
205
+ (await githubRequest<GitHubUserResponse>('/user', accessToken)).login
206
+
207
+ return {
208
+ login,
209
+ accessToken,
210
+ }
211
+ }
212
+
213
+ export async function listUserInstallations(
214
+ accessToken: string,
215
+ ): Promise<Array<GitHubInstallation>> {
216
+ const installations: Array<GitHubInstallation> = []
217
+ let page = 1
218
+ let hasMore = true
219
+
220
+ while (hasMore) {
221
+ const data = await githubRequest<GitHubInstallationsResponse>(
222
+ `/user/installations?per_page=100&page=${page}`,
223
+ accessToken,
224
+ )
225
+
226
+ installations.push(
227
+ ...data.installations.map((installation) => ({
228
+ id: installation.id,
229
+ accountId: installation.account.id,
230
+ accountLogin: installation.account.login,
231
+ accountType: installation.account.type,
232
+ repositorySelection: installation.repository_selection,
233
+ status: 'active' as const,
234
+ })),
235
+ )
236
+
237
+ if (data.installations.length < 100 || page * 100 >= data.total_count) {
238
+ hasMore = false
239
+ } else {
240
+ page += 1
241
+ }
242
+ }
243
+
244
+ return installations
245
+ }
246
+
247
+ export async function listUserInstallationsPage({
248
+ accessToken,
249
+ page,
250
+ perPage,
251
+ }: {
252
+ accessToken: string
253
+ page: number
254
+ perPage: number
255
+ }) {
256
+ const data = await githubRequest<GitHubInstallationsResponse>(
257
+ `/user/installations?per_page=${perPage}&page=${page}`,
258
+ accessToken,
259
+ )
260
+ return {
261
+ items: data.installations.map((installation) => ({
262
+ id: installation.id,
263
+ accountLogin: installation.account.login,
264
+ accountType: installation.account.type,
265
+ repositorySelection: installation.repository_selection,
266
+ })),
267
+ hasMore: page * perPage < data.total_count,
268
+ }
269
+ }
270
+
271
+ export async function listInstallationRepositoriesPage({
272
+ accessToken,
273
+ installationId,
274
+ page,
275
+ perPage,
276
+ }: {
277
+ accessToken: string
278
+ installationId: number
279
+ page: number
280
+ perPage: number
281
+ }) {
282
+ const data = await githubRequest<GitHubRepositoriesResponse>(
283
+ `/user/installations/${installationId}/repositories?per_page=${perPage}&page=${page}`,
284
+ accessToken,
285
+ )
286
+ return {
287
+ items: data.repositories.map((repository) =>
288
+ mapRepository(repository, installationId),
289
+ ),
290
+ hasMore: page * perPage < data.total_count,
291
+ }
292
+ }
293
+
294
+ export async function listInstallationRepositories({
295
+ accessToken,
296
+ installationId,
297
+ }: {
298
+ accessToken: string
299
+ installationId: number
300
+ }): Promise<Array<GitHubRepository>> {
301
+ const repositories: Array<GitHubRepository> = []
302
+ let page = 1
303
+ let hasMore = true
304
+
305
+ while (hasMore) {
306
+ const data = await githubRequest<GitHubRepositoriesResponse>(
307
+ `/user/installations/${installationId}/repositories?per_page=100&page=${page}`,
308
+ accessToken,
309
+ )
310
+
311
+ repositories.push(
312
+ ...data.repositories.map((repo) => mapRepository(repo, installationId)),
313
+ )
314
+
315
+ if (data.repositories.length < 100 || page * 100 >= data.total_count) {
316
+ hasMore = false
317
+ } else {
318
+ page += 1
319
+ }
320
+ }
321
+
322
+ return repositories
323
+ }
324
+
325
+ export async function getGitHubRepository(
326
+ accessToken: string,
327
+ fullName: string,
328
+ installationId: number,
329
+ ): Promise<GitHubRepository> {
330
+ const repo = await githubRequest<GitHubRepositoryResponse>(
331
+ `/repos/${encodeURIComponentRepoName(fullName)}`,
332
+ accessToken,
333
+ )
334
+
335
+ return mapRepository(repo, installationId)
336
+ }
337
+
338
+ export async function detectComposePath({
339
+ accessToken,
340
+ repo,
341
+ ref,
342
+ }: {
343
+ accessToken: string
344
+ repo: string
345
+ ref: string
346
+ }) {
347
+ for (const path of composeCandidates) {
348
+ const response = await fetchGitHub(
349
+ `${githubApiBaseUrl}/repos/${encodeURIComponentRepoName(repo)}/contents/${path}?ref=${encodeURIComponent(ref)}`,
350
+ {
351
+ headers: githubHeaders(accessToken),
352
+ },
353
+ )
354
+
355
+ if (response.status === 404) {
356
+ continue
357
+ }
358
+
359
+ if (!response.ok) {
360
+ throw new Error(await readGitHubError(response))
361
+ }
362
+
363
+ return path
364
+ }
365
+
366
+ return null
367
+ }
368
+
369
+ export async function getRepositoryFileText({
370
+ accessToken,
371
+ path,
372
+ ref,
373
+ repo,
374
+ }: {
375
+ accessToken: string
376
+ path: string
377
+ ref: string
378
+ repo: string
379
+ }) {
380
+ const data = await githubRequest<GitHubContentResponse>(
381
+ `/repos/${encodeURIComponentRepoName(repo)}/contents/${encodeURIComponentPath(path)}?ref=${encodeURIComponent(ref)}`,
382
+ accessToken,
383
+ )
384
+
385
+ if (data.type !== 'file' || !data.content) {
386
+ throw new Error(`${path} is not a file.`)
387
+ }
388
+
389
+ if (data.encoding !== 'base64') {
390
+ throw new Error(`${path} has unsupported encoding.`)
391
+ }
392
+
393
+ return Buffer.from(data.content.replace(/\s/g, ''), 'base64').toString('utf8')
394
+ }
395
+
396
+ export async function getBranchHeadCommit({
397
+ accessToken,
398
+ branch,
399
+ repo,
400
+ }: {
401
+ accessToken: string
402
+ branch: string
403
+ repo: string
404
+ }) {
405
+ if (
406
+ config.DEVPUSH_SMOKE_GITHUB_TOKEN &&
407
+ accessToken === config.DEVPUSH_SMOKE_GITHUB_TOKEN
408
+ ) {
409
+ return {
410
+ sha: 'smoke-branch-head',
411
+ message: `Smoke commit for ${repo}@${branch}`,
412
+ }
413
+ }
414
+
415
+ const data = await githubRequest<GitHubBranchResponse>(
416
+ `/repos/${encodeURIComponentRepoName(repo)}/branches/${encodeURIComponent(branch)}`,
417
+ accessToken,
418
+ )
419
+
420
+ return {
421
+ sha: data.commit.sha,
422
+ message: data.commit.commit?.message ?? null,
423
+ }
424
+ }
425
+
426
+ export async function listBranchCommits({
427
+ accessToken,
428
+ branch,
429
+ perPage = 5,
430
+ repo,
431
+ }: {
432
+ accessToken: string
433
+ branch: string
434
+ perPage?: number
435
+ repo: string
436
+ }) {
437
+ const data = await githubRequest<Array<GitHubCommitResponse>>(
438
+ `/repos/${encodeURIComponentRepoName(repo)}/commits?sha=${encodeURIComponent(branch)}&per_page=${perPage}`,
439
+ accessToken,
440
+ )
441
+
442
+ return data.map(mapCommit)
443
+ }
444
+
445
+ export async function listRepositoryBranches({
446
+ accessToken,
447
+ repo,
448
+ }: {
449
+ accessToken: string
450
+ repo: string
451
+ }) {
452
+ if (
453
+ config.DEVPUSH_SMOKE_GITHUB_TOKEN &&
454
+ accessToken === config.DEVPUSH_SMOKE_GITHUB_TOKEN
455
+ ) {
456
+ return ['main']
457
+ }
458
+
459
+ const branches: Array<string> = []
460
+ let page = 1
461
+ let hasMore = true
462
+
463
+ while (hasMore) {
464
+ const data = await githubRequest<GitHubBranchListResponse>(
465
+ `/repos/${encodeURIComponentRepoName(repo)}/branches?per_page=100&page=${page}`,
466
+ accessToken,
467
+ )
468
+
469
+ branches.push(...data.map((branch) => branch.name))
470
+
471
+ hasMore = data.length === 100
472
+ page += 1
473
+ }
474
+
475
+ return branches
476
+ }
477
+
478
+ export async function listRepositoryBranchesPage({
479
+ accessToken,
480
+ repo,
481
+ page,
482
+ perPage,
483
+ }: {
484
+ accessToken: string
485
+ repo: string
486
+ page: number
487
+ perPage: number
488
+ }) {
489
+ if (
490
+ config.DEVPUSH_SMOKE_GITHUB_TOKEN &&
491
+ accessToken === config.DEVPUSH_SMOKE_GITHUB_TOKEN
492
+ )
493
+ return { items: page === 1 ? ['main'] : [], hasMore: false }
494
+ const branches = await githubRequest<GitHubBranchListResponse>(
495
+ `/repos/${encodeURIComponentRepoName(repo)}/branches?per_page=${perPage}&page=${page}`,
496
+ accessToken,
497
+ )
498
+ // One additional empty page is possible when GitHub returns an exact multiple
499
+ // of perPage; continuation never loses branches or fetches every page upfront.
500
+ return {
501
+ items: branches.map((branch) => branch.name),
502
+ hasMore: branches.length === perPage,
503
+ }
504
+ }
505
+
506
+ export async function getAppInstallation(installationId: number) {
507
+ const jwt = await getGitHubAppJwt()
508
+
509
+ const installation = await githubRequest<GitHubInstallationResponse>(
510
+ `/app/installations/${installationId}`,
511
+ jwt,
512
+ )
513
+
514
+ return mapInstallation(installation)
515
+ }
516
+
517
+ async function getRepositoryAppInstallation(repositoryFullName: string) {
518
+ const jwt = await getGitHubAppJwt()
519
+ const installation = await githubRequest<GitHubInstallationResponse>(
520
+ `/repos/${encodeURIComponentRepoName(repositoryFullName)}/installation`,
521
+ jwt,
522
+ )
523
+
524
+ return mapInstallation(installation)
525
+ }
526
+
527
+ export async function createInstallationAccessToken({
528
+ installationId,
529
+ repositoryId,
530
+ repositoryFullName,
531
+ }: {
532
+ installationId: number
533
+ repositoryId: number | null
534
+ repositoryFullName: string
535
+ }) {
536
+ if (config.DEVPUSH_SMOKE_GITHUB_TOKEN) {
537
+ return {
538
+ token: config.DEVPUSH_SMOKE_GITHUB_TOKEN,
539
+ expiresAt: new Date(Date.now() + 60 * 60 * 1000).toISOString(),
540
+ }
541
+ }
542
+
543
+ try {
544
+ return await requestInstallationAccessToken({
545
+ installationId,
546
+ repositoryId,
547
+ })
548
+ } catch (error) {
549
+ if (!(error instanceof GitHubApiError) || error.status !== 404) {
550
+ throw error
551
+ }
552
+
553
+ const installation = await getRepositoryAppInstallation(repositoryFullName)
554
+
555
+ if (installation.id === installationId) {
556
+ throw error
557
+ }
558
+
559
+ await upsertGitHubInstallation(installation)
560
+ await db
561
+ .update(projects)
562
+ .set({
563
+ githubInstallationId: installation.id,
564
+ updatedAt: new Date(),
565
+ })
566
+ .where(
567
+ and(
568
+ eq(projects.githubInstallationId, installationId),
569
+ eq(projects.githubRepo, repositoryFullName),
570
+ ),
571
+ )
572
+ await markGitHubInstallationStatus(installationId, 'deleted')
573
+
574
+ return requestInstallationAccessToken({
575
+ installationId: installation.id,
576
+ repositoryId,
577
+ })
578
+ }
579
+ }
580
+
581
+ export async function upsertGitHubInstallation(
582
+ installation: GitHubInstallation,
583
+ ) {
584
+ await db
585
+ .insert(githubInstallations)
586
+ .values({
587
+ installationId: installation.id,
588
+ accountId: installation.accountId,
589
+ accountLogin: installation.accountLogin,
590
+ accountType: installation.accountType,
591
+ repositorySelection: installation.repositorySelection,
592
+ status: installation.status,
593
+ updatedAt: new Date(),
594
+ })
595
+ .onConflictDoUpdate({
596
+ target: githubInstallations.installationId,
597
+ set: {
598
+ accountId: installation.accountId,
599
+ accountLogin: installation.accountLogin,
600
+ accountType: installation.accountType,
601
+ repositorySelection: installation.repositorySelection,
602
+ status: installation.status,
603
+ updatedAt: new Date(),
604
+ },
605
+ })
606
+ }
607
+
608
+ export async function handleGitHubInstallationWebhook({
609
+ action,
610
+ event,
611
+ installationId,
612
+ }: {
613
+ action: string
614
+ event: GitHubInstallationWebhookEvent
615
+ installationId: number
616
+ }) {
617
+ const change = getGitHubInstallationChange(event, action)
618
+
619
+ if (change.type === 'status') {
620
+ await markGitHubInstallationStatus(installationId, change.status)
621
+ return
622
+ }
623
+
624
+ if (change.type === 'refresh') {
625
+ await upsertGitHubInstallation(await getAppInstallation(installationId))
626
+ }
627
+ }
628
+
629
+ async function markGitHubInstallationStatus(
630
+ installationId: number,
631
+ status: 'active' | 'deleted' | 'suspended',
632
+ ) {
633
+ await db
634
+ .update(githubInstallations)
635
+ .set({ status, updatedAt: new Date() })
636
+ .where(eq(githubInstallations.installationId, installationId))
637
+ }
638
+
639
+ function mapRepository(
640
+ repo: GitHubRepositoryResponse,
641
+ installationId: number,
642
+ ): GitHubRepository {
643
+ return {
644
+ id: repo.id,
645
+ installationId,
646
+ name: repo.name,
647
+ fullName: repo.full_name,
648
+ owner: repo.owner.login,
649
+ private: repo.private,
650
+ defaultBranch: repo.default_branch,
651
+ updatedAt: repo.updated_at,
652
+ permissions: {
653
+ push: repo.permissions?.push ?? false,
654
+ },
655
+ }
656
+ }
657
+
658
+ function mapInstallation(
659
+ installation: GitHubInstallationResponse,
660
+ ): GitHubInstallation {
661
+ return {
662
+ id: installation.id,
663
+ accountId: installation.account.id,
664
+ accountLogin: installation.account.login,
665
+ accountType: installation.account.type,
666
+ repositorySelection: installation.repository_selection,
667
+ status: installation.suspended_at ? 'suspended' : 'active',
668
+ }
669
+ }
670
+
671
+ async function requestInstallationAccessToken({
672
+ installationId,
673
+ repositoryId,
674
+ }: {
675
+ installationId: number
676
+ repositoryId: number | null
677
+ }) {
678
+ const jwt = await getGitHubAppJwt()
679
+ const response = await fetchGitHub(
680
+ `${githubApiBaseUrl}/app/installations/${installationId}/access_tokens`,
681
+ {
682
+ method: 'POST',
683
+ headers: {
684
+ ...githubHeaders(jwt),
685
+ 'Content-Type': 'application/json',
686
+ },
687
+ body: JSON.stringify({
688
+ ...(repositoryId ? { repository_ids: [repositoryId] } : {}),
689
+ permissions: {
690
+ contents: 'read',
691
+ metadata: 'read',
692
+ },
693
+ }),
694
+ },
695
+ )
696
+
697
+ if (!response.ok) {
698
+ throw new GitHubApiError(response.status, await readGitHubError(response))
699
+ }
700
+
701
+ const data = await readGitHubJson<{ token: string; expires_at: string }>(
702
+ response,
703
+ )
704
+
705
+ return {
706
+ token: data.token,
707
+ expiresAt: data.expires_at,
708
+ installationId,
709
+ }
710
+ }
711
+
712
+ class GitHubApiError extends Error {
713
+ constructor(
714
+ readonly status: number,
715
+ message: string,
716
+ ) {
717
+ super(message)
718
+ this.name = 'GitHubApiError'
719
+ }
720
+ }
721
+
722
+ function mapCommit(commit: GitHubCommitResponse) {
723
+ return {
724
+ sha: commit.sha,
725
+ message: commit.commit?.message ?? null,
726
+ authorName: commit.commit?.author?.name ?? null,
727
+ authoredAt: commit.commit?.author?.date ?? null,
728
+ url: commit.html_url ?? null,
729
+ }
730
+ }
731
+
732
+ async function githubRequest<T>(path: string, accessToken: string): Promise<T> {
733
+ const response = await fetchGitHub(`${githubApiBaseUrl}${path}`, {
734
+ headers: githubHeaders(accessToken),
735
+ })
736
+
737
+ if (!response.ok) {
738
+ throw new Error(await readGitHubError(response))
739
+ }
740
+
741
+ return readGitHubJson<T>(response)
742
+ }
743
+
744
+ function githubHeaders(accessToken: string) {
745
+ return {
746
+ Accept: 'application/vnd.github+json',
747
+ Authorization: `Bearer ${accessToken}`,
748
+ 'User-Agent': 'devpush',
749
+ 'X-GitHub-Api-Version': '2022-11-28',
750
+ }
751
+ }
752
+
753
+ async function getGitHubAppJwt() {
754
+ if (!config.GITHUB_APP_ID || !config.GITHUB_APP_PRIVATE_KEY) {
755
+ throw new Error('GitHub App is not configured.')
756
+ }
757
+
758
+ const now = Math.floor(Date.now() / 1000)
759
+
760
+ if (cachedJwt && cachedJwt.expiresAt - now > 60) {
761
+ return cachedJwt.token
762
+ }
763
+
764
+ const privateKey = createPrivateKey(
765
+ normalizePrivateKey(config.GITHUB_APP_PRIVATE_KEY),
766
+ )
767
+ const expiresAt = now + 10 * 60
768
+ const token = await new SignJWT({})
769
+ .setProtectedHeader({ alg: 'RS256' })
770
+ .setIssuedAt(now - 60)
771
+ .setExpirationTime(expiresAt)
772
+ .setIssuer(config.GITHUB_APP_ID)
773
+ .sign(privateKey)
774
+
775
+ cachedJwt = { token, expiresAt }
776
+
777
+ return token
778
+ }
779
+
780
+ function normalizePrivateKey(value: string) {
781
+ const trimmed = value.trim()
782
+ const unquoted =
783
+ (trimmed.startsWith('"') && trimmed.endsWith('"')) ||
784
+ (trimmed.startsWith("'") && trimmed.endsWith("'"))
785
+ ? trimmed.slice(1, -1)
786
+ : trimmed
787
+
788
+ const withNewlines = unquoted.replace(/\\n/g, '\n')
789
+ if (withNewlines.includes('\n')) {
790
+ return withNewlines
791
+ }
792
+
793
+ const singleLinePem = withNewlines.match(
794
+ /^-----BEGIN ([A-Z ]+)-----(.+)-----END \1-----$/,
795
+ )
796
+ if (!singleLinePem) {
797
+ return withNewlines
798
+ }
799
+
800
+ const [, label, body] = singleLinePem
801
+ if (label === undefined || body === undefined) return withNewlines
802
+ const chunks = body.match(/.{1,64}/g) ?? []
803
+ return [
804
+ `-----BEGIN ${label}-----`,
805
+ ...chunks,
806
+ `-----END ${label}-----`,
807
+ '',
808
+ ].join('\n')
809
+ }
810
+
811
+ async function readGitHubError(response: Response) {
812
+ try {
813
+ const body = await readGitHubJson<{ message?: string }>(response)
814
+
815
+ if (body.message) {
816
+ return body.message
817
+ }
818
+ } catch {
819
+ // Fall through to generic HTTP error.
820
+ }
821
+
822
+ return `GitHub request failed with ${response.status}`
823
+ }
824
+
825
+ function encodeURIComponentRepoName(fullName: string) {
826
+ return fullName.split('/').map(encodeURIComponent).join('/')
827
+ }
828
+
829
+ function encodeURIComponentPath(path: string) {
830
+ return path.split('/').map(encodeURIComponent).join('/')
831
+ }