openship 0.1.0 → 0.1.1

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 (531) hide show
  1. package/.env.example +19 -3
  2. package/CONTRIBUTING.md +69 -19
  3. package/LICENSE +661 -0
  4. package/README.md +304 -108
  5. package/apps/api/Dockerfile +6 -8
  6. package/apps/api/package.json +25 -21
  7. package/apps/api/src/app.ts +46 -3
  8. package/apps/api/src/config/env.ts +81 -7
  9. package/apps/api/src/lib/auth-mode.ts +42 -0
  10. package/apps/api/src/lib/auth.ts +105 -0
  11. package/apps/api/src/lib/cache.ts +128 -0
  12. package/apps/api/src/lib/cloud-auth-proxy.ts +310 -0
  13. package/apps/api/src/lib/cloud-client.ts +103 -0
  14. package/apps/api/src/lib/compose-parser.ts +138 -0
  15. package/apps/api/src/lib/controller-helpers.ts +90 -0
  16. package/apps/api/src/lib/email-templates.ts +94 -0
  17. package/apps/api/src/lib/encryption.ts +102 -0
  18. package/apps/api/src/lib/local-user.ts +65 -0
  19. package/apps/api/src/lib/mail.ts +48 -0
  20. package/apps/api/src/lib/notifications.ts +195 -0
  21. package/apps/api/src/lib/openship-cloud.ts +101 -0
  22. package/apps/api/src/lib/resources.ts +96 -0
  23. package/apps/api/src/lib/screenshots.ts +189 -0
  24. package/apps/api/src/lib/ssh-manager.ts +369 -0
  25. package/apps/api/src/lib/ssl-scheduler.ts +109 -0
  26. package/apps/api/src/lib/stack-detector.ts +708 -0
  27. package/apps/api/src/lib/system-debug.ts +10 -0
  28. package/apps/api/src/middleware/auth.ts +49 -13
  29. package/apps/api/src/middleware/error-handler.ts +23 -3
  30. package/apps/api/src/middleware/index.ts +2 -0
  31. package/apps/api/src/middleware/internal-auth.ts +40 -0
  32. package/apps/api/src/middleware/local-only.ts +20 -0
  33. package/apps/api/src/middleware/rate-limiter.ts +9 -0
  34. package/apps/api/src/modules/analytics/analytics.controller.ts +111 -0
  35. package/apps/api/src/modules/analytics/analytics.routes.ts +29 -0
  36. package/apps/api/src/modules/analytics/analytics.schema.ts +37 -0
  37. package/apps/api/src/modules/analytics/analytics.service.ts +264 -0
  38. package/apps/api/src/modules/auth/auth.routes.ts +233 -6
  39. package/apps/api/src/modules/billing/billing.routes.ts +8 -0
  40. package/apps/api/src/modules/cloud/cloud.controller.ts +225 -0
  41. package/apps/api/src/modules/cloud/cloud.routes.ts +40 -0
  42. package/apps/api/src/modules/deployments/build.service.ts +791 -0
  43. package/apps/api/src/modules/deployments/deployment-lifecycle.ts +138 -0
  44. package/apps/api/src/modules/deployments/deployment.controller.ts +324 -16
  45. package/apps/api/src/modules/deployments/deployment.routes.ts +33 -7
  46. package/apps/api/src/modules/deployments/deployment.schema.ts +37 -5
  47. package/apps/api/src/modules/deployments/deployment.service.ts +178 -11
  48. package/apps/api/src/modules/deployments/preflight.ts +280 -0
  49. package/apps/api/src/modules/deployments/prepare.service.ts +252 -0
  50. package/apps/api/src/modules/deployments/session-manager.ts +283 -0
  51. package/apps/api/src/modules/deployments/ssl.service.ts +73 -0
  52. package/apps/api/src/modules/domains/domain.controller.ts +59 -7
  53. package/apps/api/src/modules/domains/domain.routes.ts +20 -5
  54. package/apps/api/src/modules/domains/domain.schema.ts +35 -0
  55. package/apps/api/src/modules/domains/domain.service.ts +375 -9
  56. package/apps/api/src/modules/github/github.auth.ts +397 -0
  57. package/apps/api/src/modules/github/github.controller.ts +387 -0
  58. package/apps/api/src/modules/github/github.local-auth.ts +251 -0
  59. package/apps/api/src/modules/github/github.routes.ts +50 -0
  60. package/apps/api/src/modules/github/github.schema.ts +52 -0
  61. package/apps/api/src/modules/github/github.service.ts +592 -0
  62. package/apps/api/src/modules/github/github.types.ts +158 -0
  63. package/apps/api/src/modules/github/github.webhook.ts +186 -0
  64. package/apps/api/src/modules/github/index.ts +16 -0
  65. package/apps/api/src/modules/health/health.routes.ts +35 -0
  66. package/apps/api/src/modules/projects/project.controller.ts +481 -12
  67. package/apps/api/src/modules/projects/project.routes.ts +63 -6
  68. package/apps/api/src/modules/projects/project.schema.ts +104 -6
  69. package/apps/api/src/modules/projects/project.service.ts +502 -11
  70. package/apps/api/src/modules/settings/settings.controller.ts +55 -0
  71. package/apps/api/src/modules/settings/settings.routes.ts +25 -0
  72. package/apps/api/src/modules/settings/settings.service.ts +49 -0
  73. package/apps/api/src/modules/system/filesystem.controller.ts +66 -0
  74. package/apps/api/src/modules/system/index.ts +1 -0
  75. package/apps/api/src/modules/system/server-check.controller.ts +475 -0
  76. package/apps/api/src/modules/system/setup-session.ts +255 -0
  77. package/apps/api/src/modules/system/setup.controller.ts +128 -0
  78. package/apps/api/src/modules/system/system.routes.ts +42 -0
  79. package/apps/api/src/modules/webhooks/webhook.controller.ts +57 -15
  80. package/apps/api/src/modules/webhooks/webhook.routes.ts +11 -8
  81. package/apps/api/src/modules/webhooks/webhook.service.ts +46 -5
  82. package/apps/api/src/modules/webhooks/webhook.types.ts +35 -0
  83. package/apps/api/tsconfig.json +3 -1
  84. package/apps/cli/package.json +8 -10
  85. package/apps/cli/tsconfig.json +2 -1
  86. package/apps/dashboard/Dockerfile +4 -6
  87. package/apps/dashboard/components.json +20 -0
  88. package/apps/dashboard/next-env.d.ts +6 -0
  89. package/apps/dashboard/next.config.mjs +11 -1
  90. package/apps/dashboard/package.json +25 -13
  91. package/apps/dashboard/postcss.config.mjs +5 -0
  92. package/apps/dashboard/public/android-chrome-192x192.png +0 -0
  93. package/apps/dashboard/public/android-chrome-512x512.png +0 -0
  94. package/apps/dashboard/public/apple-touch-icon.png +0 -0
  95. package/apps/dashboard/public/favicon-16x16.png +0 -0
  96. package/apps/dashboard/public/favicon-32x32.png +0 -0
  97. package/apps/dashboard/public/favicon.ico +0 -0
  98. package/apps/dashboard/public/site.webmanifest +19 -0
  99. package/apps/dashboard/src/app/(auth)/forgot-password/page.tsx +114 -0
  100. package/apps/dashboard/src/app/(auth)/layout.tsx +18 -3
  101. package/apps/dashboard/src/app/(auth)/login/page.tsx +215 -6
  102. package/apps/dashboard/src/app/(auth)/providers.tsx +34 -0
  103. package/apps/dashboard/src/app/(auth)/register/page.tsx +171 -5
  104. package/apps/dashboard/src/app/(auth)/reset-password/page.tsx +172 -0
  105. package/apps/dashboard/src/app/(auth)/verify-email/page.tsx +190 -0
  106. package/apps/dashboard/src/app/(dashboard)/(deployment)/build/[id]/page.tsx +162 -0
  107. package/apps/dashboard/src/app/(dashboard)/(deployment)/deploy/[slug]/components/BuildSummary.tsx +166 -0
  108. package/apps/dashboard/src/app/(dashboard)/(deployment)/deploy/[slug]/components/DnsConfiguration.tsx +119 -0
  109. package/apps/dashboard/src/app/(dashboard)/(deployment)/deploy/[slug]/components/DomainSettings.tsx +291 -0
  110. package/apps/dashboard/src/app/(dashboard)/(deployment)/deploy/[slug]/components/Header.tsx +71 -0
  111. package/apps/dashboard/src/app/(dashboard)/(deployment)/deploy/[slug]/components/Sidebar.tsx +270 -0
  112. package/apps/dashboard/src/app/(dashboard)/(deployment)/deploy/[slug]/components/SkeletonLoader.tsx +87 -0
  113. package/apps/dashboard/src/app/(dashboard)/(deployment)/deploy/[slug]/page.tsx +169 -0
  114. package/apps/dashboard/src/app/(dashboard)/(deployment)/layout.tsx +7 -0
  115. package/apps/dashboard/src/app/(dashboard)/billing/page.tsx +1 -8
  116. package/apps/dashboard/src/app/(dashboard)/deployments/components/CommitDetailsModal.tsx +240 -0
  117. package/apps/dashboard/src/app/(dashboard)/deployments/components/DeploymentCard.tsx +121 -0
  118. package/apps/dashboard/src/app/(dashboard)/deployments/components/DeploymentHeader.tsx +89 -0
  119. package/apps/dashboard/src/app/(dashboard)/deployments/components/DeploymentMenu.tsx +182 -0
  120. package/apps/dashboard/src/app/(dashboard)/deployments/components/DeploymentsContent.tsx +324 -0
  121. package/apps/dashboard/src/app/(dashboard)/deployments/components/DeploymentsFilters.tsx +99 -0
  122. package/apps/dashboard/src/app/(dashboard)/deployments/components/DeploymentsList.tsx +31 -0
  123. package/apps/dashboard/src/app/(dashboard)/deployments/components/EmptyState.tsx +101 -0
  124. package/apps/dashboard/src/app/(dashboard)/deployments/components/LoadingSkeleton.tsx +57 -0
  125. package/apps/dashboard/src/app/(dashboard)/deployments/components/ProjectFilter.tsx +109 -0
  126. package/apps/dashboard/src/app/(dashboard)/deployments/components/index.ts +9 -0
  127. package/apps/dashboard/src/app/(dashboard)/deployments/page.tsx +9 -3
  128. package/apps/dashboard/src/app/(dashboard)/deployments/types.ts +44 -0
  129. package/apps/dashboard/src/app/(dashboard)/deployments/utils.ts +176 -0
  130. package/apps/dashboard/src/app/(dashboard)/domains/page.tsx +1 -6
  131. package/apps/dashboard/src/app/(dashboard)/integrations/page.tsx +321 -0
  132. package/apps/dashboard/src/app/(dashboard)/layout.tsx +22 -32
  133. package/apps/dashboard/src/app/(dashboard)/library/components/ConnectPrompt.tsx +187 -0
  134. package/apps/dashboard/src/app/(dashboard)/library/components/LibrarySidebar.tsx +147 -0
  135. package/apps/dashboard/src/app/(dashboard)/library/components/LoadingSkeleton.tsx +39 -0
  136. package/apps/dashboard/src/app/(dashboard)/library/components/LocalProjects.tsx +548 -0
  137. package/apps/dashboard/src/app/(dashboard)/library/components/RepositoryList.tsx +292 -0
  138. package/apps/dashboard/src/app/(dashboard)/library/components/TemplateGrid.tsx +36 -0
  139. package/apps/dashboard/src/app/(dashboard)/library/components/UrlImport.tsx +74 -0
  140. package/apps/dashboard/src/app/(dashboard)/library/page.tsx +121 -0
  141. package/apps/dashboard/src/app/(dashboard)/library/types.ts +26 -0
  142. package/apps/dashboard/src/app/(dashboard)/monitoring/page.tsx +1 -6
  143. package/apps/dashboard/src/app/(dashboard)/page.tsx +485 -0
  144. package/apps/dashboard/src/app/(dashboard)/projects/[id]/[[...slug]]/layout.tsx +22 -0
  145. package/apps/dashboard/src/app/(dashboard)/projects/[id]/[[...slug]]/page.tsx +286 -0
  146. package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/AdvancedSettings.tsx +235 -0
  147. package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/BuildSettings.tsx +174 -0
  148. package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/DeleteConfirmationDialog.tsx +45 -0
  149. package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/DeletionModal.tsx +95 -0
  150. package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/Deployments.tsx +11 -0
  151. package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/DomainSettings.tsx +523 -0
  152. package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/EnvironmentSettings.tsx +246 -0
  153. package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/GeneralSettings.tsx +93 -0
  154. package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/GitInfo.tsx +261 -0
  155. package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/GitSettings.tsx +391 -0
  156. package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/LogsSettings.tsx +133 -0
  157. package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/ProjectNotFound.tsx +95 -0
  158. package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/ProjectsBottomNavigation.tsx +62 -0
  159. package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/ResourceSettings.tsx +333 -0
  160. package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/SettingSection.tsx +66 -0
  161. package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/SettingsNavigation.tsx +27 -0
  162. package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/SleepModeSettings.tsx +134 -0
  163. package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/general/ProductionUrl.tsx +25 -0
  164. package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/general/ProjectIdentity.tsx +159 -0
  165. package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/general/ProjectInfo.tsx +58 -0
  166. package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/general/SkeletonLoader.tsx +170 -0
  167. package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/general/StatsCards.tsx +33 -0
  168. package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/general/TopPaths.tsx +56 -0
  169. package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/general/TrafficChart.tsx +157 -0
  170. package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/general/index.ts +14 -0
  171. package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/logs/LogsActions.tsx +52 -0
  172. package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/logs/ServerLogs.tsx +249 -0
  173. package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/logs/TerminalLogs.tsx +682 -0
  174. package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/logs/logs.css +127 -0
  175. package/apps/dashboard/src/app/(dashboard)/projects/components/DeploymentCard.tsx +73 -0
  176. package/apps/dashboard/src/app/(dashboard)/projects/components/ProjectCard.tsx +330 -0
  177. package/apps/dashboard/src/app/(dashboard)/projects/page.tsx +326 -4
  178. package/apps/dashboard/src/app/(dashboard)/providers.tsx +23 -0
  179. package/apps/dashboard/src/app/(dashboard)/servers/[serverId]/_components/components-tab.tsx +329 -0
  180. package/apps/dashboard/src/app/(dashboard)/servers/[serverId]/_components/overview-tab.tsx +229 -0
  181. package/apps/dashboard/src/app/(dashboard)/servers/[serverId]/_components/terminal-tab.tsx +38 -0
  182. package/apps/dashboard/src/app/(dashboard)/servers/[serverId]/page.tsx +437 -0
  183. package/apps/dashboard/src/app/(dashboard)/servers/new/_components/checking-state.tsx +15 -0
  184. package/apps/dashboard/src/app/(dashboard)/servers/new/_components/choose-mode.tsx +54 -0
  185. package/apps/dashboard/src/app/(dashboard)/servers/new/_components/component-row.tsx +65 -0
  186. package/apps/dashboard/src/app/(dashboard)/servers/new/_components/error-banner.tsx +13 -0
  187. package/apps/dashboard/src/app/(dashboard)/servers/new/_components/installing-panel.tsx +227 -0
  188. package/apps/dashboard/src/app/(dashboard)/servers/new/_components/results-panel.tsx +90 -0
  189. package/apps/dashboard/src/app/(dashboard)/servers/new/_components/setup-header.tsx +56 -0
  190. package/apps/dashboard/src/app/(dashboard)/servers/new/_components/types.ts +13 -0
  191. package/apps/dashboard/src/app/(dashboard)/servers/new/page.tsx +666 -0
  192. package/apps/dashboard/src/app/(dashboard)/servers/page.tsx +283 -0
  193. package/apps/dashboard/src/app/(dashboard)/settings/_components/BuildPreferences.tsx +110 -0
  194. package/apps/dashboard/src/app/(dashboard)/settings/_components/CloudConnection.tsx +147 -0
  195. package/apps/dashboard/src/app/(dashboard)/settings/_components/InstanceInfo.tsx +54 -0
  196. package/apps/dashboard/src/app/(dashboard)/settings/_components/ServerConnection.tsx +407 -0
  197. package/apps/dashboard/src/app/(dashboard)/settings/_components/SettingsSection.tsx +32 -0
  198. package/apps/dashboard/src/app/(dashboard)/settings/page.tsx +62 -3
  199. package/apps/dashboard/src/app/auth/callback/close/page.tsx +23 -0
  200. package/apps/dashboard/src/app/auth/callback/install/page.tsx +40 -0
  201. package/apps/dashboard/src/app/authorize/page.tsx +137 -0
  202. package/apps/dashboard/src/app/globals.css +57 -3
  203. package/apps/dashboard/src/app/layout.tsx +43 -3
  204. package/apps/dashboard/src/components/auth-shell.tsx +66 -0
  205. package/apps/dashboard/src/components/deployments/types.ts +44 -0
  206. package/apps/dashboard/src/components/global-deployments/index.ts +17 -0
  207. package/apps/dashboard/src/components/i18n-provider.tsx +100 -0
  208. package/apps/dashboard/src/components/import-project/BuildSettings.tsx +376 -0
  209. package/apps/dashboard/src/components/import-project/BuildSkeleton.tsx +161 -0
  210. package/apps/dashboard/src/components/import-project/BuildTerminal.tsx +358 -0
  211. package/apps/dashboard/src/components/import-project/ComposeServices.tsx +455 -0
  212. package/apps/dashboard/src/components/import-project/DeploymentProcessing.tsx +510 -0
  213. package/apps/dashboard/src/components/import-project/DockerSettings.tsx +64 -0
  214. package/apps/dashboard/src/components/import-project/EnvironmentVariables.tsx +481 -0
  215. package/apps/dashboard/src/components/import-project/Frameworks.tsx +90 -0
  216. package/apps/dashboard/src/components/import-project/ProjectSettings.tsx +160 -0
  217. package/apps/dashboard/src/components/import-project/types.ts +82 -0
  218. package/apps/dashboard/src/components/integrations/GithubPermissionModal.tsx +173 -0
  219. package/apps/dashboard/src/components/language-switcher.tsx +36 -0
  220. package/apps/dashboard/src/components/logo.tsx +13 -0
  221. package/apps/dashboard/src/components/network-error-handler.tsx +27 -0
  222. package/apps/dashboard/src/components/oauth-buttons.tsx +81 -0
  223. package/apps/dashboard/src/components/overview/ActivityChart.tsx +247 -0
  224. package/apps/dashboard/src/components/overview/ApiRequestsCard.tsx +111 -0
  225. package/apps/dashboard/src/components/overview/CreditsCard.tsx +137 -0
  226. package/apps/dashboard/src/components/overview/DeploymentsChart.tsx +195 -0
  227. package/apps/dashboard/src/components/overview/EmptyState.tsx +126 -0
  228. package/apps/dashboard/src/components/overview/LoadingSkeleton.tsx +92 -0
  229. package/apps/dashboard/src/components/overview/OverviewHeader.tsx +74 -0
  230. package/apps/dashboard/src/components/overview/OverviewHeroChart.tsx +344 -0
  231. package/apps/dashboard/src/components/overview/ProjectsCard.tsx +134 -0
  232. package/apps/dashboard/src/components/overview/QuickLinks.tsx +119 -0
  233. package/apps/dashboard/src/components/overview/SandboxesCard.tsx +150 -0
  234. package/apps/dashboard/src/components/overview/StatsGrid.tsx +166 -0
  235. package/apps/dashboard/src/components/overview/TokenUsageChart.tsx +206 -0
  236. package/apps/dashboard/src/components/overview/UsageStats.tsx +79 -0
  237. package/apps/dashboard/src/components/overview/index.ts +30 -0
  238. package/apps/dashboard/src/components/overview/types.ts +116 -0
  239. package/apps/dashboard/src/components/page-header.tsx +25 -0
  240. package/apps/dashboard/src/components/project-settings/ServerSideSwitch.jsx +60 -0
  241. package/apps/dashboard/src/components/shared/AlertBox.tsx +39 -0
  242. package/apps/dashboard/src/components/shared/CTASection.tsx +45 -0
  243. package/apps/dashboard/src/components/shared/ErrorState.tsx +176 -0
  244. package/apps/dashboard/src/components/shared/FeatureCard.tsx +20 -0
  245. package/apps/dashboard/src/components/shared/MDXComponents.tsx +24 -0
  246. package/apps/dashboard/src/components/shared/MachineSettingsModal.tsx +286 -0
  247. package/apps/dashboard/src/components/shared/Modal.tsx +39 -0
  248. package/apps/dashboard/src/components/shared/OTPInput.tsx +122 -0
  249. package/apps/dashboard/src/components/shared/PageHero.tsx +21 -0
  250. package/apps/dashboard/src/components/shared/PlatformFeatureCard.tsx +27 -0
  251. package/apps/dashboard/src/components/shared/StatCard.tsx +14 -0
  252. package/apps/dashboard/src/components/shared/ValueCard.tsx +20 -0
  253. package/apps/dashboard/src/components/shared/index.ts +8 -0
  254. package/apps/dashboard/src/components/sidebar.tsx +282 -0
  255. package/apps/dashboard/src/components/theme-provider.tsx +96 -0
  256. package/apps/dashboard/src/components/toast.tsx +106 -0
  257. package/apps/dashboard/src/components/ui/CustomCursor.tsx +107 -0
  258. package/apps/dashboard/src/components/ui/CustomSelect.tsx +126 -0
  259. package/apps/dashboard/src/components/ui/DropdownMenu.tsx +174 -0
  260. package/apps/dashboard/src/components/ui/FileIcon.jsx +59 -0
  261. package/apps/dashboard/src/components/ui/IconPickerModal.tsx +273 -0
  262. package/apps/dashboard/src/components/ui/InfoBanner.jsx +39 -0
  263. package/apps/dashboard/src/components/ui/Logo.tsx +14 -0
  264. package/apps/dashboard/src/components/ui/Modal.tsx +117 -0
  265. package/apps/dashboard/src/components/ui/PageContainer.tsx +12 -0
  266. package/apps/dashboard/src/components/ui/SectionContainer.jsx +7 -0
  267. package/apps/dashboard/src/components/ui/SlidingToggle.tsx +128 -0
  268. package/apps/dashboard/src/components/ui/Toast.tsx +29 -0
  269. package/apps/dashboard/src/components/ui/button.tsx +58 -0
  270. package/apps/dashboard/src/components/ui/card.tsx +60 -0
  271. package/apps/dashboard/src/components/ui/input.tsx +20 -0
  272. package/apps/dashboard/src/components/ui/label.tsx +19 -0
  273. package/apps/dashboard/src/components/ui/select.tsx +10 -0
  274. package/apps/dashboard/src/components/ui/textarea.tsx +9 -0
  275. package/apps/dashboard/src/constants/lang-colors.ts +25 -0
  276. package/apps/dashboard/src/constants/mock.ts +38 -0
  277. package/apps/dashboard/src/context/AuthContext.tsx +91 -0
  278. package/apps/dashboard/src/context/CloudContext.tsx +275 -0
  279. package/apps/dashboard/src/context/DeploymentContext.tsx +71 -0
  280. package/apps/dashboard/src/context/GitHubContext.tsx +288 -0
  281. package/apps/dashboard/src/context/ModalContext.tsx +300 -0
  282. package/apps/dashboard/src/context/NetworkContext.tsx +32 -0
  283. package/apps/dashboard/src/context/ProjectSettingsContext.tsx +727 -0
  284. package/apps/dashboard/src/context/ToastContext.tsx +129 -0
  285. package/apps/dashboard/src/context/deployment/index.ts +3 -0
  286. package/apps/dashboard/src/context/deployment/types.ts +183 -0
  287. package/apps/dashboard/src/context/deployment/useDeploymentBuild.ts +547 -0
  288. package/apps/dashboard/src/context/deployment/useDeploymentConfig.ts +176 -0
  289. package/apps/dashboard/src/hooks/useBuildConnection.ts +334 -0
  290. package/apps/dashboard/src/hooks/useMonitorStream.ts +146 -0
  291. package/apps/dashboard/src/hooks/useProjectData.ts +110 -0
  292. package/apps/dashboard/src/hooks/useSSEConnection.ts +465 -0
  293. package/apps/dashboard/src/hooks/useSSEStream.ts +309 -0
  294. package/apps/dashboard/src/hooks/useSessions.ts +104 -0
  295. package/apps/dashboard/src/hooks/useSetupStream.ts +223 -0
  296. package/apps/dashboard/src/i18n/dictionaries/ar.json +218 -0
  297. package/apps/dashboard/src/i18n/dictionaries/en.json +218 -0
  298. package/apps/dashboard/src/i18n/index.ts +16 -0
  299. package/apps/dashboard/src/lib/api/ai.ts +22 -0
  300. package/apps/dashboard/src/lib/api/auth.ts +13 -0
  301. package/apps/dashboard/src/lib/api/client.ts +199 -0
  302. package/apps/dashboard/src/lib/api/cloud.ts +15 -0
  303. package/apps/dashboard/src/lib/api/deploy.ts +58 -0
  304. package/apps/dashboard/src/lib/api/domains.ts +13 -0
  305. package/apps/dashboard/src/lib/api/endpoints.ts +129 -0
  306. package/apps/dashboard/src/lib/api/github.ts +31 -0
  307. package/apps/dashboard/src/lib/api/icons.ts +12 -0
  308. package/apps/dashboard/src/lib/api/index.ts +41 -0
  309. package/apps/dashboard/src/lib/api/projects.ts +153 -0
  310. package/apps/dashboard/src/lib/api/sandbox.ts +14 -0
  311. package/apps/dashboard/src/lib/api/settings.ts +21 -0
  312. package/apps/dashboard/src/lib/api/system.ts +159 -0
  313. package/apps/dashboard/src/lib/auth-client.ts +54 -0
  314. package/apps/dashboard/src/lib/country.ts +25 -0
  315. package/apps/dashboard/src/lib/server/api.ts +140 -0
  316. package/apps/dashboard/src/lib/server/session.ts +103 -0
  317. package/apps/dashboard/src/lib/sseClient.ts +241 -0
  318. package/apps/dashboard/src/lib/sseMessageProcessors.ts +323 -0
  319. package/apps/dashboard/src/lib/utils.ts +6 -0
  320. package/apps/dashboard/src/proxy.ts +46 -0
  321. package/apps/dashboard/src/styles/Toast.module.css +92 -0
  322. package/apps/dashboard/src/styles/fonts.css +93 -0
  323. package/apps/dashboard/src/styles/theme.css +336 -0
  324. package/apps/dashboard/src/types/research.ts +42 -0
  325. package/apps/dashboard/src/utils/SSEParser.js +150 -0
  326. package/apps/dashboard/src/utils/authWindow.ts +143 -0
  327. package/apps/dashboard/src/utils/codeTheme.ts +127 -0
  328. package/apps/dashboard/src/utils/colors.json +2810 -0
  329. package/apps/dashboard/src/utils/date.ts +121 -0
  330. package/apps/dashboard/src/utils/deployment.ts +128 -0
  331. package/apps/dashboard/src/utils/deploymentPhaseDetector.ts +232 -0
  332. package/apps/dashboard/src/utils/extToLang.js +103 -0
  333. package/apps/dashboard/src/utils/github.ts +62 -0
  334. package/apps/dashboard/src/utils/icons.js +46 -0
  335. package/apps/dashboard/src/utils/repoSlug.ts +110 -0
  336. package/apps/dashboard/src/utils/research-api.ts +163 -0
  337. package/apps/dashboard/src/utils/suggestions.ts +84 -0
  338. package/apps/dashboard/src/utils/theme.js +1 -0
  339. package/apps/dashboard/src/utils/ui-helpers.tsx +30 -0
  340. package/apps/dashboard/src/utils/upload.js +12 -0
  341. package/apps/dashboard/tsconfig.json +27 -5
  342. package/apps/desktop/forge.config.json +36 -0
  343. package/apps/desktop/package.json +26 -0
  344. package/apps/desktop/scripts/dev.mjs +58 -0
  345. package/apps/desktop/src/main/index.ts +675 -0
  346. package/apps/desktop/src/main/types/desktop.d.ts +33 -0
  347. package/apps/desktop/src/preload/index.ts +88 -0
  348. package/apps/desktop/src/renderer/index.html +462 -0
  349. package/apps/desktop/src/renderer/onboarding.js +511 -0
  350. package/apps/desktop/src/renderer/styles.css +1139 -0
  351. package/apps/desktop/tsconfig.json +17 -0
  352. package/apps/web/.source/browser.ts +13 -0
  353. package/apps/web/.source/dynamic.ts +8 -0
  354. package/apps/web/.source/server.ts +22 -0
  355. package/apps/web/.source/source.config.mjs +20 -0
  356. package/apps/web/Dockerfile +4 -6
  357. package/apps/web/content/blog/how-ai-builds-work.mdx +55 -0
  358. package/apps/web/content/blog/introducing-openship.mdx +46 -0
  359. package/apps/web/content/blog/self-hosting-cost-breakdown.mdx +59 -0
  360. package/apps/web/content/docs/_meta.json +14 -0
  361. package/apps/web/content/docs/api.mdx +98 -0
  362. package/apps/web/content/docs/cli.mdx +80 -0
  363. package/apps/web/content/docs/first-deployment.mdx +56 -0
  364. package/apps/web/content/docs/index.mdx +27 -0
  365. package/apps/web/content/docs/installation.mdx +65 -0
  366. package/apps/web/content/docs/quickstart.mdx +45 -0
  367. package/apps/web/next-env.d.ts +6 -0
  368. package/apps/web/next.config.mjs +16 -2
  369. package/apps/web/package.json +19 -12
  370. package/apps/web/postcss.config.mjs +5 -0
  371. package/apps/web/public/android-chrome-192x192.png +0 -0
  372. package/apps/web/public/android-chrome-512x512.png +0 -0
  373. package/apps/web/public/apple-touch-icon.png +0 -0
  374. package/apps/web/public/favicon-16x16.png +0 -0
  375. package/apps/web/public/favicon-32x32.png +0 -0
  376. package/apps/web/public/favicon.ico +0 -0
  377. package/apps/web/public/site.webmanifest +1 -0
  378. package/apps/web/source.config.ts +17 -0
  379. package/apps/web/src/app/(marketing)/layout.tsx +7 -22
  380. package/apps/web/src/app/blog/[...slug]/page.tsx +247 -0
  381. package/apps/web/src/app/blog/blog.css +575 -0
  382. package/apps/web/src/app/blog/layout.tsx +50 -0
  383. package/apps/web/src/app/blog/page.tsx +235 -0
  384. package/apps/web/src/app/docs/[[...slug]]/page.tsx +67 -0
  385. package/apps/web/src/app/docs/layout.tsx +62 -0
  386. package/apps/web/src/app/docs/toc-theme.css +71 -0
  387. package/apps/web/src/app/download/page.tsx +396 -0
  388. package/apps/web/src/app/globals.css +20 -3
  389. package/apps/web/src/app/layout.tsx +57 -3
  390. package/apps/web/src/app/page.tsx +47 -6
  391. package/apps/web/src/app/robots.ts +15 -0
  392. package/apps/web/src/app/sitemap.ts +35 -0
  393. package/apps/web/src/components/blog/share-sidebar.tsx +81 -0
  394. package/apps/web/src/components/blog/writer-sidebar.tsx +81 -0
  395. package/apps/web/src/components/landing/ai-native.tsx +169 -0
  396. package/apps/web/src/components/landing/backups.tsx +139 -0
  397. package/apps/web/src/components/landing/cdn.tsx +109 -0
  398. package/apps/web/src/components/landing/coming-soon.tsx +180 -0
  399. package/apps/web/src/components/landing/dark-section.tsx +166 -0
  400. package/apps/web/src/components/landing/dashboard.tsx +407 -0
  401. package/apps/web/src/components/landing/developer-experience.tsx +224 -0
  402. package/apps/web/src/components/landing/dns-domains.tsx +146 -0
  403. package/apps/web/src/components/landing/download-button.tsx +151 -0
  404. package/apps/web/src/components/landing/features/ai-visual.tsx +48 -0
  405. package/apps/web/src/components/landing/features/cloud-visual.tsx +44 -0
  406. package/apps/web/src/components/landing/features/git-visual.tsx +61 -0
  407. package/apps/web/src/components/landing/features/index.tsx +642 -0
  408. package/apps/web/src/components/landing/features/infra-visual.tsx +69 -0
  409. package/apps/web/src/components/landing/features/rollback-visual.tsx +42 -0
  410. package/apps/web/src/components/landing/features/ssl-visual.tsx +41 -0
  411. package/apps/web/src/components/landing/features/stacks-visual.tsx +54 -0
  412. package/apps/web/src/components/landing/final-cta.tsx +116 -0
  413. package/apps/web/src/components/landing/footer.tsx +145 -0
  414. package/apps/web/src/components/landing/hero.tsx +110 -0
  415. package/apps/web/src/components/landing/how-it-works.tsx +210 -0
  416. package/apps/web/src/components/landing/index.ts +21 -0
  417. package/apps/web/src/components/landing/mail-server.tsx +122 -0
  418. package/apps/web/src/components/landing/navbar.tsx +199 -0
  419. package/apps/web/src/components/landing/no-lockin.tsx +185 -0
  420. package/apps/web/src/components/landing/open-source-cta.tsx +151 -0
  421. package/apps/web/src/components/landing/portability.tsx +190 -0
  422. package/apps/web/src/components/landing/pricing.tsx +263 -0
  423. package/apps/web/src/components/landing/scaling.tsx +209 -0
  424. package/apps/web/src/components/landing/why-openship.tsx +291 -0
  425. package/apps/web/src/hooks/use-gsap-scroll.ts +158 -0
  426. package/apps/web/src/hooks/use-platform.ts +86 -0
  427. package/apps/web/src/lib/source.ts +12 -0
  428. package/apps/web/src/styles/fonts.css +84 -0
  429. package/apps/web/src/styles/landing.css +951 -0
  430. package/apps/web/src/styles/theme.css +227 -0
  431. package/apps/web/tsconfig.json +8 -3
  432. package/bun.lock +3260 -0
  433. package/docker-compose.yml +3 -0
  434. package/docs/screenshots/.gitkeep +0 -0
  435. package/docs/screenshots/screen.png +0 -0
  436. package/openship.code-workspace +7 -0
  437. package/package.json +23 -9
  438. package/packages/adapters/docs/ARCHITECTURE.md +160 -0
  439. package/packages/adapters/docs/BUILD-PIPELINE.md +171 -0
  440. package/packages/adapters/docs/CLOUD.md +239 -0
  441. package/packages/adapters/docs/EXECUTOR.md +174 -0
  442. package/packages/adapters/docs/LOG-STREAMING.md +175 -0
  443. package/packages/adapters/docs/SYSTEM.md +191 -0
  444. package/packages/adapters/package.json +14 -9
  445. package/packages/adapters/src/index.ts +144 -4
  446. package/packages/adapters/src/infra/cloud.ts +42 -0
  447. package/packages/adapters/src/infra/index.ts +9 -0
  448. package/packages/adapters/src/infra/noop.ts +27 -0
  449. package/packages/adapters/src/infra/traefik.ts +297 -0
  450. package/packages/adapters/src/infra/types.ts +36 -0
  451. package/packages/adapters/src/platform.ts +255 -0
  452. package/packages/adapters/src/runtime/bare.ts +567 -0
  453. package/packages/adapters/src/runtime/build-pipeline.ts +237 -0
  454. package/packages/adapters/src/runtime/cloud.ts +791 -0
  455. package/packages/adapters/src/runtime/deploy-pipeline.ts +146 -0
  456. package/packages/adapters/src/runtime/docker.ts +443 -0
  457. package/packages/adapters/src/runtime/index.ts +69 -0
  458. package/packages/adapters/src/runtime/local-build.ts +112 -0
  459. package/packages/adapters/src/runtime/transfer.ts +82 -0
  460. package/packages/adapters/src/runtime/types.ts +138 -0
  461. package/packages/adapters/src/system/catalog.ts +188 -0
  462. package/packages/adapters/src/system/checks.ts +228 -0
  463. package/packages/adapters/src/system/components.ts +45 -0
  464. package/packages/adapters/src/system/debug.ts +18 -0
  465. package/packages/adapters/src/system/environment.ts +168 -0
  466. package/packages/adapters/src/system/errors.ts +34 -0
  467. package/packages/adapters/src/system/executor.ts +600 -0
  468. package/packages/adapters/src/system/index.ts +58 -0
  469. package/packages/adapters/src/system/installer.ts +310 -0
  470. package/packages/adapters/src/system/setup.ts +442 -0
  471. package/packages/adapters/src/system/state.ts +96 -0
  472. package/packages/adapters/src/system/types.ts +116 -0
  473. package/packages/adapters/src/toolchain/catalog.ts +439 -0
  474. package/packages/adapters/src/toolchain/checks.ts +181 -0
  475. package/packages/adapters/src/toolchain/index.ts +38 -0
  476. package/packages/adapters/src/toolchain/installer.ts +189 -0
  477. package/packages/adapters/src/toolchain/types.ts +84 -0
  478. package/packages/adapters/src/types.ts +277 -0
  479. package/packages/adapters/tsconfig.json +3 -1
  480. package/packages/core/package.json +9 -9
  481. package/packages/core/src/constants.ts +44 -1
  482. package/packages/core/src/index.ts +2 -0
  483. package/packages/core/src/stacks.ts +778 -0
  484. package/packages/core/src/system.ts +111 -0
  485. package/packages/core/src/types.ts +22 -1
  486. package/packages/core/src/utils.ts +8 -0
  487. package/packages/core/tsconfig.json +3 -1
  488. package/packages/db/drizzle/0000_init.sql +231 -0
  489. package/packages/db/drizzle/0001_add_server_name.sql +1 -0
  490. package/packages/db/drizzle/meta/_journal.json +20 -0
  491. package/packages/db/drizzle.config.ts +10 -0
  492. package/packages/db/package.json +26 -14
  493. package/packages/db/src/client.ts +119 -0
  494. package/packages/db/src/index.ts +57 -8
  495. package/packages/db/src/repos/account.repo.ts +92 -0
  496. package/packages/db/src/repos/deployment.repo.ts +168 -0
  497. package/packages/db/src/repos/domain.repo.ts +102 -0
  498. package/packages/db/src/repos/git-installation.repo.ts +95 -0
  499. package/packages/db/src/repos/index.ts +43 -0
  500. package/packages/db/src/repos/instance-settings.repo.ts +43 -0
  501. package/packages/db/src/repos/project.repo.ts +159 -0
  502. package/packages/db/src/repos/session.repo.ts +82 -0
  503. package/packages/db/src/repos/settings.repo.ts +51 -0
  504. package/packages/db/src/repos/user.repo.ts +94 -0
  505. package/packages/db/src/schema/auth.ts +56 -0
  506. package/packages/db/src/schema/deployment.ts +87 -0
  507. package/packages/db/src/schema/domain.ts +46 -0
  508. package/packages/db/src/schema/github.ts +24 -0
  509. package/packages/db/src/schema/index.ts +6 -0
  510. package/packages/db/src/schema/project.ts +117 -0
  511. package/packages/db/src/schema/settings.ts +103 -0
  512. package/packages/db/tsconfig.json +3 -1
  513. package/packages/ui/package.json +17 -17
  514. package/packages/ui/src/components/status-dot.tsx +1 -1
  515. package/packages/ui/tsconfig.json +7 -2
  516. package/pnpm-workspace.yaml +0 -1
  517. package/{tooling/tsconfig/base.json → tsconfig.base.json} +0 -1
  518. package/turbo.json +4 -0
  519. package/apps/api/src/modules/auth/auth.controller.ts +0 -26
  520. package/apps/api/src/modules/auth/auth.schema.ts +0 -12
  521. package/apps/api/src/modules/auth/auth.service.ts +0 -23
  522. package/apps/dashboard/src/app/page.tsx +0 -6
  523. package/apps/web/src/app/(marketing)/docs/page.tsx +0 -10
  524. package/packages/adapters/src/base-adapter.ts +0 -49
  525. package/packages/adapters/src/docker-adapter.ts +0 -43
  526. package/packages/adapters/src/oblien-adapter.ts +0 -46
  527. package/packages/adapters/src/registry.ts +0 -22
  528. package/packages/db/prisma/schema.prisma +0 -160
  529. package/tooling/tsconfig/nextjs.json +0 -10
  530. package/tooling/tsconfig/node.json +0 -10
  531. package/tooling/tsconfig/package.json +0 -7
@@ -11,15 +11,45 @@ const envSchema = z.object({
11
11
  PORT: z.coerce.number().default(4000),
12
12
 
13
13
  /* ---------- Mode ---------- */
14
- CLOUD_MODE: z.coerce.boolean().default(false),
14
+ CLOUD_MODE: z
15
+ .enum(["true", "false", "1", "0", ""])
16
+ .default("false")
17
+ .transform((v) => v === "true" || v === "1"),
18
+ /**
19
+ * Deployment mode — determines the runtime + infrastructure combination:
20
+ * - "docker" (default) → Docker runtime + Traefik routing/SSL (self-hosted)
21
+ * - "bare" → Process runtime + Traefik routing/SSL (self-hosted)
22
+ * - "cloud" → Oblien cloud API for everything (auto-set when CLOUD_MODE=true)
23
+ * - "desktop" → Bare runtime, no routing/SSL (desktop app)
24
+ */
25
+ DEPLOY_MODE: z.enum(["docker", "bare", "cloud", "desktop"]).default("docker"),
15
26
 
16
27
  /* ---------- Database ---------- */
17
- DATABASE_URL: z.string().default("file:./dev.db"),
28
+ DATABASE_URL: z.string().default(""),
18
29
 
19
- /* ---------- Auth ---------- */
20
- JWT_SECRET: z.string().default("change-me-in-production"),
21
- JWT_EXPIRES_IN: z.string().default("15m"),
22
- JWT_REFRESH_EXPIRES_IN: z.string().default("7d"),
30
+ /* ---------- Auth (Better Auth) ---------- */
31
+ BETTER_AUTH_SECRET: z.string().default("change-me-in-production"),
32
+ BETTER_AUTH_URL: z.string().default("http://localhost:4000"),
33
+ TRUSTED_ORIGINS: z.string().optional(),
34
+
35
+ /* ---------- OAuth Providers ---------- */
36
+ GITHUB_CLIENT_ID: z.string().optional(),
37
+ GITHUB_CLIENT_SECRET: z.string().optional(),
38
+ GOOGLE_CLIENT_ID: z.string().optional(),
39
+ GOOGLE_CLIENT_SECRET: z.string().optional(),
40
+
41
+ /* ---------- GitHub Auth Strategy ---------- */
42
+ /**
43
+ * Controls how the API authenticates with GitHub:
44
+ * - "auto" (default) → inferred from DEPLOY_MODE / CLOUD_MODE
45
+ * - "app" → GitHub App installation tokens (cloud)
46
+ * - "oauth" → Better Auth OAuth flow only (self-hosted with OAuth)
47
+ * - "cli" → `gh auth login` token from the machine (local/desktop)
48
+ * - "token" → static GITHUB_TOKEN env var (CI, scripts)
49
+ */
50
+ GITHUB_AUTH_MODE: z.enum(["auto", "app", "oauth", "cli", "token"]).default("auto"),
51
+ /** Static GitHub personal access token — used when GITHUB_AUTH_MODE="token" */
52
+ GITHUB_TOKEN: z.string().optional(),
23
53
 
24
54
  /* ---------- Redis ---------- */
25
55
  REDIS_URL: z.string().default("redis://localhost:6379"),
@@ -28,12 +58,56 @@ const envSchema = z.object({
28
58
  STRIPE_SECRET_KEY: z.string().optional(),
29
59
  STRIPE_WEBHOOK_SECRET: z.string().optional(),
30
60
 
31
- /* ---------- Git Providers ---------- */
61
+ /* ---------- Git Webhooks ---------- */
32
62
  GITHUB_APP_ID: z.string().optional(),
63
+ GITHUB_APP_SLUG: z.string().optional(),
33
64
  GITHUB_PRIVATE_KEY: z.string().optional(),
34
65
  GITHUB_WEBHOOK_SECRET: z.string().optional(),
66
+
67
+ /* ---------- Email (SMTP) ---------- */
68
+ SMTP_HOST: z.string().optional(),
69
+ SMTP_PORT: z.coerce.number().optional(),
70
+ SMTP_USER: z.string().optional(),
71
+ SMTP_PASS: z.string().optional(),
72
+ SMTP_FROM: z.string().default("Openship <noreply@openship.dev>"),
73
+
74
+ /* ---------- Dashboard ---------- */
75
+ DASHBOARD_URL: z.string().default("http://localhost:3001"),
76
+
77
+ /* ---------- Network (self-hosted) ---------- */
78
+ /** Public IP of the server — used for A record instructions in self-hosted mode. */
79
+ SERVER_IP: z.string().optional(),
80
+
81
+ /* ---------- Oblien Cloud ---------- */
82
+ OBLIEN_CLIENT_ID: z.string().optional(),
83
+ OBLIEN_CLIENT_SECRET: z.string().optional(),
84
+
85
+ /** Openship Cloud API URL — used by local instances to fetch namespace tokens */
86
+ OPENSHIP_CLOUD_URL: z.string().default("https://api.openship.io"),
87
+
88
+ /** Openship Cloud dashboard URL — used for external auth redirect (desktop + cloud connect) */
89
+ OPENSHIP_CLOUD_DASHBOARD_URL: z.string().default("https://app.openship.io"),
90
+
91
+ /* ---------- Screenshots (optional) ---------- */
92
+ SCREENSHOT_SERVICE_URL: z.string().optional(),
93
+ CDN_UPLOAD_URL: z.string().optional(),
94
+
95
+ /* ---------- Internal (Electron ↔ API) ---------- */
96
+ /** Shared secret for Electron → API calls (set by desktop app on startup) */
97
+ INTERNAL_TOKEN: z.string().optional(),
98
+
99
+ /** Enables verbose timing logs for SSH/system checks and environment detection */
100
+ SYSTEM_DEBUG_LOGS: z
101
+ .enum(["true", "false", "1", "0", ""])
102
+ .default("")
103
+ .transform((v) => v === "true" || v === "1"),
35
104
  });
36
105
 
37
106
  export type Env = z.infer<typeof envSchema>;
38
107
 
39
108
  export const env = envSchema.parse(process.env);
109
+
110
+ /** Parsed trusted origins — single source of truth for CORS + Better Auth */
111
+ export const trustedOrigins = env.TRUSTED_ORIGINS
112
+ ? env.TRUSTED_ORIGINS.split(",")
113
+ : ["http://localhost:3000", "http://localhost:3001"];
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Cached auth-mode reader for desktop mode.
3
+ *
4
+ * Auth mode is written once during onboarding and rarely changes.
5
+ * Caching avoids a DB hit on every request through authMiddleware.
6
+ *
7
+ * The cache is cleared by setup.controller after any write so that
8
+ * re-onboarding (dev reset) picks up the new value immediately.
9
+ */
10
+
11
+ import { env } from "../config/env";
12
+
13
+ let cached: string | null = null;
14
+
15
+ /**
16
+ * Returns the current auth mode for this instance.
17
+ *
18
+ * "none" → zero-auth desktop (auto-provisioned local user)
19
+ * "cloud" → cloud-authenticated desktop (Openship Cloud session)
20
+ * "local" → standard Better Auth (self-hosted VPS / SaaS)
21
+ */
22
+ export async function getAuthMode(): Promise<"none" | "cloud" | "local"> {
23
+ // Non-desktop always uses local Better Auth
24
+ if (env.DEPLOY_MODE !== "desktop") return "local";
25
+
26
+ if (cached !== null) return cached as "none" | "cloud" | "local";
27
+
28
+ try {
29
+ const { repos } = await import("@repo/db");
30
+ const settings = await repos.instanceSettings.get();
31
+ cached = settings?.authMode ?? "none";
32
+ } catch {
33
+ cached = "none";
34
+ }
35
+
36
+ return cached as "none" | "cloud" | "local";
37
+ }
38
+
39
+ /** Clear the cached value — called after setup.controller writes. */
40
+ export function clearAuthModeCache() {
41
+ cached = null;
42
+ }
@@ -0,0 +1,105 @@
1
+ import { betterAuth, type User } from "better-auth";
2
+ import { drizzleAdapter } from "better-auth/adapters/drizzle";
3
+ import { db, schema } from "@repo/db";
4
+ import { env, trustedOrigins } from "../config/env";
5
+ import { sendMail, smtpEnabled } from "./mail";
6
+ import { resetPasswordEmail, verifyEmailTemplate } from "./email-templates";
7
+
8
+ /**
9
+ * Better Auth — handles registration, login, OAuth, sessions, tokens.
10
+ *
11
+ * Browser clients (dashboard) use httpOnly session cookies.
12
+ * API clients (CLI, external) use Bearer tokens via the session token.
13
+ *
14
+ * Routes are mounted at /api/auth/* in app.ts.
15
+ */
16
+ export const auth = betterAuth({
17
+ basePath: "/api/auth",
18
+ baseURL: env.BETTER_AUTH_URL,
19
+
20
+ database: drizzleAdapter(db, {
21
+ provider: "pg",
22
+ schema: {
23
+ user: schema.user,
24
+ session: schema.session,
25
+ account: schema.account,
26
+ verification: schema.verification,
27
+ },
28
+ }),
29
+
30
+ /* ---------- Email + Password ---------- */
31
+ emailAndPassword: {
32
+ enabled: true,
33
+ minPasswordLength: 8,
34
+ maxPasswordLength: 128,
35
+
36
+ /* Password reset — only functional when SMTP is configured */
37
+ sendResetPassword: smtpEnabled
38
+ ? async ({ user, url }: { user: User; url: string; token: string }) => {
39
+ const email = resetPasswordEmail(user, url);
40
+ await sendMail({ to: user.email, ...email });
41
+ }
42
+ : undefined,
43
+
44
+ /* Email verification — only functional when SMTP is configured */
45
+ requireEmailVerification: smtpEnabled,
46
+ sendVerificationEmail: smtpEnabled
47
+ ? async ({ user, url }: { user: User; url: string; token: string }) => {
48
+ const email = verifyEmailTemplate(user, url);
49
+ await sendMail({ to: user.email, ...email });
50
+ }
51
+ : undefined,
52
+ },
53
+
54
+ /* ---------- OAuth Providers ---------- */
55
+ socialProviders: {
56
+ ...(env.GITHUB_CLIENT_ID && env.GITHUB_CLIENT_SECRET
57
+ ? {
58
+ github: {
59
+ clientId: env.GITHUB_CLIENT_ID,
60
+ clientSecret: env.GITHUB_CLIENT_SECRET,
61
+ },
62
+ }
63
+ : {}),
64
+ ...(env.GOOGLE_CLIENT_ID && env.GOOGLE_CLIENT_SECRET
65
+ ? {
66
+ google: {
67
+ clientId: env.GOOGLE_CLIENT_ID,
68
+ clientSecret: env.GOOGLE_CLIENT_SECRET,
69
+ },
70
+ }
71
+ : {}),
72
+ },
73
+
74
+ /* ---------- Session ---------- */
75
+ session: {
76
+ expiresIn: 60 * 60 * 24 * 30, // 30 days
77
+ updateAge: 60 * 60, // refresh session every hour
78
+ cookieCache: {
79
+ enabled: true,
80
+ maxAge: 60 * 60 * 24, // cache session in cookie for 24h (avoids DB hit)
81
+ },
82
+ },
83
+
84
+ /* ---------- Custom fields on user ---------- */
85
+ user: {
86
+ additionalFields: {
87
+ role: {
88
+ type: "string",
89
+ defaultValue: "user",
90
+ input: false,
91
+ },
92
+ autoProvisioned: {
93
+ type: "boolean",
94
+ defaultValue: false,
95
+ input: false,
96
+ },
97
+ },
98
+ },
99
+
100
+ /* ---------- Security ---------- */
101
+ secret: env.BETTER_AUTH_SECRET,
102
+ trustedOrigins,
103
+ });
104
+
105
+ export type Auth = typeof auth;
@@ -0,0 +1,128 @@
1
+ /**
2
+ * TtlCache — generic in-memory cache with automatic TTL expiration.
3
+ *
4
+ * Entries are lazily evicted on read and periodically swept via a
5
+ * background timer (default: every 60 s). Supports per-key TTL,
6
+ * prefix-based invalidation, and a hard size cap.
7
+ *
8
+ * Usage:
9
+ * const cache = new TtlCache<string>({ maxSize: 1000, sweepIntervalMs: 60_000 });
10
+ * cache.set("key", "value", 300); // 300 seconds TTL
11
+ * cache.get("key"); // "value" | null
12
+ * cache.invalidateByPrefix("user:"); // remove all keys starting with "user:"
13
+ */
14
+
15
+ interface CacheEntry<T> {
16
+ value: T;
17
+ expiresAt: number;
18
+ }
19
+
20
+ export interface TtlCacheOptions {
21
+ /** Maximum number of entries before oldest are evicted (default: 5000). */
22
+ maxSize?: number;
23
+ /** Interval in ms for the background sweep timer (default: 60000). 0 = disabled. */
24
+ sweepIntervalMs?: number;
25
+ }
26
+
27
+ export class TtlCache<T> {
28
+ private readonly store = new Map<string, CacheEntry<T>>();
29
+ private readonly maxSize: number;
30
+ private sweepTimer: ReturnType<typeof setInterval> | null = null;
31
+
32
+ constructor(opts: TtlCacheOptions = {}) {
33
+ this.maxSize = opts.maxSize ?? 5_000;
34
+ const sweepMs = opts.sweepIntervalMs ?? 60_000;
35
+
36
+ if (sweepMs > 0) {
37
+ this.sweepTimer = setInterval(() => this.sweep(), sweepMs);
38
+ /* Allow the process to exit even if the timer is running */
39
+ if (this.sweepTimer && typeof this.sweepTimer === "object" && "unref" in this.sweepTimer) {
40
+ this.sweepTimer.unref();
41
+ }
42
+ }
43
+ }
44
+
45
+ /** Get a value, returning null if missing or expired. */
46
+ get(key: string): T | null {
47
+ const entry = this.store.get(key);
48
+ if (!entry) return null;
49
+ if (Date.now() > entry.expiresAt) {
50
+ this.store.delete(key);
51
+ return null;
52
+ }
53
+ return entry.value;
54
+ }
55
+
56
+ /** Set a value with a TTL in seconds. */
57
+ set(key: string, value: T, ttlSeconds: number): void {
58
+ if (this.store.size >= this.maxSize) {
59
+ this.sweep();
60
+ }
61
+ this.store.set(key, {
62
+ value,
63
+ expiresAt: Date.now() + ttlSeconds * 1_000,
64
+ });
65
+ }
66
+
67
+ /** Check if a key exists and is not expired. */
68
+ has(key: string): boolean {
69
+ return this.get(key) !== null;
70
+ }
71
+
72
+ /** Delete a specific key. */
73
+ delete(key: string): void {
74
+ this.store.delete(key);
75
+ }
76
+
77
+ /** Invalidate all keys that start with the given prefix. */
78
+ invalidateByPrefix(prefix: string): void {
79
+ for (const key of this.store.keys()) {
80
+ if (key.startsWith(prefix)) this.store.delete(key);
81
+ }
82
+ }
83
+
84
+ /** Invalidate all keys that contain the given substring. */
85
+ invalidateBySubstring(sub: string): void {
86
+ for (const key of this.store.keys()) {
87
+ if (key.includes(sub)) this.store.delete(key);
88
+ }
89
+ }
90
+
91
+ /** Remove all expired entries. */
92
+ sweep(): void {
93
+ const now = Date.now();
94
+ for (const [key, entry] of this.store) {
95
+ if (now > entry.expiresAt) this.store.delete(key);
96
+ }
97
+ }
98
+
99
+ /** Clear all entries. */
100
+ clear(): void {
101
+ this.store.clear();
102
+ }
103
+
104
+ /** Current number of entries (including possibly expired). */
105
+ get size(): number {
106
+ return this.store.size;
107
+ }
108
+
109
+ /** Iterate over all non-expired values. */
110
+ *values(): IterableIterator<T> {
111
+ const now = Date.now();
112
+ for (const [key, entry] of this.store) {
113
+ if (now > entry.expiresAt) {
114
+ this.store.delete(key);
115
+ } else {
116
+ yield entry.value;
117
+ }
118
+ }
119
+ }
120
+
121
+ /** Stop the background sweep timer (for graceful shutdown / tests). */
122
+ dispose(): void {
123
+ if (this.sweepTimer) {
124
+ clearInterval(this.sweepTimer);
125
+ this.sweepTimer = null;
126
+ }
127
+ }
128
+ }
@@ -0,0 +1,310 @@
1
+ /**
2
+ * Cloud auth proxy — shared utilities for Openship Cloud authentication.
3
+ *
4
+ * Used by:
5
+ * - Desktop mode: cloud-callback exchanges a one-time code for a local session
6
+ * - Self-hosted settings: connect-callback stores cloud token for deploys
7
+ * - Cloud mode (SaaS): desktop-handoff generates one-time codes
8
+ *
9
+ * All external auth happens on app.openship.io — this module only handles
10
+ * the local side (mirroring users, creating sessions, managing codes).
11
+ */
12
+
13
+ import { randomUUID, randomBytes, createHash, timingSafeEqual } from "node:crypto";
14
+ import { db, schema, repos, eq } from "@repo/db";
15
+ import { encrypt } from "./encryption";
16
+ import { env } from "../config/env";
17
+
18
+ export interface CloudUser {
19
+ id: string;
20
+ name: string;
21
+ email: string;
22
+ emailVerified: boolean;
23
+ image?: string | null;
24
+ }
25
+
26
+ /**
27
+ * Ensure a local user record exists that mirrors the cloud user.
28
+ *
29
+ * Uses the cloud user's email as the unique key. If the user already
30
+ * exists locally (from a previous login), updates their info.
31
+ * Returns the local user ID.
32
+ */
33
+ async function mirrorCloudUser(cloudUser: CloudUser): Promise<string> {
34
+ const existing = await repos.user.findByEmail(cloudUser.email);
35
+
36
+ if (existing) {
37
+ // Update name/image if changed
38
+ await db
39
+ .update(schema.user)
40
+ .set({
41
+ name: cloudUser.name,
42
+ image: cloudUser.image,
43
+ emailVerified: true,
44
+ })
45
+ .where(eq(schema.user.email, cloudUser.email));
46
+ return existing.id;
47
+ }
48
+
49
+ // Create new local user — use a deterministic ID so cloud user maps 1:1
50
+ const id = cloudUser.id;
51
+ await db.insert(schema.user).values({
52
+ id,
53
+ name: cloudUser.name,
54
+ email: cloudUser.email,
55
+ emailVerified: true,
56
+ role: "admin",
57
+ autoProvisioned: false,
58
+ });
59
+
60
+ return id;
61
+ }
62
+
63
+ /**
64
+ * Store the cloud session token (encrypted) for later cloud API calls.
65
+ */
66
+ async function storeCloudSession(userId: string, cloudSessionToken: string): Promise<void> {
67
+ const encrypted = encrypt(cloudSessionToken);
68
+ const settings = await repos.settings.findByUser(userId);
69
+ if (settings) {
70
+ await repos.settings.update(userId, { cloudSessionToken: encrypted });
71
+ } else {
72
+ await repos.settings.upsert({
73
+ id: randomUUID(),
74
+ userId,
75
+ cloudSessionToken: encrypted,
76
+ });
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Create a local Better Auth session directly in the DB.
82
+ *
83
+ * This is used for desktop/cloud-proxy auth where we don't have a local password.
84
+ * The session is created for the mirrored user so that `auth.api.getSession()`
85
+ * recognizes it from the cookie.
86
+ *
87
+ * Returns the session token (to be set as a cookie).
88
+ */
89
+ async function createLocalSession(
90
+ userId: string,
91
+ ipAddress?: string,
92
+ userAgent?: string,
93
+ ): Promise<{ token: string; expiresAt: Date }> {
94
+ const token = randomBytes(32).toString("hex");
95
+ const expiresAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000); // 30 days
96
+
97
+ await db.insert(schema.session).values({
98
+ id: randomUUID(),
99
+ token,
100
+ userId,
101
+ expiresAt,
102
+ ipAddress: ipAddress ?? null,
103
+ userAgent: userAgent ?? null,
104
+ });
105
+
106
+ return { token, expiresAt };
107
+ }
108
+
109
+ // ─── One-time handoff codes (cloud-side generates, desktop-side consumes) ────
110
+
111
+ /**
112
+ * In-memory store for one-time handoff codes.
113
+ * Code → { user, sessionToken, expiresAt }
114
+ *
115
+ * Used by CLOUD_MODE instances to hold auth data between the
116
+ * auth completion and the desktop/local instance's code exchange.
117
+ */
118
+ const handoffCodes = new Map<string, { user: CloudUser; sessionToken: string; codeChallenge: string | null; expiresAt: number }>();
119
+
120
+ let lastPurge = 0;
121
+ function purgeExpiredCodes() {
122
+ const now = Date.now();
123
+ if (now - lastPurge < 60_000) return;
124
+ lastPurge = now;
125
+ for (const [code, data] of handoffCodes) {
126
+ if (data.expiresAt < now) handoffCodes.delete(code);
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Generate a one-time code that holds the auth result.
132
+ * Called on the CLOUD instance after authentication completes.
133
+ * The code is valid for 60 seconds and can only be used once.
134
+ *
135
+ * @param codeChallenge — PKCE S256 challenge (base64url). When provided,
136
+ * the corresponding code_verifier must be presented at exchange time.
137
+ */
138
+ async function generateHandoffCode(user: CloudUser, sessionToken: string, codeChallenge?: string): Promise<string> {
139
+ purgeExpiredCodes();
140
+ const code = randomBytes(32).toString("hex");
141
+ handoffCodes.set(code, {
142
+ user,
143
+ sessionToken,
144
+ codeChallenge: codeChallenge ?? null,
145
+ expiresAt: Date.now() + 60_000,
146
+ });
147
+ return code;
148
+ }
149
+
150
+ /**
151
+ * Exchange a one-time code for the auth result.
152
+ * Called on the CLOUD instance by the desktop/local callback.
153
+ *
154
+ * If a code_challenge was stored, the caller must provide the matching
155
+ * code_verifier (PKCE S256). Returns null on mismatch.
156
+ */
157
+ function exchangeHandoffCode(code: string, codeVerifier?: string): { user: CloudUser; sessionToken: string } | null {
158
+ purgeExpiredCodes();
159
+ const data = handoffCodes.get(code);
160
+ if (!data || data.expiresAt < Date.now()) {
161
+ handoffCodes.delete(code);
162
+ return null;
163
+ }
164
+
165
+ // PKCE verification — if a challenge was stored, verifier is mandatory
166
+ if (data.codeChallenge) {
167
+ if (!codeVerifier) {
168
+ return null; // verifier required but not provided
169
+ }
170
+ const computed = createHash("sha256").update(codeVerifier).digest("base64url");
171
+ if (computed !== data.codeChallenge) {
172
+ return null; // PKCE mismatch
173
+ }
174
+ }
175
+
176
+ handoffCodes.delete(code); // one-time use
177
+ return { user: data.user, sessionToken: data.sessionToken };
178
+ }
179
+
180
+ /**
181
+ * Exchange a one-time code with the Openship Cloud API.
182
+ * Shared by desktop cloud-callback and self-hosted connect-callback.
183
+ *
184
+ * @param codeVerifier — PKCE code_verifier (plain). Required when the
185
+ * authorization was initiated with a code_challenge.
186
+ */
187
+ async function exchangeCodeWithCloud(code: string, codeVerifier?: string): Promise<{ user: CloudUser; sessionToken: string } | null> {
188
+ const res = await fetch(`${env.OPENSHIP_CLOUD_URL}/api/cloud/exchange-code`, {
189
+ method: "POST",
190
+ headers: { "Content-Type": "application/json" },
191
+ body: JSON.stringify({ code, code_verifier: codeVerifier }),
192
+ });
193
+ if (!res.ok) return null;
194
+ const { data } = (await res.json()) as {
195
+ data: { user: CloudUser; sessionToken: string };
196
+ };
197
+ return data;
198
+ }
199
+
200
+ // ─── Desktop auth nonce relay (single-user, in-memory) ──────────────────────
201
+
202
+ /**
203
+ * When Electron starts cloud auth, it registers a nonce.
204
+ * After the system browser completes /cloud-callback, the session token
205
+ * is stored against that nonce. Electron polls to pick it up.
206
+ */
207
+ let pendingNonce: { value: string; state: string; codeVerifier: string; registeredAt: number } | null = null;
208
+ let resolvedAuth: { nonce: string; claimCode: string } | null = null;
209
+ let pendingClaim: { code: string; token: string; expiresAt: number; createdAt: number } | null = null;
210
+ /** Nonce value preserved after validateDesktopState consumes pendingNonce, used by pollDesktopAuth */
211
+ let activeNonce: string | null = null;
212
+
213
+ const NONCE_TTL = 5 * 60 * 1000; // 5 minutes
214
+
215
+ function registerDesktopNonce(nonce: string, state: string, codeVerifier: string): void {
216
+ pendingNonce = { value: nonce, state, codeVerifier, registeredAt: Date.now() };
217
+ resolvedAuth = null;
218
+ pendingClaim = null;
219
+ activeNonce = nonce;
220
+ }
221
+
222
+ /**
223
+ * Store the session result so Electron can pick it up via polling.
224
+ *
225
+ * The nonce value needed for polling was saved at registerDesktopNonce time.
226
+ * validateDesktopState already consumed pendingNonce, so we accept the
227
+ * nonce value explicitly.
228
+ */
229
+ function resolveDesktopAuth(nonce: string, token: string, expiresAt: Date): void {
230
+ const claimCode = randomBytes(16).toString("hex");
231
+ resolvedAuth = { nonce, claimCode };
232
+ pendingClaim = { code: claimCode, token, expiresAt: expiresAt.getTime(), createdAt: Date.now() };
233
+ }
234
+
235
+ /**
236
+ * Validate the state parameter returned from the cloud and retrieve
237
+ * the stored code_verifier for PKCE.
238
+ *
239
+ * Returns the code_verifier and nonce if state matches, null otherwise.
240
+ * Consumes the nonce atomically — prevents replay attacks.
241
+ */
242
+ function validateDesktopState(state: string): { codeVerifier: string; nonce: string } | null {
243
+ if (!pendingNonce) return null;
244
+ if (Date.now() - pendingNonce.registeredAt > NONCE_TTL) {
245
+ pendingNonce = null;
246
+ return null;
247
+ }
248
+ // Timing-safe comparison for state
249
+ const expected = Buffer.from(pendingNonce.state);
250
+ const actual = Buffer.from(state);
251
+ if (expected.length !== actual.length) {
252
+ pendingNonce = null;
253
+ return null;
254
+ }
255
+ if (!timingSafeEqual(expected, actual)) {
256
+ pendingNonce = null;
257
+ return null;
258
+ }
259
+ const result = { codeVerifier: pendingNonce.codeVerifier, nonce: pendingNonce.value };
260
+ pendingNonce = null; // consume — one-time use
261
+ return result;
262
+ }
263
+
264
+ function pollDesktopAuth(nonce: string): { status: "pending" | "resolved" | "expired"; claimCode?: string } {
265
+ if (resolvedAuth && resolvedAuth.nonce === nonce) {
266
+ const result = { status: "resolved" as const, claimCode: resolvedAuth.claimCode };
267
+ resolvedAuth = null; // one-time read
268
+ activeNonce = null;
269
+ return result;
270
+ }
271
+ if (pendingNonce && pendingNonce.value === nonce) {
272
+ if (Date.now() - pendingNonce.registeredAt > NONCE_TTL) {
273
+ pendingNonce = null;
274
+ activeNonce = null;
275
+ return { status: "expired" };
276
+ }
277
+ return { status: "pending" };
278
+ }
279
+ // Between validateDesktopState (consumes pendingNonce) and resolveDesktopAuth
280
+ // (sets resolvedAuth), both are null. activeNonce keeps "pending" during this window.
281
+ if (activeNonce === nonce) {
282
+ return { status: "pending" };
283
+ }
284
+ return { status: "expired" };
285
+ }
286
+
287
+ function exchangeDesktopClaim(code: string): { token: string; expiresAt: Date } | null {
288
+ if (!pendingClaim || pendingClaim.code !== code) return null;
289
+ if (Date.now() - pendingClaim.createdAt > 60_000) {
290
+ pendingClaim = null;
291
+ return null;
292
+ }
293
+ const result = { token: pendingClaim.token, expiresAt: new Date(pendingClaim.expiresAt) };
294
+ pendingClaim = null; // one-time use
295
+ return result;
296
+ }
297
+
298
+ export {
299
+ mirrorCloudUser,
300
+ storeCloudSession,
301
+ createLocalSession,
302
+ generateHandoffCode,
303
+ exchangeHandoffCode,
304
+ exchangeCodeWithCloud,
305
+ registerDesktopNonce,
306
+ resolveDesktopAuth,
307
+ validateDesktopState,
308
+ pollDesktopAuth,
309
+ exchangeDesktopClaim,
310
+ };