@repo-os/repoos 0.5.45

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 (362) hide show
  1. package/LICENSE.md +110 -0
  2. package/README.md +272 -0
  3. package/dist/.build-info.json +4 -0
  4. package/dist/.build-stamp.json +3 -0
  5. package/dist/cli/colors.d.ts +13 -0
  6. package/dist/cli/colors.js +37 -0
  7. package/dist/cli/index.d.ts +2 -0
  8. package/dist/cli/index.js +221 -0
  9. package/dist/commands/check.d.ts +170 -0
  10. package/dist/commands/check.js +1227 -0
  11. package/dist/commands/docs.d.ts +2 -0
  12. package/dist/commands/docs.js +70 -0
  13. package/dist/commands/gc.d.ts +1 -0
  14. package/dist/commands/gc.js +57 -0
  15. package/dist/commands/init.d.ts +9 -0
  16. package/dist/commands/init.js +699 -0
  17. package/dist/commands/screenshots.d.ts +7 -0
  18. package/dist/commands/screenshots.js +77 -0
  19. package/dist/commands/serve.d.ts +55 -0
  20. package/dist/commands/serve.js +204 -0
  21. package/dist/commands/status.d.ts +140 -0
  22. package/dist/commands/status.js +569 -0
  23. package/dist/commands/stop.d.ts +1 -0
  24. package/dist/commands/stop.js +59 -0
  25. package/dist/commands/tasks.d.ts +47 -0
  26. package/dist/commands/tasks.js +334 -0
  27. package/dist/commands/tunnel.d.ts +1 -0
  28. package/dist/commands/tunnel.js +1271 -0
  29. package/dist/commands/ui-harness.d.ts +53 -0
  30. package/dist/commands/ui-harness.js +62 -0
  31. package/dist/commands/ui-smoke.d.ts +6 -0
  32. package/dist/commands/ui-smoke.js +220 -0
  33. package/dist/commands/upgrade.d.ts +15 -0
  34. package/dist/commands/upgrade.js +188 -0
  35. package/dist/core/auth-store.d.ts +81 -0
  36. package/dist/core/auth-store.js +530 -0
  37. package/dist/core/auth.d.ts +75 -0
  38. package/dist/core/auth.js +124 -0
  39. package/dist/core/bootstrap.d.ts +41 -0
  40. package/dist/core/bootstrap.js +236 -0
  41. package/dist/core/build.d.ts +92 -0
  42. package/dist/core/build.js +249 -0
  43. package/dist/core/canary.d.ts +10 -0
  44. package/dist/core/canary.js +14 -0
  45. package/dist/core/check-failure-summary.d.ts +10 -0
  46. package/dist/core/check-failure-summary.js +34 -0
  47. package/dist/core/config.d.ts +160 -0
  48. package/dist/core/config.js +1288 -0
  49. package/dist/core/context-pack.d.ts +74 -0
  50. package/dist/core/context-pack.js +778 -0
  51. package/dist/core/db.d.ts +236 -0
  52. package/dist/core/db.js +747 -0
  53. package/dist/core/detect.d.ts +69 -0
  54. package/dist/core/detect.js +251 -0
  55. package/dist/core/docs.d.ts +76 -0
  56. package/dist/core/docs.js +225 -0
  57. package/dist/core/frontmatter.d.ts +29 -0
  58. package/dist/core/frontmatter.js +215 -0
  59. package/dist/core/git.d.ts +505 -0
  60. package/dist/core/git.js +1453 -0
  61. package/dist/core/indexer.d.ts +33 -0
  62. package/dist/core/indexer.js +282 -0
  63. package/dist/core/input.d.ts +66 -0
  64. package/dist/core/input.js +222 -0
  65. package/dist/core/logger.d.ts +70 -0
  66. package/dist/core/logger.js +138 -0
  67. package/dist/core/models.d.ts +45 -0
  68. package/dist/core/models.js +291 -0
  69. package/dist/core/net-probe.d.ts +4 -0
  70. package/dist/core/net-probe.js +20 -0
  71. package/dist/core/providers/deepinfra.d.ts +23 -0
  72. package/dist/core/providers/deepinfra.js +44 -0
  73. package/dist/core/providers/index.d.ts +24 -0
  74. package/dist/core/providers/index.js +59 -0
  75. package/dist/core/providers/openrouter.d.ts +28 -0
  76. package/dist/core/providers/openrouter.js +82 -0
  77. package/dist/core/providers/spend.d.ts +86 -0
  78. package/dist/core/providers/spend.js +286 -0
  79. package/dist/core/providers/types.d.ts +56 -0
  80. package/dist/core/providers/types.js +39 -0
  81. package/dist/core/repoos.d.ts +59 -0
  82. package/dist/core/repoos.js +221 -0
  83. package/dist/core/runtime.d.ts +35 -0
  84. package/dist/core/runtime.js +167 -0
  85. package/dist/core/skills-registry.d.ts +39 -0
  86. package/dist/core/skills-registry.js +148 -0
  87. package/dist/core/skills-registry.test.d.ts +1 -0
  88. package/dist/core/skills-registry.test.js +52 -0
  89. package/dist/core/sysmem.d.ts +7 -0
  90. package/dist/core/sysmem.js +55 -0
  91. package/dist/core/tailscale.d.ts +2 -0
  92. package/dist/core/tailscale.js +19 -0
  93. package/dist/core/task.d.ts +53 -0
  94. package/dist/core/task.js +310 -0
  95. package/dist/core/toml-line.d.ts +20 -0
  96. package/dist/core/toml-line.js +52 -0
  97. package/dist/core/toml-validate.d.ts +44 -0
  98. package/dist/core/toml-validate.js +335 -0
  99. package/dist/core/tunnel-assistant.d.ts +30 -0
  100. package/dist/core/tunnel-assistant.js +65 -0
  101. package/dist/core/tunnel-registry.d.ts +96 -0
  102. package/dist/core/tunnel-registry.js +267 -0
  103. package/dist/core/tunnel-registry.test.d.ts +1 -0
  104. package/dist/core/tunnel-registry.test.js +309 -0
  105. package/dist/core/tunnel.d.ts +103 -0
  106. package/dist/core/tunnel.js +320 -0
  107. package/dist/core/types.d.ts +811 -0
  108. package/dist/core/types.js +27 -0
  109. package/dist/core/worktree-gc.d.ts +57 -0
  110. package/dist/core/worktree-gc.js +194 -0
  111. package/dist/server/__tests__/integration-job.test.d.ts +1 -0
  112. package/dist/server/__tests__/integration-job.test.js +129 -0
  113. package/dist/server/agents.d.ts +1020 -0
  114. package/dist/server/agents.js +4070 -0
  115. package/dist/server/attachments.d.ts +51 -0
  116. package/dist/server/attachments.js +130 -0
  117. package/dist/server/auto-engineering.d.ts +45 -0
  118. package/dist/server/auto-engineering.js +275 -0
  119. package/dist/server/branchless-release.d.ts +22 -0
  120. package/dist/server/branchless-release.js +67 -0
  121. package/dist/server/built-in-agents.d.ts +305 -0
  122. package/dist/server/built-in-agents.js +1838 -0
  123. package/dist/server/cto-monitor.d.ts +35 -0
  124. package/dist/server/cto-monitor.js +281 -0
  125. package/dist/server/cto.d.ts +125 -0
  126. package/dist/server/cto.js +590 -0
  127. package/dist/server/deployments.d.ts +119 -0
  128. package/dist/server/deployments.js +325 -0
  129. package/dist/server/diff-snapshot.d.ts +20 -0
  130. package/dist/server/diff-snapshot.js +73 -0
  131. package/dist/server/done.d.ts +160 -0
  132. package/dist/server/done.js +488 -0
  133. package/dist/server/freeform.d.ts +55 -0
  134. package/dist/server/freeform.js +179 -0
  135. package/dist/server/handoff.d.ts +99 -0
  136. package/dist/server/handoff.js +575 -0
  137. package/dist/server/hetzner.d.ts +45 -0
  138. package/dist/server/hetzner.js +117 -0
  139. package/dist/server/icons.d.ts +8 -0
  140. package/dist/server/icons.js +195 -0
  141. package/dist/server/integration-job.d.ts +75 -0
  142. package/dist/server/integration-job.js +144 -0
  143. package/dist/server/integration-orchestrator.d.ts +211 -0
  144. package/dist/server/integration-orchestrator.js +1299 -0
  145. package/dist/server/integration-status.d.ts +48 -0
  146. package/dist/server/integration-status.js +71 -0
  147. package/dist/server/live-index.d.ts +295 -0
  148. package/dist/server/live-index.js +452 -0
  149. package/dist/server/model-test.d.ts +20 -0
  150. package/dist/server/model-test.js +100 -0
  151. package/dist/server/ntfy.d.ts +57 -0
  152. package/dist/server/ntfy.js +112 -0
  153. package/dist/server/pm-attachments.d.ts +35 -0
  154. package/dist/server/pm-attachments.js +181 -0
  155. package/dist/server/pm-runs.d.ts +58 -0
  156. package/dist/server/pm-runs.js +80 -0
  157. package/dist/server/preview.d.ts +174 -0
  158. package/dist/server/preview.js +641 -0
  159. package/dist/server/release.d.ts +88 -0
  160. package/dist/server/release.js +449 -0
  161. package/dist/server/reload.d.ts +194 -0
  162. package/dist/server/reload.js +586 -0
  163. package/dist/server/remote-validation.d.ts +132 -0
  164. package/dist/server/remote-validation.js +478 -0
  165. package/dist/server/repo-lock.d.ts +36 -0
  166. package/dist/server/repo-lock.js +141 -0
  167. package/dist/server/review-guard.d.ts +14 -0
  168. package/dist/server/review-guard.js +159 -0
  169. package/dist/server/review.d.ts +243 -0
  170. package/dist/server/review.js +1211 -0
  171. package/dist/server/routes/agents.d.ts +6 -0
  172. package/dist/server/routes/agents.js +28 -0
  173. package/dist/server/routes/auth.d.ts +60 -0
  174. package/dist/server/routes/auth.js +755 -0
  175. package/dist/server/routes/config.d.ts +20 -0
  176. package/dist/server/routes/config.js +347 -0
  177. package/dist/server/routes/debugger.d.ts +33 -0
  178. package/dist/server/routes/debugger.js +360 -0
  179. package/dist/server/routes/deployments.d.ts +3 -0
  180. package/dist/server/routes/deployments.js +16 -0
  181. package/dist/server/routes/docs.d.ts +8 -0
  182. package/dist/server/routes/docs.js +135 -0
  183. package/dist/server/routes/helpers.d.ts +21 -0
  184. package/dist/server/routes/helpers.js +171 -0
  185. package/dist/server/routes/index.d.ts +19 -0
  186. package/dist/server/routes/index.js +18 -0
  187. package/dist/server/routes/info.d.ts +17 -0
  188. package/dist/server/routes/info.js +177 -0
  189. package/dist/server/routes/inputs.d.ts +7 -0
  190. package/dist/server/routes/inputs.js +125 -0
  191. package/dist/server/routes/model-providers.d.ts +27 -0
  192. package/dist/server/routes/model-providers.js +94 -0
  193. package/dist/server/routes/models.d.ts +3 -0
  194. package/dist/server/routes/models.js +36 -0
  195. package/dist/server/routes/notify.d.ts +2 -0
  196. package/dist/server/routes/notify.js +17 -0
  197. package/dist/server/routes/playground.d.ts +29 -0
  198. package/dist/server/routes/playground.js +81 -0
  199. package/dist/server/routes/release.d.ts +20 -0
  200. package/dist/server/routes/release.js +86 -0
  201. package/dist/server/routes/router.d.ts +12 -0
  202. package/dist/server/routes/router.js +32 -0
  203. package/dist/server/routes/skill-registry.d.ts +5 -0
  204. package/dist/server/routes/skill-registry.js +51 -0
  205. package/dist/server/routes/tasks.d.ts +58 -0
  206. package/dist/server/routes/tasks.js +1442 -0
  207. package/dist/server/routes/transcribe.d.ts +2 -0
  208. package/dist/server/routes/transcribe.js +124 -0
  209. package/dist/server/routes/types.d.ts +68 -0
  210. package/dist/server/routes/types.js +1 -0
  211. package/dist/server/routes/ui.d.ts +5 -0
  212. package/dist/server/routes/ui.js +81 -0
  213. package/dist/server/routes/utils.d.ts +3 -0
  214. package/dist/server/routes/utils.js +23 -0
  215. package/dist/server/serve-reaper.d.ts +135 -0
  216. package/dist/server/serve-reaper.js +491 -0
  217. package/dist/server/server.d.ts +49 -0
  218. package/dist/server/server.js +2171 -0
  219. package/dist/server/supervisor.d.ts +89 -0
  220. package/dist/server/supervisor.js +231 -0
  221. package/dist/server/system.d.ts +198 -0
  222. package/dist/server/system.js +480 -0
  223. package/dist/server/task-check.d.ts +45 -0
  224. package/dist/server/task-check.js +71 -0
  225. package/dist/server/task-transitions.d.ts +51 -0
  226. package/dist/server/task-transitions.js +78 -0
  227. package/dist/server/task-watchdog.d.ts +158 -0
  228. package/dist/server/task-watchdog.js +681 -0
  229. package/dist/server/test-run.d.ts +28 -0
  230. package/dist/server/test-run.js +81 -0
  231. package/dist/server/watcher.d.ts +21 -0
  232. package/dist/server/watcher.js +194 -0
  233. package/dist/server/write.d.ts +94 -0
  234. package/dist/server/write.js +345 -0
  235. package/dist/ui/assets/AgentsView-CF4wRvHh.css +1 -0
  236. package/dist/ui/assets/AgentsView-D-myL-kA.js +1 -0
  237. package/dist/ui/assets/ContextView-D3TC-RSp.css +1 -0
  238. package/dist/ui/assets/ContextView-DsOqAdyE.js +4 -0
  239. package/dist/ui/assets/DashboardView-BOhsIJqO.js +1 -0
  240. package/dist/ui/assets/DashboardView-Ck9o0RP2.css +1 -0
  241. package/dist/ui/assets/DeploymentsView--iIUVfEH.css +1 -0
  242. package/dist/ui/assets/DeploymentsView-DXu_FYR2.js +1 -0
  243. package/dist/ui/assets/InputsView-24HVbVPk.js +1 -0
  244. package/dist/ui/assets/InputsView-BLb7GHtx.css +1 -0
  245. package/dist/ui/assets/LoginView-6QlGg6cx.css +1 -0
  246. package/dist/ui/assets/LoginView-BqupI0Lt.js +1 -0
  247. package/dist/ui/assets/ReleasesView-BEG1A0YH.css +1 -0
  248. package/dist/ui/assets/ReleasesView-CAgMuxpe.js +3 -0
  249. package/dist/ui/assets/SettingsView-CA-NYSMK.css +1 -0
  250. package/dist/ui/assets/SettingsView-O3iZSnjg.js +3 -0
  251. package/dist/ui/assets/WorkView-D8Tl6qwy.js +1 -0
  252. package/dist/ui/assets/WorkView-DMYL2s3W.css +1 -0
  253. package/dist/ui/assets/abnfDiagram-VCTEODGH-J5lkSnEE.js +1 -0
  254. package/dist/ui/assets/arc-BHZnmhhM.js +1 -0
  255. package/dist/ui/assets/architecture-7GRP2DOG-pLdFt-xS.js +1 -0
  256. package/dist/ui/assets/architectureDiagram-5GKGNRK7-C3k89uKX.js +36 -0
  257. package/dist/ui/assets/array-BifhSqXX.js +1 -0
  258. package/dist/ui/assets/blockDiagram-I7D4REHJ-AWNoBzop.js +129 -0
  259. package/dist/ui/assets/button-lbikqSph.js +1 -0
  260. package/dist/ui/assets/c4Diagram-7LVT6UL2-DKG1D5gd.js +38 -0
  261. package/dist/ui/assets/channel-gA4qBv3A.js +1 -0
  262. package/dist/ui/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
  263. package/dist/ui/assets/chunk-4HAMMTFA-D1YR8lF_.js +62 -0
  264. package/dist/ui/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
  265. package/dist/ui/assets/chunk-75Z2AOVW-5GuBxfsL.js +2 -0
  266. package/dist/ui/assets/chunk-DU6HZSFF-DfdNNqwa.js +127 -0
  267. package/dist/ui/assets/chunk-F27PBJKO-BPZzinQ4.js +1 -0
  268. package/dist/ui/assets/chunk-FOHPRMQF-l2z4K2jD.js +161 -0
  269. package/dist/ui/assets/chunk-GMAD6QVW-DGWvSKj5.js +72 -0
  270. package/dist/ui/assets/chunk-GVQU2GXP-D6Aw75wJ.js +1 -0
  271. package/dist/ui/assets/chunk-IMKFNOWR-B8mMBKCW.js +231 -0
  272. package/dist/ui/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
  273. package/dist/ui/assets/chunk-L3NEJ4N5-D5OJuXYE.js +1 -0
  274. package/dist/ui/assets/chunk-OSK3NFVY-B7r9w2UW.js +10 -0
  275. package/dist/ui/assets/chunk-P2QGCYS3-C74X4AQX.js +1 -0
  276. package/dist/ui/assets/chunk-POPQ4Y6H-C5_u1gUS.js +1 -0
  277. package/dist/ui/assets/chunk-PWAF6VOD-DfoTLtxn.js +1 -0
  278. package/dist/ui/assets/chunk-SHT3W25Y-D40m2eqz.js +168 -0
  279. package/dist/ui/assets/chunk-SVP7TREG-euxi0Kap.js +88 -0
  280. package/dist/ui/assets/chunk-TICWLB2K-fkdxngzN.js +206 -0
  281. package/dist/ui/assets/chunk-XXDRQBXY-DFBUG-OT.js +1 -0
  282. package/dist/ui/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
  283. package/dist/ui/assets/classDiagram-ZZMXUADV-BzW8bq2P.js +1 -0
  284. package/dist/ui/assets/classDiagram-v2-VYDZK3BY-BzW8bq2P.js +1 -0
  285. package/dist/ui/assets/cose-bilkent-JH36ORCC-BTcgMon1.js +1 -0
  286. package/dist/ui/assets/cynefin-OW5HDTMX-B5m_COsS.js +1 -0
  287. package/dist/ui/assets/cynefinDiagram-5FMLGOSQ-C-AG6CGe.js +62 -0
  288. package/dist/ui/assets/cytoscape.esm-FBntH-9t.js +321 -0
  289. package/dist/ui/assets/dagre-GXQ25YYZ-DDfDnp8L.js +4 -0
  290. package/dist/ui/assets/dagre-PrKaheQc.js +1 -0
  291. package/dist/ui/assets/defaultLocale-BFoDCU3G.js +1 -0
  292. package/dist/ui/assets/diagram-S7CK7UJ4-DjQZAlEY.js +30 -0
  293. package/dist/ui/assets/diagram-UQ7AKVKN-DAZ9yp1B.js +41 -0
  294. package/dist/ui/assets/diagram-VSXAHHWV-BWepUKJD.js +3 -0
  295. package/dist/ui/assets/diagram-VX7I27RA-EcV1J81b.js +24 -0
  296. package/dist/ui/assets/diagram-Z3DM3KII-Dbz7XU_K.js +24 -0
  297. package/dist/ui/assets/dist-DTg6UBE_.js +1 -0
  298. package/dist/ui/assets/ebnfDiagram-PWID7BFC-BUyQw8Mm.js +1 -0
  299. package/dist/ui/assets/erDiagram-RLTQ6QDP-WVf8ukKt.js +99 -0
  300. package/dist/ui/assets/eventmodeling-NTZA5JFV-DHqyLOAn.js +1 -0
  301. package/dist/ui/assets/flowDiagram-HODETNUW-CYk_ze5K.js +1 -0
  302. package/dist/ui/assets/ganttDiagram-EL5Y4UJY-rB4MZuB-.js +292 -0
  303. package/dist/ui/assets/gitGraph-4MIJSDKK-BOTWM8dY.js +1 -0
  304. package/dist/ui/assets/gitGraphDiagram-WWUBYQGX-eYscuhtP.js +106 -0
  305. package/dist/ui/assets/graphlib-DS17s2tU.js +1 -0
  306. package/dist/ui/assets/index-BACBfnga.css +1 -0
  307. package/dist/ui/assets/index-BOQ-ust6.js +26 -0
  308. package/dist/ui/assets/info-A6RAGUB7-_67wOORe.js +1 -0
  309. package/dist/ui/assets/infoDiagram-27XIBGKW-XVXbddTA.js +2 -0
  310. package/dist/ui/assets/init-C-OQMol4.js +1 -0
  311. package/dist/ui/assets/ishikawaDiagram-5VMMS53U-ClLNiEru.js +70 -0
  312. package/dist/ui/assets/journeyDiagram-3NMN7TZE-NRawjR6i.js +139 -0
  313. package/dist/ui/assets/kanban-definition-UXKFOSKX-BgSzBfIx.js +89 -0
  314. package/dist/ui/assets/katex-DolUETbr.js +257 -0
  315. package/dist/ui/assets/line-KAyfuul4.js +1 -0
  316. package/dist/ui/assets/linear-cCCPOpu7.js +1 -0
  317. package/dist/ui/assets/mermaid-parser.core-CgXZk1iN.js +7 -0
  318. package/dist/ui/assets/mermaid.core-BUUkChHW.js +44 -0
  319. package/dist/ui/assets/mindmap-definition-YA3MSWOX-Cpn7vW4r.js +96 -0
  320. package/dist/ui/assets/ordinal-BDEzSJ7C.js +1 -0
  321. package/dist/ui/assets/packet-AYTQ26CC-0lZwM-d6.js +1 -0
  322. package/dist/ui/assets/path-COt_16Va.js +1 -0
  323. package/dist/ui/assets/pegDiagram-XKGWAZYB-impy0inn.js +1 -0
  324. package/dist/ui/assets/pie-WAS4IAKB-CqfdGHYg.js +1 -0
  325. package/dist/ui/assets/pieDiagram-E7YTZNPT-CxjL4I1h.js +39 -0
  326. package/dist/ui/assets/quadrantDiagram-AXDQQJYC-Cjd1QMIe.js +7 -0
  327. package/dist/ui/assets/radar-RG4KPBEZ-CRkQIwiv.js +1 -0
  328. package/dist/ui/assets/railroad-74A4TZTK-i67XSWTB.js +1 -0
  329. package/dist/ui/assets/railroad-abnf-HS5TGJTU-Cv9m5aS3.js +1 -0
  330. package/dist/ui/assets/railroad-ebnf-LZEXJU2U-C5ZIn-Jh.js +1 -0
  331. package/dist/ui/assets/railroad-peg-WCYAUIDC-CGra9Oq-.js +1 -0
  332. package/dist/ui/assets/railroadDiagram-O6MQD6OU-_pK0qyaY.js +1 -0
  333. package/dist/ui/assets/releases-B0WNV-SY.js +1 -0
  334. package/dist/ui/assets/repoos-cto-square.webp +0 -0
  335. package/dist/ui/assets/repoos-helpful-pm-square.webp +0 -0
  336. package/dist/ui/assets/repoos-orchestrator-square.webp +0 -0
  337. package/dist/ui/assets/repoos-performance-agent-square.webp +0 -0
  338. package/dist/ui/assets/repoos-ross-from-friends-square.webp +0 -0
  339. package/dist/ui/assets/repoos-tech-debt-agent-square.webp +0 -0
  340. package/dist/ui/assets/requirementDiagram-BXWQKSXE-CRrFX-yw.js +84 -0
  341. package/dist/ui/assets/rough.esm-By172zw-.js +1 -0
  342. package/dist/ui/assets/sankeyDiagram-P5KCCOFB-X_xFRj9E.js +40 -0
  343. package/dist/ui/assets/sequenceDiagram-WJ2MYXX4-Byb49lKf.js +162 -0
  344. package/dist/ui/assets/sizeCapture-INFHLROL-B0uUizjq.js +1 -0
  345. package/dist/ui/assets/src-BH-TyZbA.js +1 -0
  346. package/dist/ui/assets/stateDiagram-D77RDMKH-CzaPrqYF.js +1 -0
  347. package/dist/ui/assets/stateDiagram-v2-MP3YSRHH-BCXckz7K.js +1 -0
  348. package/dist/ui/assets/swimlanes-42K2YHIH-Cx_1lnqr.js +1 -0
  349. package/dist/ui/assets/swimlanesDiagram-VR7AAH4N-54qsKi6A.js +8 -0
  350. package/dist/ui/assets/timeline-definition-24CTP7MA-B1IbZPQ-.js +120 -0
  351. package/dist/ui/assets/treeView-Q6P3EWNA-DFWhVE3d.js +1 -0
  352. package/dist/ui/assets/treemap-WGGIJYW6-BmbYPThn.js +1 -0
  353. package/dist/ui/assets/vendor-ui-DPm7zLxi.js +3 -0
  354. package/dist/ui/assets/vendor-vue-CobgyEJp.js +1 -0
  355. package/dist/ui/assets/vennDiagram-4TSXK5OY-Ae2AtRpt.js +34 -0
  356. package/dist/ui/assets/wardley-WFR3VGLG-BtD4X4tL.js +1 -0
  357. package/dist/ui/assets/wardleyDiagram-VM6X3IG4-B3YN2fTd.js +78 -0
  358. package/dist/ui/assets/xychartDiagram-S5SC5T6Z-CIzJfMKA.js +7 -0
  359. package/dist/ui/favicon.svg +11 -0
  360. package/dist/ui/index.html +49 -0
  361. package/dist/ui/sw.js +48 -0
  362. package/package.json +91 -0
@@ -0,0 +1,755 @@
1
+ /**
2
+ * Authentication API routes.
3
+ *
4
+ * POST /api/auth/request-otp – Send an email OTP code
5
+ * POST /api/auth/verify-otp – Verify OTP, create session, set cookie
6
+ * POST /api/auth/logout – Revoke session, clear cookie
7
+ * GET /api/auth/me – Current user info (or 401)
8
+ * GET /api/auth/status – Auth enabled? Bootstrap needed?
9
+ * POST /api/auth/bootstrap-admin – Create the first admin
10
+ *
11
+ * Admin-only:
12
+ * GET /api/auth/users – List users
13
+ * POST /api/auth/users – Add a user
14
+ * DELETE /api/auth/users/:email – Remove a user
15
+ * PATCH /api/auth/users/:email – Change role
16
+ * GET /api/auth/audit – Audit log
17
+ * POST /api/auth/users/:email/invite – (Re)send an invite email
18
+ */
19
+ import { createVerify, createPublicKey } from "node:crypto";
20
+ import { basename } from "node:path";
21
+ import { json, readBody } from "./utils.js";
22
+ import { getAuthStore } from "../../core/auth-store.js";
23
+ import { generateOtp, hashOtp, timingSafeEqualStr, isValidEmail, parseCookies, buildSessionCookie, SESSION_COOKIE_NAME, DEFAULT_SESSION_MAX_AGE, otpRequestLimiter, otpVerifyLimiter, globalRateLimiter, generatePkceVerifier, pkceChallenge, randomHex, } from "../../core/auth.js";
24
+ // ---------------------------------------------------------------------------
25
+ // Helpers
26
+ // ---------------------------------------------------------------------------
27
+ function getClientIp(req) {
28
+ const forwarded = req.headers["x-forwarded-for"];
29
+ if (typeof forwarded === "string")
30
+ return forwarded.split(",")[0].trim();
31
+ return req.socket.remoteAddress ?? "unknown";
32
+ }
33
+ function getConfiguredMaxAge(config) {
34
+ return config.auth?.sessionMaxAge ?? DEFAULT_SESSION_MAX_AGE;
35
+ }
36
+ function getSecureFlag(req) {
37
+ const proto = req.headers["x-forwarded-proto"];
38
+ if (proto === "https")
39
+ return true;
40
+ // In development, don't require Secure flag
41
+ return false;
42
+ }
43
+ function getSessionTokenFromRequest(req) {
44
+ const cookies = parseCookies(req.headers.cookie);
45
+ return cookies[SESSION_COOKIE_NAME] ?? null;
46
+ }
47
+ export function getCurrentUser(req, config) {
48
+ if (!config.auth?.enabled)
49
+ return null; // auth disabled, no user context
50
+ const store = getAuthStore(config.root);
51
+ if (!store)
52
+ return null;
53
+ const token = getSessionTokenFromRequest(req);
54
+ if (!token)
55
+ return null;
56
+ const session = store.getSession(token);
57
+ if (!session)
58
+ return null;
59
+ return { email: session.email, role: session.role };
60
+ }
61
+ function requireAdmin(req, config, res) {
62
+ const user = getCurrentUser(req, config);
63
+ if (!user) {
64
+ json(res, 401, { error: "Authentication required" });
65
+ return null;
66
+ }
67
+ if (user.role !== "admin") {
68
+ json(res, 403, { error: "Admin access required" });
69
+ return null;
70
+ }
71
+ return user;
72
+ }
73
+ // ---------------------------------------------------------------------------
74
+ // Email sending via Resend
75
+ // ---------------------------------------------------------------------------
76
+ /**
77
+ * The standardized default sender name: "RepoOS at <repo>", where <repo> is
78
+ * the checkout's directory name (same convention as the ntfy test
79
+ * notification in notify.ts). auth.emailProvider.fromName overrides it.
80
+ */
81
+ function defaultFromName(config) {
82
+ return `RepoOS at ${basename(config.root)}`;
83
+ }
84
+ /**
85
+ * RFC 5322 `From` header: "Display Name <addr>". Without a configured
86
+ * fromName, mail clients fall back to showing the address's local part
87
+ * (e.g. "otp") as the sender name, so this always supplies one.
88
+ */
89
+ export function buildFromHeader(config, provider) {
90
+ const name = provider.fromName || defaultFromName(config);
91
+ return `${name} <${provider.fromAddress}>`;
92
+ }
93
+ async function sendResendEmail(config, toEmail, subject, html) {
94
+ const provider = config.auth?.emailProvider;
95
+ if (!provider || provider.type !== "resend")
96
+ return false;
97
+ try {
98
+ const res = await fetch("https://api.resend.com/emails", {
99
+ method: "POST",
100
+ headers: {
101
+ Authorization: `Bearer ${provider.apiKey}`,
102
+ "Content-Type": "application/json",
103
+ },
104
+ body: JSON.stringify({
105
+ from: buildFromHeader(config, provider),
106
+ to: [toEmail],
107
+ subject,
108
+ html,
109
+ }),
110
+ });
111
+ if (!res.ok) {
112
+ const detail = await res.text().catch(() => "");
113
+ console.error(`Resend send to ${toEmail} failed (${res.status}): ${detail.slice(0, 500)}`);
114
+ }
115
+ return res.ok;
116
+ }
117
+ catch (err) {
118
+ console.error(`Resend send to ${toEmail} threw: ${err instanceof Error ? err.message : String(err)}`);
119
+ return false;
120
+ }
121
+ }
122
+ async function sendOtpEmail(config, toEmail, code) {
123
+ const repoName = basename(config.root);
124
+ return sendResendEmail(config, toEmail, `${code} Your RepoOS Login Code — ${repoName}`, `
125
+ <div style="font-family: sans-serif; max-width: 480px; margin: 0 auto; padding: 32px;">
126
+ <h2 style="margin-bottom: 16px;">RepoOS Login Code — ${repoName}</h2>
127
+ <p>Your one-time login code for <strong>${repoName}</strong> is:</p>
128
+ <div style="font-size: 32px; font-weight: bold; letter-spacing: 8px; margin: 24px 0; text-align: center; color: #333;">
129
+ ${code}
130
+ </div>
131
+ <p style="color: #666; font-size: 14px;">This code expires in 10 minutes and can only be used once.</p>
132
+ <p style="color: #999; font-size: 12px; margin-top: 24px;">If you use RepoOS on more than one repo, this code only works for <strong>${repoName}</strong> — check you're on the right one before entering it.</p>
133
+ <p style="color: #999; font-size: 12px; margin-top: 8px;">If you didn't request this code, you can safely ignore this email.</p>
134
+ </div>
135
+ `);
136
+ }
137
+ /** Best-effort base URL for links in outbound email: proto + host from the inviting request. */
138
+ function requestOrigin(req) {
139
+ const proto = req.headers["x-forwarded-proto"] ?? "http";
140
+ return `${proto}://${req.headers.host}`;
141
+ }
142
+ async function sendInviteEmail(config, toEmail, loginUrl) {
143
+ const repoName = basename(config.root);
144
+ return sendResendEmail(config, toEmail, `You're invited to RepoOS at ${repoName}`, `
145
+ <div style="font-family: sans-serif; max-width: 480px; margin: 0 auto; padding: 32px;">
146
+ <h2 style="margin-bottom: 16px;">You're invited to RepoOS at ${repoName}</h2>
147
+ <p>An admin has added <strong>${toEmail}</strong> to the allowlist for the <strong>${repoName}</strong> repo. Sign in any time with a one-time email code${config.auth?.google ? " or Google" : ""}:</p>
148
+ <p style="margin: 24px 0; text-align: center;">
149
+ <a href="${loginUrl}" style="display: inline-block; padding: 12px 24px; background: #3b82f6; color: #fff; text-decoration: none; border-radius: 8px; font-weight: 600;">Sign in to RepoOS</a>
150
+ </p>
151
+ <p style="color: #999; font-size: 12px; margin-top: 24px;">If you use RepoOS on more than one repo, this invite is only for <strong>${repoName}</strong> — check you're on the right one before signing in.</p>
152
+ <p style="color: #999; font-size: 12px; margin-top: 8px;">If you weren't expecting this, you can safely ignore this email.</p>
153
+ </div>
154
+ `);
155
+ }
156
+ let cachedJwks = null;
157
+ const JWKS_CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
158
+ async function getGoogleJwks() {
159
+ const now = Date.now();
160
+ if (cachedJwks && now - cachedJwks.fetchedAt < JWKS_CACHE_TTL_MS) {
161
+ return cachedJwks.keys;
162
+ }
163
+ const res = await fetch("https://www.googleapis.com/oauth2/v3/certs");
164
+ if (!res.ok)
165
+ throw new Error(`Failed to fetch Google JWKS: ${res.status}`);
166
+ const data = (await res.json());
167
+ cachedJwks = { keys: data.keys, fetchedAt: now };
168
+ return data.keys;
169
+ }
170
+ function base64UrlToBuffer(b64url) {
171
+ const b64 = b64url.replace(/-/g, "+").replace(/_/g, "/");
172
+ const padded = b64 + "=".repeat((4 - (b64.length % 4)) % 4);
173
+ return Buffer.from(padded, "base64");
174
+ }
175
+ function importRsaPublicKey(n, e) {
176
+ const nBuf = base64UrlToBuffer(n);
177
+ const eBuf = base64UrlToBuffer(e);
178
+ function encodeLength(len) {
179
+ if (len < 0x80)
180
+ return Buffer.from([len]);
181
+ const bytes = [];
182
+ let l = len;
183
+ while (l > 0) {
184
+ bytes.unshift(l & 0xff);
185
+ l >>= 8;
186
+ }
187
+ return Buffer.from([0x80 | bytes.length, ...bytes]);
188
+ }
189
+ function encodeInteger(buf) {
190
+ const prefix = buf[0] & 0x80 ? Buffer.from([0x00]) : Buffer.alloc(0);
191
+ return Buffer.concat([
192
+ Buffer.from([0x02]),
193
+ encodeLength(buf.length + prefix.length),
194
+ prefix,
195
+ buf,
196
+ ]);
197
+ }
198
+ const nEncoded = encodeInteger(nBuf);
199
+ const eEncoded = encodeInteger(eBuf);
200
+ const rsaPubKey = Buffer.concat([
201
+ Buffer.from([0x30]),
202
+ encodeLength(nEncoded.length + eEncoded.length),
203
+ nEncoded,
204
+ eEncoded,
205
+ ]);
206
+ const algId = Buffer.from("300d06092a864886f70d01010b0500", "hex");
207
+ const spki = Buffer.concat([
208
+ Buffer.from([0x30]),
209
+ encodeLength(algId.length + rsaPubKey.length + 1),
210
+ algId,
211
+ Buffer.from([0x03]),
212
+ encodeLength(rsaPubKey.length + 1),
213
+ Buffer.from([0x00]),
214
+ rsaPubKey,
215
+ ]);
216
+ return createPublicKey({ key: spki, format: "der", type: "spki" });
217
+ }
218
+ async function verifyGoogleIdToken(idToken, clientId) {
219
+ const parts = idToken.split(".");
220
+ if (parts.length !== 3)
221
+ return null;
222
+ const [headerB64, payloadB64, signatureB64] = parts;
223
+ const header = JSON.parse(base64UrlToBuffer(headerB64).toString("utf8"));
224
+ if (header.alg !== "RS256" || header.typ !== "JWT")
225
+ return null;
226
+ const payload = JSON.parse(base64UrlToBuffer(payloadB64).toString("utf8"));
227
+ // Standard claims validation
228
+ if (payload.iss !== "accounts.google.com" && payload.iss !== "https://accounts.google.com") {
229
+ return null;
230
+ }
231
+ if (payload.aud !== clientId)
232
+ return null;
233
+ if (typeof payload.exp !== "number" || payload.exp * 1000 < Date.now())
234
+ return null;
235
+ // Fetch JWKS and verify signature
236
+ try {
237
+ const keys = await getGoogleJwks();
238
+ const key = keys.find((k) => k.kid === header.kid);
239
+ if (!key)
240
+ return null;
241
+ const publicKey = await importRsaPublicKey(key.n, key.e);
242
+ const valid = createVerify("RSA-SHA256")
243
+ .update(idToken.split(".")[0] + "." + payloadB64)
244
+ .verify(publicKey, signatureB64.replace(/-/g, "+").replace(/_/g, "/"));
245
+ if (!valid)
246
+ return null;
247
+ if (typeof payload.email !== "string")
248
+ return null;
249
+ return { email: payload.email, sub: payload.sub };
250
+ }
251
+ catch {
252
+ return null;
253
+ }
254
+ }
255
+ // ---------------------------------------------------------------------------
256
+ // Route handlers
257
+ // ---------------------------------------------------------------------------
258
+ /** Auth status: is auth enabled? Is bootstrap needed? */
259
+ export const authStatus = (ctx, _req, res) => {
260
+ const { config } = ctx;
261
+ const auth = config.auth ?? {};
262
+ const store = getAuthStore(config.root);
263
+ const userCount = store?.listUsers().length ?? 0;
264
+ return json(res, 200, {
265
+ enabled: auth.enabled === true,
266
+ bootstrapNeeded: auth.enabled === true && userCount === 0,
267
+ hasGoogle: !!(auth.google?.clientId && auth.google?.clientSecret),
268
+ hasEmailProvider: !!(auth.emailProvider?.apiKey && auth.emailProvider?.fromAddress),
269
+ // So the login page can show which instance you're signing into — the
270
+ // same "RepoOS at <repo>" convention used in outbound email. Just the
271
+ // directory name, never the full server filesystem path.
272
+ repoName: basename(config.root),
273
+ });
274
+ };
275
+ /** Bootstrap the first admin account. Only works when no users exist. */
276
+ export const bootstrapAdmin = async (ctx, req, res) => {
277
+ const { config } = ctx;
278
+ if (!config.auth?.enabled) {
279
+ return json(res, 400, { error: "Auth is not enabled" });
280
+ }
281
+ const store = getAuthStore(config.root);
282
+ if (!store) {
283
+ return json(res, 500, { error: "Auth store unavailable" });
284
+ }
285
+ if (store.listUsers().length > 0) {
286
+ return json(res, 400, { error: "Users already exist — use the admin panel instead" });
287
+ }
288
+ // Startup already fails closed when auth is enabled with zero users and no
289
+ // bootstrapAdmin configured (server.ts), so this is always set here — but
290
+ // guard anyway rather than trust that invariant blindly.
291
+ const configuredAdmin = config.auth?.bootstrapAdmin?.trim().toLowerCase();
292
+ if (!configuredAdmin) {
293
+ return json(res, 400, { error: "No bootstrap admin email is configured" });
294
+ }
295
+ const body = (await readBody(req));
296
+ const email = typeof body.email === "string" ? body.email.trim().toLowerCase() : "";
297
+ if (!isValidEmail(email)) {
298
+ return json(res, 400, { error: "Valid email address required" });
299
+ }
300
+ // Only the operator-configured email may claim the founding admin account —
301
+ // otherwise the first visitor to reach /login on a fresh instance (which,
302
+ // per this task's own premise, may be sitting behind a public Cloudflare
303
+ // Tunnel) could self-appoint as admin before the real owner ever logs in.
304
+ if (email !== configuredAdmin) {
305
+ return json(res, 403, { error: "This email is not authorized to bootstrap the admin account" });
306
+ }
307
+ // Create the admin user
308
+ store.upsertUser(email, "admin", null, email.split("@")[0]);
309
+ store.logAudit("bootstrap_admin", email, null, "Initial bootstrap");
310
+ // Clear the bootstrap admin config
311
+ if (config.auth) {
312
+ delete config.auth.bootstrapAdmin;
313
+ }
314
+ // Create a session for the new admin
315
+ const maxAge = getConfiguredMaxAge(config);
316
+ const token = store.createSession(email, "admin", maxAge);
317
+ if (!token) {
318
+ return json(res, 500, { error: "Failed to create session" });
319
+ }
320
+ const secure = getSecureFlag(req);
321
+ res.setHeader("Set-Cookie", buildSessionCookie(token, maxAge, secure));
322
+ return json(res, 200, {
323
+ ok: true,
324
+ email,
325
+ role: "admin",
326
+ });
327
+ };
328
+ /** Request an OTP code via email. */
329
+ export const requestOtp = async (ctx, req, res) => {
330
+ const { config, logger } = ctx;
331
+ if (!config.auth?.enabled) {
332
+ return json(res, 400, { error: "Auth is not enabled" });
333
+ }
334
+ const store = getAuthStore(config.root);
335
+ if (!store) {
336
+ return json(res, 500, { error: "Auth store unavailable" });
337
+ }
338
+ const ip = getClientIp(req);
339
+ if (!globalRateLimiter.tryAcquire(ip)) {
340
+ return json(res, 429, { error: "Too many requests. Please try again later." });
341
+ }
342
+ const body = (await readBody(req));
343
+ const email = typeof body.email === "string" ? body.email.trim().toLowerCase() : "";
344
+ if (!isValidEmail(email)) {
345
+ // Non-enumerating response
346
+ return json(res, 200, { ok: true });
347
+ }
348
+ // Rate limit per email
349
+ const emailKey = `otp_req:${email}`;
350
+ if (!otpRequestLimiter.tryAcquire(emailKey)) {
351
+ return json(res, 429, { error: "Too many OTP requests. Please wait before trying again." });
352
+ }
353
+ // Check if email is in the allowlist
354
+ const user = store.getUser(email);
355
+ if (!user) {
356
+ // Non-enumerating: same response whether or not user exists
357
+ return json(res, 200, { ok: true });
358
+ }
359
+ // Generate and store OTP
360
+ const code = generateOtp();
361
+ const codeHash = hashOtp(code);
362
+ store.createOtpChallenge(email, codeHash, 600, ip); // 10 minutes
363
+ // Send email
364
+ const sent = await sendOtpEmail(config, email, code);
365
+ if (!sent) {
366
+ logger.system("warn", "Failed to send OTP email", { email });
367
+ // Still return ok to avoid enumeration
368
+ }
369
+ return json(res, 200, { ok: true });
370
+ };
371
+ /** Verify an OTP code and create a session. */
372
+ export const verifyOtp = async (ctx, req, res) => {
373
+ const { config } = ctx;
374
+ if (!config.auth?.enabled) {
375
+ return json(res, 400, { error: "Auth is not enabled" });
376
+ }
377
+ const store = getAuthStore(config.root);
378
+ if (!store) {
379
+ return json(res, 500, { error: "Auth store unavailable" });
380
+ }
381
+ const ip = getClientIp(req);
382
+ const body = (await readBody(req));
383
+ const email = typeof body.email === "string" ? body.email.trim().toLowerCase() : "";
384
+ const code = typeof body.code === "string" ? body.code.trim() : "";
385
+ if (!isValidEmail(email) || !code) {
386
+ return json(res, 401, { error: "Invalid or expired code" });
387
+ }
388
+ // Rate limit verify attempts
389
+ if (!otpVerifyLimiter.tryAcquire(`otp_verify:${email}`)) {
390
+ return json(res, 429, { error: "Too many failed attempts. Please wait before trying again." });
391
+ }
392
+ const user = store.getUser(email);
393
+ if (!user) {
394
+ return json(res, 401, { error: "Invalid or expired code" });
395
+ }
396
+ // Dev backdoor: a static code, set only via REPOOS_AUTH_DEV_BACKDOOR_CODE
397
+ // and only ever loaded outside production (see config.ts), lets the
398
+ // developer and agents log into a real managed preview without a mailbox.
399
+ const usedBackdoor = !!config.auth.devBackdoorCode &&
400
+ process.env.NODE_ENV !== "production" &&
401
+ timingSafeEqualStr(code, config.auth.devBackdoorCode);
402
+ if (!usedBackdoor) {
403
+ const codeHash = hashOtp(code);
404
+ const challenge = store.findValidOtp(email, codeHash);
405
+ if (!challenge) {
406
+ // Check if any valid (non-matching) OTP exists to distinguish "wrong code" from "expired"
407
+ return json(res, 401, { error: "Invalid or expired code" });
408
+ }
409
+ store.markOtpUsed(challenge.id);
410
+ }
411
+ otpVerifyLimiter.reset(`otp_verify:${email}`);
412
+ otpRequestLimiter.reset(`otp_req:${email}`);
413
+ // Create session
414
+ const maxAge = getConfiguredMaxAge(config);
415
+ const token = store.createSession(email, user.role, maxAge);
416
+ if (!token) {
417
+ return json(res, 500, { error: "Failed to create session" });
418
+ }
419
+ store.logAudit("otp_login", email, email);
420
+ const secure = getSecureFlag(req);
421
+ res.setHeader("Set-Cookie", buildSessionCookie(token, maxAge, secure));
422
+ return json(res, 200, {
423
+ ok: true,
424
+ email,
425
+ role: user.role,
426
+ });
427
+ };
428
+ /** Google OAuth login redirect. */
429
+ export const googleLogin = (ctx, _req, res) => {
430
+ const { config } = ctx;
431
+ if (!config.auth?.enabled || !config.auth?.google) {
432
+ return json(res, 400, { error: "Google OAuth not configured" });
433
+ }
434
+ const state = randomHex(16);
435
+ const nonce = randomHex(16);
436
+ const codeVerifier = generatePkceVerifier();
437
+ const codeChallenge = pkceChallenge(codeVerifier);
438
+ const redirectUri = `${_req.headers["x-forwarded-proto"] ?? "http"}://${_req.headers.host}/api/auth/callback/google`;
439
+ // Store state, nonce, and PKCE verifier in short-lived cookies
440
+ const secure = getSecureFlag(_req);
441
+ const cookieBase = "HttpOnly; Path=/; Max-Age=600; SameSite=Lax" + (secure ? "; Secure" : "");
442
+ res.setHeader("Set-Cookie", [
443
+ `repoos_oauth_state=${state}; ${cookieBase}`,
444
+ `repoos_oauth_nonce=${nonce}; ${cookieBase}`,
445
+ `repoos_oauth_pkce=${codeVerifier}; ${cookieBase}`,
446
+ ]);
447
+ const params = new URLSearchParams({
448
+ client_id: config.auth.google.clientId,
449
+ redirect_uri: redirectUri,
450
+ response_type: "code",
451
+ scope: "openid email profile",
452
+ state,
453
+ nonce,
454
+ access_type: "offline",
455
+ prompt: "select_account",
456
+ code_challenge: codeChallenge,
457
+ code_challenge_method: "S256",
458
+ });
459
+ res.writeHead(302, { Location: `https://accounts.google.com/o/oauth2/v2/auth?${params}` });
460
+ res.end();
461
+ };
462
+ /** Google OAuth callback. */
463
+ export const googleCallback = async (ctx, req, res) => {
464
+ const { config, logger } = ctx;
465
+ const url = new URL(req.url ?? "/", "http://localhost");
466
+ const code = url.searchParams.get("code");
467
+ const state = url.searchParams.get("state");
468
+ const error = url.searchParams.get("error");
469
+ if (error || !code || !state) {
470
+ res.writeHead(302, { Location: "/login?error=oauth_failed" });
471
+ res.end();
472
+ return;
473
+ }
474
+ // Verify state
475
+ const cookies = parseCookies(req.headers.cookie);
476
+ const savedState = cookies["repoos_oauth_state"];
477
+ const savedNonce = cookies["repoos_oauth_nonce"];
478
+ const codeVerifier = cookies["repoos_oauth_pkce"];
479
+ if (!savedState || !timingSafeEqualStr(state, savedState)) {
480
+ res.writeHead(302, { Location: "/login?error=invalid_state" });
481
+ res.end();
482
+ return;
483
+ }
484
+ // Nonce cookie must be present — reject if missing
485
+ if (!savedNonce) {
486
+ res.writeHead(302, { Location: "/login?error=invalid_nonce" });
487
+ res.end();
488
+ return;
489
+ }
490
+ // Clear the OAuth cookies
491
+ res.setHeader("Set-Cookie", [
492
+ "repoos_oauth_state=; HttpOnly; Path=/; Max-Age=0",
493
+ "repoos_oauth_nonce=; HttpOnly; Path=/; Max-Age=0",
494
+ "repoos_oauth_pkce=; HttpOnly; Path=/; Max-Age=0",
495
+ ]);
496
+ if (!config.auth?.google) {
497
+ res.writeHead(302, { Location: "/login?error=oauth_not_configured" });
498
+ res.end();
499
+ return;
500
+ }
501
+ const proto = req.headers["x-forwarded-proto"] ?? "http";
502
+ const redirectUri = `${proto}://${req.headers.host}/api/auth/callback/google`;
503
+ // Exchange code for tokens
504
+ try {
505
+ const tokenBody = {
506
+ code,
507
+ client_id: config.auth.google.clientId,
508
+ client_secret: config.auth.google.clientSecret,
509
+ redirect_uri: redirectUri,
510
+ grant_type: "authorization_code",
511
+ };
512
+ if (codeVerifier) {
513
+ tokenBody.code_verifier = codeVerifier;
514
+ }
515
+ const tokenRes = await fetch("https://oauth2.googleapis.com/token", {
516
+ method: "POST",
517
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
518
+ body: new URLSearchParams(tokenBody),
519
+ });
520
+ if (!tokenRes.ok) {
521
+ logger.system("warn", "Google OAuth token exchange failed", { status: tokenRes.status });
522
+ res.writeHead(302, { Location: "/login?error=oauth_failed" });
523
+ res.end();
524
+ return;
525
+ }
526
+ const tokens = (await tokenRes.json());
527
+ if (!tokens.id_token) {
528
+ res.writeHead(302, { Location: "/login?error=oauth_failed" });
529
+ res.end();
530
+ return;
531
+ }
532
+ // Verify the id_token (aud, iss, signature, expiry) via Google JWKS
533
+ const verified = await verifyGoogleIdToken(tokens.id_token, config.auth.google.clientId);
534
+ if (!verified) {
535
+ res.writeHead(302, { Location: "/login?error=invalid_token" });
536
+ res.end();
537
+ return;
538
+ }
539
+ // Verify nonce claim matches
540
+ // Decode payload again for nonce check (already verified signature above)
541
+ const payload = JSON.parse(Buffer.from(tokens.id_token.split(".")[1], "base64url").toString("utf8"));
542
+ if (payload.nonce !== savedNonce) {
543
+ res.writeHead(302, { Location: "/login?error=invalid_nonce" });
544
+ res.end();
545
+ return;
546
+ }
547
+ const email = verified.email;
548
+ // Check allowlist
549
+ const store = getAuthStore(config.root);
550
+ const user = store?.getUser(email);
551
+ if (!user) {
552
+ res.writeHead(302, { Location: "/login?error=not_allowed" });
553
+ res.end();
554
+ return;
555
+ }
556
+ // Create session
557
+ const maxAge = getConfiguredMaxAge(config);
558
+ const token = store.createSession(email, user.role, maxAge);
559
+ if (!token) {
560
+ res.writeHead(302, { Location: "/login?error=session_failed" });
561
+ res.end();
562
+ return;
563
+ }
564
+ store.logAudit("google_login", email, email);
565
+ const secure = getSecureFlag(req);
566
+ res.setHeader("Set-Cookie", buildSessionCookie(token, maxAge, secure));
567
+ res.writeHead(302, { Location: "/" });
568
+ res.end();
569
+ return;
570
+ }
571
+ catch (err) {
572
+ logger.system("error", "Google OAuth callback error", { error: String(err) });
573
+ res.writeHead(302, { Location: "/login?error=oauth_failed" });
574
+ res.end();
575
+ return;
576
+ }
577
+ };
578
+ /** Get current user info. */
579
+ export const authMe = (ctx, req, res) => {
580
+ const { config } = ctx;
581
+ if (!config.auth?.enabled) {
582
+ return json(res, 200, { authenticated: false, authEnabled: false });
583
+ }
584
+ const user = getCurrentUser(req, config);
585
+ if (!user) {
586
+ return json(res, 200, { authenticated: false, authEnabled: true });
587
+ }
588
+ return json(res, 200, {
589
+ authenticated: true,
590
+ authEnabled: true,
591
+ email: user.email,
592
+ role: user.role,
593
+ });
594
+ };
595
+ /** Logout: revoke session and clear cookie. */
596
+ export const authLogout = (ctx, req, res) => {
597
+ const { config } = ctx;
598
+ const token = getSessionTokenFromRequest(req);
599
+ if (token) {
600
+ const store = getAuthStore(config.root);
601
+ store?.revokeSession(token);
602
+ }
603
+ res.setHeader("Set-Cookie", `${SESSION_COOKIE_NAME}=; HttpOnly; Path=/; Max-Age=0; SameSite=Lax`);
604
+ return json(res, 200, { ok: true });
605
+ };
606
+ // ---------------------------------------------------------------------------
607
+ // Admin routes
608
+ // ---------------------------------------------------------------------------
609
+ /** List all users. */
610
+ export const listUsers = (ctx, req, res) => {
611
+ const { config } = ctx;
612
+ const user = requireAdmin(req, config, res);
613
+ if (!user)
614
+ return;
615
+ const store = getAuthStore(config.root);
616
+ if (!store)
617
+ return json(res, 500, { error: "Auth store unavailable" });
618
+ const users = store.listUsers();
619
+ return json(res, 200, { users });
620
+ };
621
+ /** Add a user. */
622
+ export const addUser = async (ctx, req, res) => {
623
+ const { config } = ctx;
624
+ const admin = requireAdmin(req, config, res);
625
+ if (!admin)
626
+ return;
627
+ const store = getAuthStore(config.root);
628
+ if (!store)
629
+ return json(res, 500, { error: "Auth store unavailable" });
630
+ const body = (await readBody(req));
631
+ const email = typeof body.email === "string" ? body.email.trim().toLowerCase() : "";
632
+ const role = body.role === "admin" ? "admin" : "member";
633
+ if (!isValidEmail(email)) {
634
+ return json(res, 400, { error: "Valid email address required" });
635
+ }
636
+ const existing = store.getUser(email);
637
+ if (existing) {
638
+ return json(res, 409, { error: "User already exists" });
639
+ }
640
+ store.upsertUser(email, role, admin.email);
641
+ store.logAudit("add_user", email, admin.email, `role: ${role}`);
642
+ return json(res, 200, { ok: true, email, role });
643
+ };
644
+ /** (Re)send an invite email. Callable any time, not just once at add-time. */
645
+ export const sendInvite = async (ctx, req, res) => {
646
+ const { config } = ctx;
647
+ const admin = requireAdmin(req, config, res);
648
+ if (!admin)
649
+ return;
650
+ const store = getAuthStore(config.root);
651
+ if (!store)
652
+ return json(res, 500, { error: "Auth store unavailable" });
653
+ // Extract email from URL path: /api/auth/users/:email/invite
654
+ const url = new URL(req.url ?? "/", "http://localhost");
655
+ const segments = url.pathname.split("/").filter(Boolean);
656
+ const email = decodeURIComponent(segments[segments.length - 2] ?? "")
657
+ .trim()
658
+ .toLowerCase();
659
+ if (!isValidEmail(email)) {
660
+ return json(res, 400, { error: "Invalid email" });
661
+ }
662
+ const user = store.getUser(email);
663
+ if (!user) {
664
+ return json(res, 404, { error: "User not found" });
665
+ }
666
+ if (!config.auth?.emailProvider) {
667
+ return json(res, 400, { error: "No email provider configured — cannot send invites" });
668
+ }
669
+ const loginUrl = `${requestOrigin(req)}/login`;
670
+ const sent = await sendInviteEmail(config, email, loginUrl);
671
+ if (!sent) {
672
+ return json(res, 502, {
673
+ error: "Failed to send invite email — check server logs for the provider error",
674
+ });
675
+ }
676
+ store.logAudit("invite_sent", email, admin.email);
677
+ return json(res, 200, { ok: true });
678
+ };
679
+ /** Remove a user. */
680
+ export const deleteUser = async (ctx, req, res) => {
681
+ const { config } = ctx;
682
+ const admin = requireAdmin(req, config, res);
683
+ if (!admin)
684
+ return;
685
+ const store = getAuthStore(config.root);
686
+ if (!store)
687
+ return json(res, 500, { error: "Auth store unavailable" });
688
+ // Extract email from URL path: /api/auth/users/:email
689
+ const url = new URL(req.url ?? "/", "http://localhost");
690
+ const email = decodeURIComponent(url.pathname.split("/").pop() ?? "")
691
+ .trim()
692
+ .toLowerCase();
693
+ if (!isValidEmail(email)) {
694
+ return json(res, 400, { error: "Invalid email" });
695
+ }
696
+ // Prevent removing the last admin
697
+ const user = store.getUser(email);
698
+ if (!user) {
699
+ return json(res, 404, { error: "User not found" });
700
+ }
701
+ if (user.role === "admin" && store.getAdminCount() <= 1) {
702
+ return json(res, 400, { error: "Cannot remove the last admin" });
703
+ }
704
+ store.deleteUser(email);
705
+ store.revokeAllSessions(email);
706
+ store.logAudit("remove_user", email, admin.email);
707
+ return json(res, 200, { ok: true });
708
+ };
709
+ /** Change a user's role. */
710
+ export const updateUserRole = async (ctx, req, res) => {
711
+ const { config } = ctx;
712
+ const admin = requireAdmin(req, config, res);
713
+ if (!admin)
714
+ return;
715
+ const store = getAuthStore(config.root);
716
+ if (!store)
717
+ return json(res, 500, { error: "Auth store unavailable" });
718
+ const body = (await readBody(req));
719
+ const role = body.role === "admin" ? "admin" : "member";
720
+ // Extract email from URL path
721
+ const url = new URL(req.url ?? "/", "http://localhost");
722
+ const email = decodeURIComponent(url.pathname.split("/").pop() ?? "")
723
+ .trim()
724
+ .toLowerCase();
725
+ if (!isValidEmail(email)) {
726
+ return json(res, 400, { error: "Invalid email" });
727
+ }
728
+ const user = store.getUser(email);
729
+ if (!user) {
730
+ return json(res, 404, { error: "User not found" });
731
+ }
732
+ // Prevent demoting the last admin
733
+ if (user.role === "admin" && role !== "admin" && store.getAdminCount() <= 1) {
734
+ return json(res, 400, { error: "Cannot demote the last admin" });
735
+ }
736
+ store.upsertUser(email, role, admin.email);
737
+ // Propagate the new role to all active sessions so authorization is immediate.
738
+ store.updateSessionRoles(email, role);
739
+ store.logAudit("change_role", email, admin.email, `role: ${role}`);
740
+ return json(res, 200, { ok: true, email, role });
741
+ };
742
+ /** Get audit log. */
743
+ export const getAuditLog = (ctx, req, res) => {
744
+ const { config } = ctx;
745
+ const admin = requireAdmin(req, config, res);
746
+ if (!admin)
747
+ return;
748
+ const store = getAuthStore(config.root);
749
+ if (!store)
750
+ return json(res, 500, { error: "Auth store unavailable" });
751
+ const url = new URL(req.url ?? "/", "http://localhost");
752
+ const limit = Math.min(Number(url.searchParams.get("limit") ?? "50"), 200);
753
+ const entries = store.getAuditLog(limit);
754
+ return json(res, 200, { entries });
755
+ };