ruflo 3.7.0-alpha.11 → 3.7.0-alpha.13

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 (498) hide show
  1. package/README.md +393 -393
  2. package/bin/ruflo.js +57 -57
  3. package/package.json +1 -1
  4. package/src/chat-ui/Dockerfile +25 -25
  5. package/src/chat-ui/patch-mcp-url-safety.sh +28 -28
  6. package/src/config/config.example.json +76 -76
  7. package/src/mcp-bridge/Dockerfile +45 -45
  8. package/src/mcp-bridge/index.js +1668 -1668
  9. package/src/mcp-bridge/mcp-stdio-kernel.js +159 -159
  10. package/src/mcp-bridge/package.json +17 -17
  11. package/src/mcp-bridge/test-harness.js +470 -470
  12. package/src/nginx/Dockerfile +10 -10
  13. package/src/nginx/nginx.conf +67 -67
  14. package/src/nginx/static/favicon-dark.svg +4 -4
  15. package/src/nginx/static/favicon.svg +4 -4
  16. package/src/nginx/static/icon.svg +5 -5
  17. package/src/nginx/static/logo.svg +9 -9
  18. package/src/nginx/static/manifest.json +22 -22
  19. package/src/nginx/static/welcome.js +184 -184
  20. package/src/ruvocal/.claude/skills/add-model-descriptions/SKILL.md +73 -73
  21. package/src/ruvocal/.claude-flow/daemon-state.json +135 -0
  22. package/src/ruvocal/.claude-flow/data/pending-insights.jsonl +0 -0
  23. package/src/ruvocal/.claude-flow/data/ranked-context.json +5 -0
  24. package/src/ruvocal/.claude-flow/logs/daemon.log +31 -0
  25. package/src/ruvocal/.claude-flow/logs/headless/audit_1777949411822_juxau0_prompt.log +989 -0
  26. package/src/ruvocal/.claude-flow/logs/headless/audit_1777949411822_juxau0_result.log +67 -0
  27. package/src/ruvocal/.claude-flow/logs/headless/audit_1777950042278_jvj5xq_prompt.log +989 -0
  28. package/src/ruvocal/.claude-flow/logs/headless/audit_1777950042278_jvj5xq_result.log +93 -0
  29. package/src/ruvocal/.claude-flow/logs/headless/optimize_1777949531823_yt5yc2_prompt.log +1498 -0
  30. package/src/ruvocal/.claude-flow/logs/headless/optimize_1777949531823_yt5yc2_result.log +93 -0
  31. package/src/ruvocal/.claude-flow/logs/headless/testgaps_1777949771821_elw1j4_prompt.log +1498 -0
  32. package/src/ruvocal/.claude-flow/logs/headless/testgaps_1777949771821_elw1j4_result.log +100 -0
  33. package/src/ruvocal/.claude-flow/metrics/codebase-map.json +11 -0
  34. package/src/ruvocal/.claude-flow/metrics/consolidation.json +6 -0
  35. package/src/ruvocal/.claude-flow/neural/stats.json +6 -0
  36. package/src/ruvocal/.claude-flow/sessions/current.json +13 -0
  37. package/src/ruvocal/.devcontainer/Dockerfile +9 -9
  38. package/src/ruvocal/.devcontainer/devcontainer.json +36 -36
  39. package/src/ruvocal/.dockerignore +16 -16
  40. package/src/ruvocal/.eslintignore +13 -13
  41. package/src/ruvocal/.eslintrc.cjs +45 -45
  42. package/src/ruvocal/.gcloudignore +18 -18
  43. package/src/ruvocal/.github/ISSUE_TEMPLATE/bug-report--chat-ui-.md +43 -43
  44. package/src/ruvocal/.github/ISSUE_TEMPLATE/config-support.md +9 -9
  45. package/src/ruvocal/.github/ISSUE_TEMPLATE/feature-request--chat-ui-.md +17 -17
  46. package/src/ruvocal/.github/ISSUE_TEMPLATE/huggingchat.md +11 -11
  47. package/src/ruvocal/.github/release.yml +16 -16
  48. package/src/ruvocal/.github/workflows/build-docs.yml +18 -18
  49. package/src/ruvocal/.github/workflows/build-image.yml +142 -142
  50. package/src/ruvocal/.github/workflows/build-pr-docs.yml +20 -20
  51. package/src/ruvocal/.github/workflows/deploy-dev.yml +63 -63
  52. package/src/ruvocal/.github/workflows/deploy-prod.yml +78 -78
  53. package/src/ruvocal/.github/workflows/lint-and-test.yml +84 -84
  54. package/src/ruvocal/.github/workflows/slugify.yaml +72 -72
  55. package/src/ruvocal/.github/workflows/trufflehog.yml +17 -17
  56. package/src/ruvocal/.github/workflows/upload-pr-documentation.yml +16 -16
  57. package/src/ruvocal/.husky/lint-stage-config.js +4 -4
  58. package/src/ruvocal/.husky/pre-commit +2 -2
  59. package/src/ruvocal/.prettierignore +14 -14
  60. package/src/ruvocal/.prettierrc +7 -7
  61. package/src/ruvocal/.swarm/attestation.db +0 -0
  62. package/src/ruvocal/.swarm/hnsw.index +0 -0
  63. package/src/ruvocal/.swarm/hnsw.metadata.json +1 -0
  64. package/src/ruvocal/.swarm/memory.db +0 -0
  65. package/src/ruvocal/.swarm/schema.sql +305 -0
  66. package/src/ruvocal/CLAUDE.md +126 -126
  67. package/src/ruvocal/Dockerfile +96 -96
  68. package/src/ruvocal/LICENSE +202 -202
  69. package/src/ruvocal/PRIVACY.md +41 -41
  70. package/src/ruvocal/README.md +164 -164
  71. package/src/ruvocal/chart/Chart.yaml +5 -5
  72. package/src/ruvocal/chart/env/dev.yaml +260 -260
  73. package/src/ruvocal/chart/env/prod.yaml +273 -273
  74. package/src/ruvocal/chart/templates/_helpers.tpl +22 -22
  75. package/src/ruvocal/chart/templates/config.yaml +10 -10
  76. package/src/ruvocal/chart/templates/deployment.yaml +81 -81
  77. package/src/ruvocal/chart/templates/hpa.yaml +45 -45
  78. package/src/ruvocal/chart/templates/infisical.yaml +24 -24
  79. package/src/ruvocal/chart/templates/ingress-internal.yaml +32 -32
  80. package/src/ruvocal/chart/templates/ingress.yaml +32 -32
  81. package/src/ruvocal/chart/templates/network-policy.yaml +36 -36
  82. package/src/ruvocal/chart/templates/service-account.yaml +13 -13
  83. package/src/ruvocal/chart/templates/service-monitor.yaml +17 -17
  84. package/src/ruvocal/chart/templates/service.yaml +21 -21
  85. package/src/ruvocal/chart/values.yaml +73 -73
  86. package/src/ruvocal/cloudbuild.yaml +68 -68
  87. package/src/ruvocal/config/branding.env.example +19 -19
  88. package/src/ruvocal/docker-compose.yml +21 -21
  89. package/src/ruvocal/docs/adr/ADR-029-HUGGINGFACE-CHAT-UI-CLOUD-RUN.md +1236 -1236
  90. package/src/ruvocal/docs/adr/ADR-033-RUVECTOR-RUFLO-MCP-INTEGRATION.md +111 -111
  91. package/src/ruvocal/docs/adr/ADR-034-OPTIONAL-MCP-BACKENDS.md +117 -117
  92. package/src/ruvocal/docs/adr/ADR-035-MCP-TOOL-GROUPS.md +186 -186
  93. package/src/ruvocal/docs/adr/ADR-037-AUTOPILOT-CHAT-MODE.md +1500 -1500
  94. package/src/ruvocal/docs/adr/ADR-038-RUVOCAL-FORK.md +286 -286
  95. package/src/ruvocal/docs/source/_toctree.yml +30 -30
  96. package/src/ruvocal/docs/source/configuration/common-issues.md +38 -38
  97. package/src/ruvocal/docs/source/configuration/llm-router.md +105 -105
  98. package/src/ruvocal/docs/source/configuration/mcp-tools.md +84 -84
  99. package/src/ruvocal/docs/source/configuration/metrics.md +9 -9
  100. package/src/ruvocal/docs/source/configuration/open-id.md +57 -57
  101. package/src/ruvocal/docs/source/configuration/overview.md +89 -89
  102. package/src/ruvocal/docs/source/configuration/theming.md +20 -20
  103. package/src/ruvocal/docs/source/developing/architecture.md +48 -48
  104. package/src/ruvocal/docs/source/index.md +53 -53
  105. package/src/ruvocal/docs/source/installation/docker.md +43 -43
  106. package/src/ruvocal/docs/source/installation/helm.md +43 -43
  107. package/src/ruvocal/docs/source/installation/local.md +62 -62
  108. package/src/ruvocal/entrypoint.sh +18 -18
  109. package/src/ruvocal/mcp-bridge/Dockerfile +45 -45
  110. package/src/ruvocal/mcp-bridge/cloudbuild.yaml +49 -49
  111. package/src/ruvocal/mcp-bridge/index.js +1878 -1878
  112. package/src/ruvocal/mcp-bridge/mcp-stdio-kernel.js +159 -159
  113. package/src/ruvocal/mcp-bridge/package-lock.json +762 -762
  114. package/src/ruvocal/mcp-bridge/package.json +17 -17
  115. package/src/ruvocal/mcp-bridge/test-harness.js +470 -470
  116. package/src/ruvocal/package-lock.json +11741 -11741
  117. package/src/ruvocal/package.json +121 -121
  118. package/src/ruvocal/postcss.config.js +6 -6
  119. package/src/ruvocal/rvf.manifest.json +204 -204
  120. package/src/ruvocal/scripts/config.ts +64 -64
  121. package/src/ruvocal/scripts/generate-welcome.mjs +181 -181
  122. package/src/ruvocal/scripts/populate.ts +288 -288
  123. package/src/ruvocal/scripts/samples.txt +194 -194
  124. package/src/ruvocal/scripts/setups/vitest-setup-server.ts +44 -44
  125. package/src/ruvocal/scripts/updateLocalEnv.ts +48 -48
  126. package/src/ruvocal/src/ambient.d.ts +7 -7
  127. package/src/ruvocal/src/app.d.ts +29 -29
  128. package/src/ruvocal/src/app.html +53 -53
  129. package/src/ruvocal/src/hooks.server.ts +32 -32
  130. package/src/ruvocal/src/hooks.ts +6 -6
  131. package/src/ruvocal/src/lib/APIClient.ts +148 -148
  132. package/src/ruvocal/src/lib/actions/clickOutside.ts +18 -18
  133. package/src/ruvocal/src/lib/actions/snapScrollToBottom.ts +346 -346
  134. package/src/ruvocal/src/lib/buildPrompt.ts +33 -33
  135. package/src/ruvocal/src/lib/components/AnnouncementBanner.svelte +20 -20
  136. package/src/ruvocal/src/lib/components/BackgroundGenerationPoller.svelte +168 -168
  137. package/src/ruvocal/src/lib/components/CodeBlock.svelte +73 -73
  138. package/src/ruvocal/src/lib/components/CopyToClipBoardBtn.svelte +92 -92
  139. package/src/ruvocal/src/lib/components/DeleteConversationModal.svelte +75 -75
  140. package/src/ruvocal/src/lib/components/EditConversationModal.svelte +100 -100
  141. package/src/ruvocal/src/lib/components/ExpandNavigation.svelte +22 -22
  142. package/src/ruvocal/src/lib/components/FoundationBackground.svelte +242 -242
  143. package/src/ruvocal/src/lib/components/HoverTooltip.svelte +44 -44
  144. package/src/ruvocal/src/lib/components/HtmlPreviewModal.svelte +143 -143
  145. package/src/ruvocal/src/lib/components/InfiniteScroll.svelte +50 -50
  146. package/src/ruvocal/src/lib/components/MobileNav.svelte +300 -300
  147. package/src/ruvocal/src/lib/components/Modal.svelte +115 -115
  148. package/src/ruvocal/src/lib/components/ModelCardMetadata.svelte +71 -71
  149. package/src/ruvocal/src/lib/components/NavConversationItem.svelte +151 -151
  150. package/src/ruvocal/src/lib/components/NavMenu.svelte +313 -313
  151. package/src/ruvocal/src/lib/components/Pagination.svelte +97 -97
  152. package/src/ruvocal/src/lib/components/PaginationArrow.svelte +27 -27
  153. package/src/ruvocal/src/lib/components/Portal.svelte +24 -24
  154. package/src/ruvocal/src/lib/components/RetryBtn.svelte +18 -18
  155. package/src/ruvocal/src/lib/components/RuFloUniverse.svelte +185 -185
  156. package/src/ruvocal/src/lib/components/RufloHelpModal.svelte +411 -411
  157. package/src/ruvocal/src/lib/components/ScrollToBottomBtn.svelte +47 -47
  158. package/src/ruvocal/src/lib/components/ScrollToPreviousBtn.svelte +77 -77
  159. package/src/ruvocal/src/lib/components/ShareConversationModal.svelte +182 -182
  160. package/src/ruvocal/src/lib/components/StopGeneratingBtn.svelte +69 -69
  161. package/src/ruvocal/src/lib/components/SubscribeModal.svelte +87 -87
  162. package/src/ruvocal/src/lib/components/Switch.svelte +36 -36
  163. package/src/ruvocal/src/lib/components/SystemPromptModal.svelte +44 -44
  164. package/src/ruvocal/src/lib/components/Toast.svelte +27 -27
  165. package/src/ruvocal/src/lib/components/Tooltip.svelte +30 -30
  166. package/src/ruvocal/src/lib/components/WelcomeModal.svelte +46 -46
  167. package/src/ruvocal/src/lib/components/chat/Alternatives.svelte +77 -77
  168. package/src/ruvocal/src/lib/components/chat/BlockWrapper.svelte +72 -72
  169. package/src/ruvocal/src/lib/components/chat/ChatInput.svelte +490 -490
  170. package/src/ruvocal/src/lib/components/chat/ChatIntroduction.svelte +123 -123
  171. package/src/ruvocal/src/lib/components/chat/ChatMessage.svelte +548 -548
  172. package/src/ruvocal/src/lib/components/chat/ChatWindow.svelte +1057 -1057
  173. package/src/ruvocal/src/lib/components/chat/FileDropzone.svelte +92 -92
  174. package/src/ruvocal/src/lib/components/chat/ImageLightbox.svelte +66 -66
  175. package/src/ruvocal/src/lib/components/chat/MarkdownBlock.svelte +23 -23
  176. package/src/ruvocal/src/lib/components/chat/MarkdownRenderer.svelte +69 -69
  177. package/src/ruvocal/src/lib/components/chat/MarkdownRenderer.svelte.test.ts +58 -58
  178. package/src/ruvocal/src/lib/components/chat/MessageAvatar.svelte +103 -103
  179. package/src/ruvocal/src/lib/components/chat/ModelSwitch.svelte +64 -64
  180. package/src/ruvocal/src/lib/components/chat/OpenReasoningResults.svelte +81 -81
  181. package/src/ruvocal/src/lib/components/chat/TaskGroup.svelte +88 -88
  182. package/src/ruvocal/src/lib/components/chat/ToolUpdate.svelte +273 -273
  183. package/src/ruvocal/src/lib/components/chat/UploadedFile.svelte +253 -253
  184. package/src/ruvocal/src/lib/components/chat/UrlFetchModal.svelte +203 -203
  185. package/src/ruvocal/src/lib/components/chat/VoiceRecorder.svelte +214 -214
  186. package/src/ruvocal/src/lib/components/icons/IconBurger.svelte +20 -20
  187. package/src/ruvocal/src/lib/components/icons/IconCheap.svelte +20 -20
  188. package/src/ruvocal/src/lib/components/icons/IconChevron.svelte +24 -24
  189. package/src/ruvocal/src/lib/components/icons/IconDazzled.svelte +40 -40
  190. package/src/ruvocal/src/lib/components/icons/IconFast.svelte +20 -20
  191. package/src/ruvocal/src/lib/components/icons/IconLoading.svelte +22 -22
  192. package/src/ruvocal/src/lib/components/icons/IconMCP.svelte +28 -28
  193. package/src/ruvocal/src/lib/components/icons/IconMoon.svelte +21 -21
  194. package/src/ruvocal/src/lib/components/icons/IconNew.svelte +20 -20
  195. package/src/ruvocal/src/lib/components/icons/IconOmni.svelte +90 -90
  196. package/src/ruvocal/src/lib/components/icons/IconPaperclip.svelte +24 -24
  197. package/src/ruvocal/src/lib/components/icons/IconPro.svelte +37 -37
  198. package/src/ruvocal/src/lib/components/icons/IconShare.svelte +21 -21
  199. package/src/ruvocal/src/lib/components/icons/IconSun.svelte +93 -93
  200. package/src/ruvocal/src/lib/components/icons/Logo.svelte +68 -68
  201. package/src/ruvocal/src/lib/components/icons/LogoHuggingFaceBorderless.svelte +54 -54
  202. package/src/ruvocal/src/lib/components/mcp/AddServerForm.svelte +250 -250
  203. package/src/ruvocal/src/lib/components/mcp/MCPServerManager.svelte +185 -185
  204. package/src/ruvocal/src/lib/components/mcp/ServerCard.svelte +203 -203
  205. package/src/ruvocal/src/lib/components/players/AudioPlayer.svelte +82 -82
  206. package/src/ruvocal/src/lib/components/voice/AudioWaveform.svelte +96 -96
  207. package/src/ruvocal/src/lib/components/wasm/GalleryPanel.svelte +357 -357
  208. package/src/ruvocal/src/lib/constants/mcpExamples.ts +114 -114
  209. package/src/ruvocal/src/lib/constants/mime.ts +11 -11
  210. package/src/ruvocal/src/lib/constants/pagination.ts +1 -1
  211. package/src/ruvocal/src/lib/constants/publicSepToken.ts +1 -1
  212. package/src/ruvocal/src/lib/constants/routerExamples.ts +133 -133
  213. package/src/ruvocal/src/lib/constants/rvagentPresets.ts +206 -206
  214. package/src/ruvocal/src/lib/createShareLink.ts +27 -27
  215. package/src/ruvocal/src/lib/jobs/refresh-conversation-stats.ts +297 -297
  216. package/src/ruvocal/src/lib/migrations/lock.ts +56 -56
  217. package/src/ruvocal/src/lib/migrations/migrations.spec.ts +74 -74
  218. package/src/ruvocal/src/lib/migrations/migrations.ts +109 -109
  219. package/src/ruvocal/src/lib/migrations/routines/01-update-search-assistants.ts +50 -50
  220. package/src/ruvocal/src/lib/migrations/routines/02-update-assistants-models.ts +48 -48
  221. package/src/ruvocal/src/lib/migrations/routines/04-update-message-updates.ts +151 -151
  222. package/src/ruvocal/src/lib/migrations/routines/05-update-message-files.ts +56 -56
  223. package/src/ruvocal/src/lib/migrations/routines/06-trim-message-updates.ts +56 -56
  224. package/src/ruvocal/src/lib/migrations/routines/08-update-featured-to-review.ts +32 -32
  225. package/src/ruvocal/src/lib/migrations/routines/09-delete-empty-conversations.spec.ts +214 -214
  226. package/src/ruvocal/src/lib/migrations/routines/09-delete-empty-conversations.ts +88 -88
  227. package/src/ruvocal/src/lib/migrations/routines/10-update-reports-assistantid.ts +29 -29
  228. package/src/ruvocal/src/lib/migrations/routines/index.ts +15 -15
  229. package/src/ruvocal/src/lib/server/__tests__/conversation-stop-generating.spec.ts +103 -103
  230. package/src/ruvocal/src/lib/server/abortRegistry.ts +57 -57
  231. package/src/ruvocal/src/lib/server/abortedGenerations.ts +43 -43
  232. package/src/ruvocal/src/lib/server/adminToken.ts +62 -62
  233. package/src/ruvocal/src/lib/server/api/__tests__/conversations-id.spec.ts +296 -296
  234. package/src/ruvocal/src/lib/server/api/__tests__/conversations-message.spec.ts +216 -216
  235. package/src/ruvocal/src/lib/server/api/__tests__/conversations.spec.ts +235 -235
  236. package/src/ruvocal/src/lib/server/api/__tests__/misc.spec.ts +72 -72
  237. package/src/ruvocal/src/lib/server/api/__tests__/testHelpers.ts +86 -86
  238. package/src/ruvocal/src/lib/server/api/__tests__/user-reports.spec.ts +78 -78
  239. package/src/ruvocal/src/lib/server/api/__tests__/user.spec.ts +239 -239
  240. package/src/ruvocal/src/lib/server/api/types.ts +37 -37
  241. package/src/ruvocal/src/lib/server/api/utils/requireAuth.ts +22 -22
  242. package/src/ruvocal/src/lib/server/api/utils/resolveConversation.ts +69 -69
  243. package/src/ruvocal/src/lib/server/api/utils/resolveModel.ts +27 -27
  244. package/src/ruvocal/src/lib/server/api/utils/superjsonResponse.ts +15 -15
  245. package/src/ruvocal/src/lib/server/apiToken.ts +11 -11
  246. package/src/ruvocal/src/lib/server/auth.ts +554 -554
  247. package/src/ruvocal/src/lib/server/config.ts +187 -187
  248. package/src/ruvocal/src/lib/server/conversation.ts +83 -83
  249. package/src/ruvocal/src/lib/server/database/__tests__/rvf.spec.ts +709 -709
  250. package/src/ruvocal/src/lib/server/database/postgres.ts +700 -700
  251. package/src/ruvocal/src/lib/server/database/rvf.ts +1078 -1078
  252. package/src/ruvocal/src/lib/server/database.ts +145 -145
  253. package/src/ruvocal/src/lib/server/endpoints/document.ts +68 -68
  254. package/src/ruvocal/src/lib/server/endpoints/endpoints.ts +43 -43
  255. package/src/ruvocal/src/lib/server/endpoints/images.ts +211 -211
  256. package/src/ruvocal/src/lib/server/endpoints/openai/endpointOai.ts +266 -266
  257. package/src/ruvocal/src/lib/server/endpoints/openai/openAIChatToTextGenerationStream.ts +212 -212
  258. package/src/ruvocal/src/lib/server/endpoints/openai/openAICompletionToTextGenerationStream.ts +32 -32
  259. package/src/ruvocal/src/lib/server/endpoints/preprocessMessages.ts +61 -61
  260. package/src/ruvocal/src/lib/server/exitHandler.ts +59 -59
  261. package/src/ruvocal/src/lib/server/files/downloadFile.ts +34 -34
  262. package/src/ruvocal/src/lib/server/files/uploadFile.ts +29 -29
  263. package/src/ruvocal/src/lib/server/findRepoRoot.ts +13 -13
  264. package/src/ruvocal/src/lib/server/generateFromDefaultEndpoint.ts +46 -46
  265. package/src/ruvocal/src/lib/server/hooks/error.ts +37 -37
  266. package/src/ruvocal/src/lib/server/hooks/fetch.ts +22 -22
  267. package/src/ruvocal/src/lib/server/hooks/handle.ts +250 -250
  268. package/src/ruvocal/src/lib/server/hooks/init.ts +51 -51
  269. package/src/ruvocal/src/lib/server/isURLLocal.spec.ts +31 -31
  270. package/src/ruvocal/src/lib/server/isURLLocal.ts +74 -74
  271. package/src/ruvocal/src/lib/server/logger.ts +42 -42
  272. package/src/ruvocal/src/lib/server/mcp/clientPool.spec.ts +175 -175
  273. package/src/ruvocal/src/lib/server/mcp/hf.ts +32 -32
  274. package/src/ruvocal/src/lib/server/mcp/httpClient.ts +122 -122
  275. package/src/ruvocal/src/lib/server/mcp/registry.ts +76 -76
  276. package/src/ruvocal/src/lib/server/mcp/tools.ts +196 -196
  277. package/src/ruvocal/src/lib/server/metrics.ts +255 -255
  278. package/src/ruvocal/src/lib/server/models.ts +518 -518
  279. package/src/ruvocal/src/lib/server/requestContext.ts +55 -55
  280. package/src/ruvocal/src/lib/server/router/arch.ts +230 -230
  281. package/src/ruvocal/src/lib/server/router/endpoint.ts +316 -316
  282. package/src/ruvocal/src/lib/server/router/multimodal.ts +28 -28
  283. package/src/ruvocal/src/lib/server/router/policy.ts +49 -49
  284. package/src/ruvocal/src/lib/server/router/toolsRoute.ts +51 -51
  285. package/src/ruvocal/src/lib/server/router/types.ts +21 -21
  286. package/src/ruvocal/src/lib/server/sendSlack.ts +23 -23
  287. package/src/ruvocal/src/lib/server/textGeneration/generate.ts +258 -258
  288. package/src/ruvocal/src/lib/server/textGeneration/index.ts +96 -96
  289. package/src/ruvocal/src/lib/server/textGeneration/mcp/fileRefs.ts +155 -155
  290. package/src/ruvocal/src/lib/server/textGeneration/mcp/routerResolution.ts +108 -108
  291. package/src/ruvocal/src/lib/server/textGeneration/mcp/runMcpFlow.ts +831 -831
  292. package/src/ruvocal/src/lib/server/textGeneration/mcp/toolInvocation.ts +349 -349
  293. package/src/ruvocal/src/lib/server/textGeneration/mcp/wasmTools.test.ts +633 -633
  294. package/src/ruvocal/src/lib/server/textGeneration/reasoning.ts +23 -23
  295. package/src/ruvocal/src/lib/server/textGeneration/title.ts +83 -83
  296. package/src/ruvocal/src/lib/server/textGeneration/types.ts +28 -28
  297. package/src/ruvocal/src/lib/server/textGeneration/utils/prepareFiles.ts +88 -88
  298. package/src/ruvocal/src/lib/server/textGeneration/utils/routing.ts +21 -21
  299. package/src/ruvocal/src/lib/server/textGeneration/utils/toolPrompt.ts +49 -49
  300. package/src/ruvocal/src/lib/server/urlSafety.ts +77 -77
  301. package/src/ruvocal/src/lib/server/usageLimits.ts +30 -30
  302. package/src/ruvocal/src/lib/stores/autopilotStore.svelte.ts +175 -175
  303. package/src/ruvocal/src/lib/stores/backgroundGenerations.svelte.ts +32 -32
  304. package/src/ruvocal/src/lib/stores/backgroundGenerations.ts +1 -1
  305. package/src/ruvocal/src/lib/stores/errors.ts +9 -9
  306. package/src/ruvocal/src/lib/stores/isAborted.ts +3 -3
  307. package/src/ruvocal/src/lib/stores/isPro.ts +4 -4
  308. package/src/ruvocal/src/lib/stores/loading.ts +3 -3
  309. package/src/ruvocal/src/lib/stores/mcpServers.ts +534 -534
  310. package/src/ruvocal/src/lib/stores/pendingChatInput.ts +3 -3
  311. package/src/ruvocal/src/lib/stores/pendingMessage.ts +9 -9
  312. package/src/ruvocal/src/lib/stores/settings.ts +182 -182
  313. package/src/ruvocal/src/lib/stores/shareModal.ts +13 -13
  314. package/src/ruvocal/src/lib/stores/titleUpdate.ts +8 -8
  315. package/src/ruvocal/src/lib/stores/wasmMcp.ts +472 -472
  316. package/src/ruvocal/src/lib/switchTheme.ts +124 -124
  317. package/src/ruvocal/src/lib/types/AbortedGeneration.ts +8 -8
  318. package/src/ruvocal/src/lib/types/Assistant.ts +31 -31
  319. package/src/ruvocal/src/lib/types/AssistantStats.ts +11 -11
  320. package/src/ruvocal/src/lib/types/ConfigKey.ts +4 -4
  321. package/src/ruvocal/src/lib/types/ConvSidebar.ts +9 -9
  322. package/src/ruvocal/src/lib/types/Conversation.ts +27 -27
  323. package/src/ruvocal/src/lib/types/ConversationStats.ts +13 -13
  324. package/src/ruvocal/src/lib/types/Message.ts +41 -41
  325. package/src/ruvocal/src/lib/types/MessageEvent.ts +10 -10
  326. package/src/ruvocal/src/lib/types/MessageUpdate.ts +139 -139
  327. package/src/ruvocal/src/lib/types/MigrationResult.ts +7 -7
  328. package/src/ruvocal/src/lib/types/Model.ts +23 -23
  329. package/src/ruvocal/src/lib/types/Report.ts +12 -12
  330. package/src/ruvocal/src/lib/types/Review.ts +6 -6
  331. package/src/ruvocal/src/lib/types/Semaphore.ts +19 -19
  332. package/src/ruvocal/src/lib/types/Session.ts +22 -22
  333. package/src/ruvocal/src/lib/types/Settings.ts +93 -93
  334. package/src/ruvocal/src/lib/types/SharedConversation.ts +9 -9
  335. package/src/ruvocal/src/lib/types/Template.ts +6 -6
  336. package/src/ruvocal/src/lib/types/Timestamps.ts +4 -4
  337. package/src/ruvocal/src/lib/types/TokenCache.ts +6 -6
  338. package/src/ruvocal/src/lib/types/Tool.ts +77 -77
  339. package/src/ruvocal/src/lib/types/UrlDependency.ts +5 -5
  340. package/src/ruvocal/src/lib/types/User.ts +14 -14
  341. package/src/ruvocal/src/lib/utils/PublicConfig.svelte.ts +75 -75
  342. package/src/ruvocal/src/lib/utils/auth.ts +17 -17
  343. package/src/ruvocal/src/lib/utils/chunk.ts +33 -33
  344. package/src/ruvocal/src/lib/utils/cookiesAreEnabled.ts +13 -13
  345. package/src/ruvocal/src/lib/utils/debounce.ts +17 -17
  346. package/src/ruvocal/src/lib/utils/deepestChild.ts +6 -6
  347. package/src/ruvocal/src/lib/utils/favicon.ts +21 -21
  348. package/src/ruvocal/src/lib/utils/fetchJSON.ts +23 -23
  349. package/src/ruvocal/src/lib/utils/file2base64.ts +14 -14
  350. package/src/ruvocal/src/lib/utils/formatUserCount.ts +37 -37
  351. package/src/ruvocal/src/lib/utils/generationState.spec.ts +75 -75
  352. package/src/ruvocal/src/lib/utils/generationState.ts +26 -26
  353. package/src/ruvocal/src/lib/utils/getHref.ts +41 -41
  354. package/src/ruvocal/src/lib/utils/getReturnFromGenerator.ts +7 -7
  355. package/src/ruvocal/src/lib/utils/haptics.ts +64 -64
  356. package/src/ruvocal/src/lib/utils/hashConv.ts +12 -12
  357. package/src/ruvocal/src/lib/utils/hf.ts +17 -17
  358. package/src/ruvocal/src/lib/utils/isDesktop.ts +7 -7
  359. package/src/ruvocal/src/lib/utils/isUrl.ts +8 -8
  360. package/src/ruvocal/src/lib/utils/isVirtualKeyboard.ts +16 -16
  361. package/src/ruvocal/src/lib/utils/loadAttachmentsFromUrls.ts +115 -115
  362. package/src/ruvocal/src/lib/utils/marked.spec.ts +96 -96
  363. package/src/ruvocal/src/lib/utils/marked.ts +531 -531
  364. package/src/ruvocal/src/lib/utils/mcpValidation.ts +147 -147
  365. package/src/ruvocal/src/lib/utils/mergeAsyncGenerators.ts +38 -38
  366. package/src/ruvocal/src/lib/utils/messageUpdates.spec.ts +262 -262
  367. package/src/ruvocal/src/lib/utils/messageUpdates.ts +324 -324
  368. package/src/ruvocal/src/lib/utils/mime.ts +56 -56
  369. package/src/ruvocal/src/lib/utils/models.ts +14 -14
  370. package/src/ruvocal/src/lib/utils/parseBlocks.ts +120 -120
  371. package/src/ruvocal/src/lib/utils/parseIncompleteMarkdown.ts +644 -644
  372. package/src/ruvocal/src/lib/utils/parseStringToList.ts +10 -10
  373. package/src/ruvocal/src/lib/utils/randomUuid.ts +14 -14
  374. package/src/ruvocal/src/lib/utils/searchTokens.ts +33 -33
  375. package/src/ruvocal/src/lib/utils/sha256.ts +7 -7
  376. package/src/ruvocal/src/lib/utils/stringifyError.ts +12 -12
  377. package/src/ruvocal/src/lib/utils/sum.ts +3 -3
  378. package/src/ruvocal/src/lib/utils/template.spec.ts +59 -59
  379. package/src/ruvocal/src/lib/utils/template.ts +53 -53
  380. package/src/ruvocal/src/lib/utils/timeout.ts +9 -9
  381. package/src/ruvocal/src/lib/utils/toolProgress.spec.ts +46 -46
  382. package/src/ruvocal/src/lib/utils/toolProgress.ts +11 -11
  383. package/src/ruvocal/src/lib/utils/tree/addChildren.spec.ts +102 -102
  384. package/src/ruvocal/src/lib/utils/tree/addChildren.ts +48 -48
  385. package/src/ruvocal/src/lib/utils/tree/addSibling.spec.ts +81 -81
  386. package/src/ruvocal/src/lib/utils/tree/addSibling.ts +41 -41
  387. package/src/ruvocal/src/lib/utils/tree/buildSubtree.spec.ts +110 -110
  388. package/src/ruvocal/src/lib/utils/tree/buildSubtree.ts +24 -24
  389. package/src/ruvocal/src/lib/utils/tree/convertLegacyConversation.spec.ts +31 -31
  390. package/src/ruvocal/src/lib/utils/tree/convertLegacyConversation.ts +36 -36
  391. package/src/ruvocal/src/lib/utils/tree/isMessageId.spec.ts +15 -15
  392. package/src/ruvocal/src/lib/utils/tree/isMessageId.ts +5 -5
  393. package/src/ruvocal/src/lib/utils/tree/tree.d.ts +14 -14
  394. package/src/ruvocal/src/lib/utils/tree/treeHelpers.spec.ts +167 -167
  395. package/src/ruvocal/src/lib/utils/updates.ts +39 -39
  396. package/src/ruvocal/src/lib/utils/urlParams.ts +13 -13
  397. package/src/ruvocal/src/lib/wasm/idb.ts +438 -438
  398. package/src/ruvocal/src/lib/wasm/index.ts +1213 -1213
  399. package/src/ruvocal/src/lib/wasm/tests/wasm-capabilities.test.ts +565 -565
  400. package/src/ruvocal/src/lib/wasm/wasm.worker.ts +332 -332
  401. package/src/ruvocal/src/lib/wasm/workerClient.ts +166 -166
  402. package/src/ruvocal/src/lib/workers/autopilotWorker.ts +221 -221
  403. package/src/ruvocal/src/lib/workers/detailFetchWorker.ts +100 -100
  404. package/src/ruvocal/src/lib/workers/markdownWorker.ts +61 -61
  405. package/src/ruvocal/src/routes/+error.svelte +20 -20
  406. package/src/ruvocal/src/routes/+layout.svelte +324 -324
  407. package/src/ruvocal/src/routes/+layout.ts +91 -91
  408. package/src/ruvocal/src/routes/+page.svelte +168 -168
  409. package/src/ruvocal/src/routes/.well-known/oauth-cimd/+server.ts +37 -37
  410. package/src/ruvocal/src/routes/__debug/openai/+server.ts +21 -21
  411. package/src/ruvocal/src/routes/admin/export/+server.ts +159 -159
  412. package/src/ruvocal/src/routes/admin/stats/compute/+server.ts +16 -16
  413. package/src/ruvocal/src/routes/api/conversation/[id]/+server.ts +40 -40
  414. package/src/ruvocal/src/routes/api/conversation/[id]/message/[messageId]/+server.ts +42 -42
  415. package/src/ruvocal/src/routes/api/conversations/+server.ts +48 -48
  416. package/src/ruvocal/src/routes/api/fetch-url/+server.ts +147 -147
  417. package/src/ruvocal/src/routes/api/mcp/health/+server.ts +292 -292
  418. package/src/ruvocal/src/routes/api/mcp/servers/+server.ts +32 -32
  419. package/src/ruvocal/src/routes/api/models/+server.ts +25 -25
  420. package/src/ruvocal/src/routes/api/transcribe/+server.ts +104 -104
  421. package/src/ruvocal/src/routes/api/user/+server.ts +15 -15
  422. package/src/ruvocal/src/routes/api/user/validate-token/+server.ts +20 -20
  423. package/src/ruvocal/src/routes/api/v2/conversations/+server.ts +48 -48
  424. package/src/ruvocal/src/routes/api/v2/conversations/[id]/+server.ts +94 -94
  425. package/src/ruvocal/src/routes/api/v2/conversations/[id]/message/[messageId]/+server.ts +43 -43
  426. package/src/ruvocal/src/routes/api/v2/conversations/import-share/+server.ts +23 -23
  427. package/src/ruvocal/src/routes/api/v2/debug/config/+server.ts +16 -16
  428. package/src/ruvocal/src/routes/api/v2/debug/refresh/+server.ts +30 -30
  429. package/src/ruvocal/src/routes/api/v2/export/+server.ts +196 -196
  430. package/src/ruvocal/src/routes/api/v2/feature-flags/+server.ts +14 -14
  431. package/src/ruvocal/src/routes/api/v2/models/+server.ts +38 -38
  432. package/src/ruvocal/src/routes/api/v2/models/[namespace]/+server.ts +8 -8
  433. package/src/ruvocal/src/routes/api/v2/models/[namespace]/[model]/+server.ts +8 -8
  434. package/src/ruvocal/src/routes/api/v2/models/[namespace]/[model]/subscribe/+server.ts +28 -28
  435. package/src/ruvocal/src/routes/api/v2/models/[namespace]/subscribe/+server.ts +28 -28
  436. package/src/ruvocal/src/routes/api/v2/models/old/+server.ts +7 -7
  437. package/src/ruvocal/src/routes/api/v2/models/refresh/+server.ts +33 -33
  438. package/src/ruvocal/src/routes/api/v2/public-config/+server.ts +7 -7
  439. package/src/ruvocal/src/routes/api/v2/user/+server.ts +17 -17
  440. package/src/ruvocal/src/routes/api/v2/user/billing-orgs/+server.ts +73 -73
  441. package/src/ruvocal/src/routes/api/v2/user/reports/+server.ts +17 -17
  442. package/src/ruvocal/src/routes/api/v2/user/settings/+server.ts +110 -110
  443. package/src/ruvocal/src/routes/conversation/+server.ts +115 -115
  444. package/src/ruvocal/src/routes/conversation/[id]/+page.svelte +586 -586
  445. package/src/ruvocal/src/routes/conversation/[id]/+page.ts +60 -60
  446. package/src/ruvocal/src/routes/conversation/[id]/+server.ts +740 -740
  447. package/src/ruvocal/src/routes/conversation/[id]/message/[messageId]/prompt/+server.ts +66 -66
  448. package/src/ruvocal/src/routes/conversation/[id]/share/+server.ts +69 -69
  449. package/src/ruvocal/src/routes/conversation/[id]/stop-generating/+server.ts +35 -35
  450. package/src/ruvocal/src/routes/healthcheck/+server.ts +3 -3
  451. package/src/ruvocal/src/routes/login/+server.ts +5 -5
  452. package/src/ruvocal/src/routes/login/callback/+server.ts +103 -103
  453. package/src/ruvocal/src/routes/login/callback/updateUser.spec.ts +157 -157
  454. package/src/ruvocal/src/routes/login/callback/updateUser.ts +215 -215
  455. package/src/ruvocal/src/routes/logout/+server.ts +18 -18
  456. package/src/ruvocal/src/routes/metrics/+server.ts +18 -18
  457. package/src/ruvocal/src/routes/models/+page.svelte +233 -233
  458. package/src/ruvocal/src/routes/models/[...model]/+page.svelte +161 -161
  459. package/src/ruvocal/src/routes/models/[...model]/+page.ts +14 -14
  460. package/src/ruvocal/src/routes/models/[...model]/thumbnail.png/+server.ts +64 -64
  461. package/src/ruvocal/src/routes/models/[...model]/thumbnail.png/ModelThumbnail.svelte +28 -28
  462. package/src/ruvocal/src/routes/privacy/+page.svelte +11 -11
  463. package/src/ruvocal/src/routes/r/[id]/+page.ts +34 -34
  464. package/src/ruvocal/src/routes/settings/(nav)/+layout.svelte +282 -282
  465. package/src/ruvocal/src/routes/settings/(nav)/+layout.ts +1 -1
  466. package/src/ruvocal/src/routes/settings/(nav)/+server.ts +59 -59
  467. package/src/ruvocal/src/routes/settings/(nav)/[...model]/+page.svelte +464 -464
  468. package/src/ruvocal/src/routes/settings/(nav)/[...model]/+page.ts +14 -14
  469. package/src/ruvocal/src/routes/settings/(nav)/application/+page.svelte +362 -362
  470. package/src/ruvocal/src/routes/settings/+layout.svelte +40 -40
  471. package/src/ruvocal/src/styles/highlight-js.css +195 -195
  472. package/src/ruvocal/src/styles/main.css +144 -144
  473. package/src/ruvocal/static/chatui/favicon-dark.svg +3 -3
  474. package/src/ruvocal/static/chatui/favicon-dev.svg +3 -3
  475. package/src/ruvocal/static/chatui/favicon.svg +3 -3
  476. package/src/ruvocal/static/chatui/icon.svg +3 -3
  477. package/src/ruvocal/static/chatui/logo.svg +7 -7
  478. package/src/ruvocal/static/chatui/manifest.json +54 -54
  479. package/src/ruvocal/static/chatui/welcome.js +184 -184
  480. package/src/ruvocal/static/huggingchat/favicon-dark.svg +4 -4
  481. package/src/ruvocal/static/huggingchat/favicon-dev.svg +4 -4
  482. package/src/ruvocal/static/huggingchat/favicon.svg +4 -4
  483. package/src/ruvocal/static/huggingchat/fulltext-logo.svg +1 -1
  484. package/src/ruvocal/static/huggingchat/icon.svg +4 -4
  485. package/src/ruvocal/static/huggingchat/logo.svg +4 -4
  486. package/src/ruvocal/static/huggingchat/manifest.json +54 -54
  487. package/src/ruvocal/static/huggingchat/routes.chat.json +226 -226
  488. package/src/ruvocal/static/robots.txt +10 -10
  489. package/src/ruvocal/static/wasm/rvagent_wasm.js +1539 -1539
  490. package/src/ruvocal/stub/@reflink/reflink/package.json +5 -5
  491. package/src/ruvocal/svelte.config.js +53 -53
  492. package/src/ruvocal/tailwind.config.cjs +30 -30
  493. package/src/ruvocal/tsconfig.json +19 -19
  494. package/src/ruvocal/vite.config.ts +87 -87
  495. package/src/scripts/deploy.sh +116 -116
  496. package/src/scripts/generate-config.js +245 -245
  497. package/src/scripts/generate-welcome.js +187 -187
  498. package/src/scripts/package-rvf.sh +116 -116
@@ -1,554 +1,554 @@
1
- import {
2
- Issuer,
3
- type BaseClient,
4
- type UserinfoResponse,
5
- type TokenSet,
6
- custom,
7
- generators,
8
- } from "openid-client";
9
- import type { RequestEvent } from "@sveltejs/kit";
10
- import { addHours, addWeeks, differenceInMinutes, subMinutes } from "date-fns";
11
- import { config } from "$lib/server/config";
12
- import { sha256 } from "$lib/utils/sha256";
13
- import { z } from "zod";
14
- import { dev } from "$app/environment";
15
- import { redirect, type Cookies } from "@sveltejs/kit";
16
- import { collections } from "$lib/server/database";
17
- import JSON5 from "json5";
18
- import { logger } from "$lib/server/logger";
19
- import { ObjectId } from "mongodb";
20
- import { adminTokenManager } from "./adminToken";
21
- import type { User } from "$lib/types/User";
22
- import type { Session } from "$lib/types/Session";
23
- import { base } from "$app/paths";
24
- import { acquireLock, isDBLocked, releaseLock } from "$lib/migrations/lock";
25
- import { Semaphores } from "$lib/types/Semaphore";
26
-
27
- export interface OIDCSettings {
28
- redirectURI: string;
29
- }
30
-
31
- export interface OIDCUserInfo {
32
- token: TokenSet;
33
- userData: UserinfoResponse;
34
- }
35
-
36
- const stringWithDefault = (value: string) =>
37
- z
38
- .string()
39
- .default(value)
40
- .transform((el) => (el ? el : value));
41
-
42
- export const OIDConfig = z
43
- .object({
44
- CLIENT_ID: stringWithDefault(config.OPENID_CLIENT_ID),
45
- CLIENT_SECRET: stringWithDefault(config.OPENID_CLIENT_SECRET),
46
- PROVIDER_URL: stringWithDefault(config.OPENID_PROVIDER_URL),
47
- SCOPES: stringWithDefault(config.OPENID_SCOPES),
48
- NAME_CLAIM: stringWithDefault(config.OPENID_NAME_CLAIM).refine(
49
- (el) => !["preferred_username", "email", "picture", "sub"].includes(el),
50
- { message: "nameClaim cannot be one of the restricted keys." }
51
- ),
52
- TOLERANCE: stringWithDefault(config.OPENID_TOLERANCE),
53
- RESOURCE: stringWithDefault(config.OPENID_RESOURCE),
54
- ID_TOKEN_SIGNED_RESPONSE_ALG: z.string().optional(),
55
- })
56
- .parse(JSON5.parse(config.OPENID_CONFIG || "{}"));
57
-
58
- export const loginEnabled = !!OIDConfig.CLIENT_ID;
59
-
60
- const sameSite = z
61
- .enum(["lax", "none", "strict"])
62
- .default(dev || config.ALLOW_INSECURE_COOKIES === "true" ? "lax" : "none")
63
- .parse(config.COOKIE_SAMESITE === "" ? undefined : config.COOKIE_SAMESITE);
64
-
65
- const secure = z
66
- .boolean()
67
- .default(!(dev || config.ALLOW_INSECURE_COOKIES === "true"))
68
- .parse(config.COOKIE_SECURE === "" ? undefined : config.COOKIE_SECURE === "true");
69
-
70
- function sanitizeReturnPath(path: string | undefined | null): string | undefined {
71
- if (!path) {
72
- return undefined;
73
- }
74
- if (path.startsWith("//")) {
75
- return undefined;
76
- }
77
- if (!path.startsWith("/")) {
78
- return undefined;
79
- }
80
- return path;
81
- }
82
-
83
- export function refreshSessionCookie(cookies: Cookies, sessionId: string) {
84
- cookies.set(config.COOKIE_NAME, sessionId, {
85
- path: "/",
86
- // So that it works inside the space's iframe
87
- sameSite,
88
- secure,
89
- httpOnly: true,
90
- expires: addWeeks(new Date(), 2),
91
- });
92
- }
93
-
94
- export async function findUser(
95
- sessionId: string,
96
- coupledCookieHash: string | undefined,
97
- url: URL
98
- ): Promise<{
99
- user: User | null;
100
- invalidateSession: boolean;
101
- oauth?: Session["oauth"];
102
- }> {
103
- const session = await collections.sessions.findOne({ sessionId });
104
-
105
- if (!session) {
106
- return { user: null, invalidateSession: false };
107
- }
108
-
109
- if (coupledCookieHash && session.coupledCookieHash !== coupledCookieHash) {
110
- return { user: null, invalidateSession: true };
111
- }
112
-
113
- // Check if OAuth token needs refresh
114
- if (session.oauth?.token && session.oauth.refreshToken) {
115
- // If token expires in less than 5 minutes, refresh it
116
- if (differenceInMinutes(session.oauth.token.expiresAt, new Date()) < 5) {
117
- const lockKey = `${Semaphores.OAUTH_TOKEN_REFRESH}:${sessionId}`;
118
-
119
- // Acquire lock for token refresh
120
- const lockId = await acquireLock(lockKey);
121
- if (lockId) {
122
- try {
123
- // Attempt to refresh the token
124
- const newTokenSet = await refreshOAuthToken(
125
- { redirectURI: `${config.PUBLIC_ORIGIN}${base}/login/callback` },
126
- session.oauth.refreshToken,
127
- url
128
- );
129
-
130
- if (!newTokenSet || !newTokenSet.access_token) {
131
- // Token refresh failed, invalidate session
132
- return { user: null, invalidateSession: true };
133
- }
134
-
135
- // Update session with new token information
136
- const updatedOAuth = tokenSetToSessionOauth(newTokenSet);
137
-
138
- if (!updatedOAuth) {
139
- // Token refresh failed, invalidate session
140
- return { user: null, invalidateSession: true };
141
- }
142
-
143
- await collections.sessions.updateOne(
144
- { sessionId },
145
- {
146
- $set: {
147
- oauth: updatedOAuth,
148
- updatedAt: new Date(),
149
- },
150
- }
151
- );
152
-
153
- session.oauth = updatedOAuth;
154
- } catch (err) {
155
- logger.error(err, "Error during token refresh:");
156
- return { user: null, invalidateSession: true };
157
- } finally {
158
- await releaseLock(lockKey, lockId);
159
- }
160
- } else if (new Date() > session.oauth.token.expiresAt) {
161
- // If the token has expired, we need to wait for the token refresh to complete
162
- let attempts = 0;
163
- do {
164
- await new Promise((resolve) => setTimeout(resolve, 200));
165
- attempts++;
166
- if (attempts > 20) {
167
- return { user: null, invalidateSession: true };
168
- }
169
- } while (await isDBLocked(lockKey));
170
-
171
- const updatedSession = await collections.sessions.findOne({ sessionId });
172
- if (!updatedSession || updatedSession.oauth?.token === session.oauth.token) {
173
- return { user: null, invalidateSession: true };
174
- }
175
-
176
- session.oauth = updatedSession.oauth;
177
- }
178
- }
179
- }
180
-
181
- return {
182
- user: await collections.users.findOne({ _id: session.userId }),
183
- invalidateSession: false,
184
- oauth: session.oauth,
185
- };
186
- }
187
- export const authCondition = (locals: App.Locals) => {
188
- if (!locals.user && !locals.sessionId) {
189
- throw new Error("User or sessionId is required");
190
- }
191
-
192
- return locals.user
193
- ? { userId: locals.user._id }
194
- : { sessionId: locals.sessionId, userId: { $exists: false } };
195
- };
196
-
197
- export function tokenSetToSessionOauth(tokenSet: TokenSet): Session["oauth"] {
198
- if (!tokenSet.access_token) {
199
- return undefined;
200
- }
201
-
202
- return {
203
- token: {
204
- value: tokenSet.access_token,
205
- expiresAt: tokenSet.expires_at
206
- ? subMinutes(new Date(tokenSet.expires_at * 1000), 1)
207
- : addWeeks(new Date(), 2),
208
- },
209
- refreshToken: tokenSet.refresh_token || undefined,
210
- };
211
- }
212
-
213
- /**
214
- * Generates a CSRF token using the user sessionId. Note that we don't need a secret because sessionId is enough.
215
- */
216
- export async function generateCsrfToken(
217
- sessionId: string,
218
- redirectUrl: string,
219
- next?: string
220
- ): Promise<string> {
221
- const sanitizedNext = sanitizeReturnPath(next);
222
- const data = {
223
- expiration: addHours(new Date(), 1).getTime(),
224
- redirectUrl,
225
- ...(sanitizedNext ? { next: sanitizedNext } : {}),
226
- } as {
227
- expiration: number;
228
- redirectUrl: string;
229
- next?: string;
230
- };
231
-
232
- return Buffer.from(
233
- JSON.stringify({
234
- data,
235
- signature: await sha256(JSON.stringify(data) + "##" + sessionId),
236
- })
237
- ).toString("base64");
238
- }
239
-
240
- let lastIssuer: Issuer<BaseClient> | null = null;
241
- let lastIssuerFetchedAt: Date | null = null;
242
- async function getOIDCClient(settings: OIDCSettings, url: URL): Promise<BaseClient> {
243
- if (
244
- lastIssuer &&
245
- lastIssuerFetchedAt &&
246
- differenceInMinutes(new Date(), lastIssuerFetchedAt) >= 10
247
- ) {
248
- lastIssuer = null;
249
- lastIssuerFetchedAt = null;
250
- }
251
- if (!lastIssuer) {
252
- lastIssuer = await Issuer.discover(OIDConfig.PROVIDER_URL);
253
- lastIssuerFetchedAt = new Date();
254
- }
255
-
256
- const issuer = lastIssuer;
257
-
258
- const client_config: ConstructorParameters<typeof issuer.Client>[0] = {
259
- client_id: OIDConfig.CLIENT_ID,
260
- client_secret: OIDConfig.CLIENT_SECRET,
261
- redirect_uris: [settings.redirectURI],
262
- response_types: ["code"],
263
- [custom.clock_tolerance]: OIDConfig.TOLERANCE || undefined,
264
- id_token_signed_response_alg: OIDConfig.ID_TOKEN_SIGNED_RESPONSE_ALG || undefined,
265
- };
266
-
267
- if (OIDConfig.CLIENT_ID === "__CIMD__") {
268
- // See https://datatracker.ietf.org/doc/draft-ietf-oauth-client-id-metadata-document/
269
- client_config.client_id = new URL(
270
- `${base}/.well-known/oauth-cimd`,
271
- config.PUBLIC_ORIGIN || url.origin
272
- ).toString();
273
- }
274
-
275
- const alg_supported = issuer.metadata["id_token_signing_alg_values_supported"];
276
-
277
- if (Array.isArray(alg_supported)) {
278
- client_config.id_token_signed_response_alg ??= alg_supported[0];
279
- }
280
-
281
- return new issuer.Client(client_config);
282
- }
283
-
284
- export async function getOIDCAuthorizationUrl(
285
- settings: OIDCSettings,
286
- params: { sessionId: string; next?: string; url: URL; cookies: Cookies }
287
- ): Promise<string> {
288
- const client = await getOIDCClient(settings, params.url);
289
- const csrfToken = await generateCsrfToken(
290
- params.sessionId,
291
- settings.redirectURI,
292
- sanitizeReturnPath(params.next)
293
- );
294
-
295
- const codeVerifier = generators.codeVerifier();
296
- const codeChallenge = generators.codeChallenge(codeVerifier);
297
-
298
- params.cookies.set("hfChat-codeVerifier", codeVerifier, {
299
- path: "/",
300
- sameSite,
301
- secure,
302
- httpOnly: true,
303
- expires: addHours(new Date(), 1),
304
- });
305
-
306
- return client.authorizationUrl({
307
- code_challenge_method: "S256",
308
- code_challenge: codeChallenge,
309
- scope: OIDConfig.SCOPES,
310
- state: csrfToken,
311
- resource: OIDConfig.RESOURCE || undefined,
312
- });
313
- }
314
-
315
- export async function getOIDCUserData(
316
- settings: OIDCSettings,
317
- code: string,
318
- codeVerifier: string,
319
- iss: string | undefined,
320
- url: URL
321
- ): Promise<OIDCUserInfo> {
322
- const client = await getOIDCClient(settings, url);
323
- const token = await client.callback(
324
- settings.redirectURI,
325
- {
326
- code,
327
- iss,
328
- },
329
- { code_verifier: codeVerifier }
330
- );
331
- const userData = await client.userinfo(token);
332
-
333
- return { token, userData };
334
- }
335
-
336
- /**
337
- * Refreshes an OAuth token using the refresh token
338
- */
339
- export async function refreshOAuthToken(
340
- settings: OIDCSettings,
341
- refreshToken: string,
342
- url: URL
343
- ): Promise<TokenSet | null> {
344
- const client = await getOIDCClient(settings, url);
345
- const tokenSet = await client.refresh(refreshToken);
346
- return tokenSet;
347
- }
348
-
349
- export async function validateAndParseCsrfToken(
350
- token: string,
351
- sessionId: string
352
- ): Promise<{
353
- /** This is the redirect url that was passed to the OIDC provider */
354
- redirectUrl: string;
355
- /** Relative path (within this app) to return to after login */
356
- next?: string;
357
- } | null> {
358
- try {
359
- const { data, signature } = z
360
- .object({
361
- data: z.object({
362
- expiration: z.number().int(),
363
- redirectUrl: z.string().url(),
364
- next: z.string().optional(),
365
- }),
366
- signature: z.string().length(64),
367
- })
368
- .parse(JSON.parse(token));
369
-
370
- const reconstructSign = await sha256(JSON.stringify(data) + "##" + sessionId);
371
-
372
- if (data.expiration > Date.now() && signature === reconstructSign) {
373
- return { redirectUrl: data.redirectUrl, next: sanitizeReturnPath(data.next) };
374
- }
375
- } catch (e) {
376
- logger.error(e, "Error validating and parsing CSRF token");
377
- }
378
- return null;
379
- }
380
-
381
- type CookieRecord = Cookies;
382
- type HeaderRecord = Headers;
383
-
384
- export async function getCoupledCookieHash(cookie: CookieRecord): Promise<string | undefined> {
385
- if (!config.COUPLE_SESSION_WITH_COOKIE_NAME) {
386
- return undefined;
387
- }
388
-
389
- const cookieValue = cookie.get(config.COUPLE_SESSION_WITH_COOKIE_NAME);
390
-
391
- if (!cookieValue) {
392
- return "no-cookie";
393
- }
394
-
395
- return await sha256(cookieValue);
396
- }
397
-
398
- export async function authenticateRequest(
399
- headers: HeaderRecord,
400
- cookie: CookieRecord,
401
- url: URL,
402
- isApi?: boolean
403
- ): Promise<App.Locals & { secretSessionId: string }> {
404
- const token = cookie.get(config.COOKIE_NAME);
405
-
406
- let email = null;
407
- if (config.TRUSTED_EMAIL_HEADER) {
408
- email = headers.get(config.TRUSTED_EMAIL_HEADER);
409
- }
410
-
411
- let secretSessionId: string | null = null;
412
- let sessionId: string | null = null;
413
-
414
- if (email) {
415
- secretSessionId = sessionId = await sha256(email);
416
- return {
417
- user: {
418
- _id: new ObjectId(sessionId.slice(0, 24)),
419
- name: email,
420
- email,
421
- createdAt: new Date(),
422
- updatedAt: new Date(),
423
- hfUserId: email,
424
- avatarUrl: "",
425
- },
426
- sessionId,
427
- secretSessionId,
428
- isAdmin: adminTokenManager.isAdmin(sessionId),
429
- };
430
- }
431
-
432
- if (token) {
433
- secretSessionId = token;
434
- sessionId = await sha256(token);
435
-
436
- const result = await findUser(sessionId, await getCoupledCookieHash(cookie), url);
437
-
438
- if (result.invalidateSession) {
439
- secretSessionId = crypto.randomUUID();
440
- sessionId = await sha256(secretSessionId);
441
-
442
- if (await collections.sessions.findOne({ sessionId })) {
443
- throw new Error("Session ID collision");
444
- }
445
- }
446
-
447
- return {
448
- user: result.user ?? undefined,
449
- token: result.oauth?.token?.value,
450
- sessionId,
451
- secretSessionId,
452
- isAdmin: result.user?.isAdmin || adminTokenManager.isAdmin(sessionId),
453
- };
454
- }
455
-
456
- if (isApi) {
457
- const authorization = headers.get("Authorization");
458
- if (authorization?.startsWith("Bearer ")) {
459
- const token = authorization.slice(7);
460
- const hash = await sha256(token);
461
- sessionId = secretSessionId = hash;
462
-
463
- const cacheHit = await collections.tokenCaches.findOne({ tokenHash: hash });
464
- if (cacheHit) {
465
- const user = await collections.users.findOne({ hfUserId: cacheHit.userId });
466
- if (!user) {
467
- throw new Error("User not found");
468
- }
469
- return {
470
- user,
471
- sessionId,
472
- token,
473
- secretSessionId,
474
- isAdmin: user.isAdmin || adminTokenManager.isAdmin(sessionId),
475
- };
476
- }
477
-
478
- const response = await fetch("https://huggingface.co/api/whoami-v2", {
479
- headers: { Authorization: `Bearer ${token}` },
480
- });
481
-
482
- if (!response.ok) {
483
- throw new Error("Unauthorized");
484
- }
485
-
486
- const data = await response.json();
487
- const user = await collections.users.findOne({ hfUserId: data.id });
488
- if (!user) {
489
- throw new Error("User not found");
490
- }
491
-
492
- await collections.tokenCaches.insertOne({
493
- tokenHash: hash,
494
- userId: data.id,
495
- createdAt: new Date(),
496
- updatedAt: new Date(),
497
- });
498
-
499
- return {
500
- user,
501
- sessionId,
502
- secretSessionId,
503
- token,
504
- isAdmin: user.isAdmin || adminTokenManager.isAdmin(sessionId),
505
- };
506
- }
507
- }
508
-
509
- // Generate new session if none exists
510
- secretSessionId = crypto.randomUUID();
511
- sessionId = await sha256(secretSessionId);
512
-
513
- if (await collections.sessions.findOne({ sessionId })) {
514
- throw new Error("Session ID collision");
515
- }
516
-
517
- return { user: undefined, sessionId, secretSessionId, isAdmin: false };
518
- }
519
-
520
- export async function triggerOauthFlow({ url, locals, cookies }: RequestEvent): Promise<Response> {
521
- // const referer = request.headers.get("referer");
522
- // let redirectURI = `${(referer ? new URL(referer) : url).origin}${base}/login/callback`;
523
- let redirectURI = `${url.origin}${base}/login/callback`;
524
-
525
- // TODO: Handle errors if provider is not responding
526
-
527
- if (url.searchParams.has("callback")) {
528
- const callback = url.searchParams.get("callback") || redirectURI;
529
- if (config.ALTERNATIVE_REDIRECT_URLS.includes(callback)) {
530
- redirectURI = callback;
531
- }
532
- }
533
-
534
- // Preserve a safe in-app return path after login.
535
- // Priority: explicit ?next=... (must be an absolute path), else the current path (when auto-login kicks in).
536
- let next: string | undefined = undefined;
537
- const nextParam = sanitizeReturnPath(url.searchParams.get("next"));
538
- if (nextParam) {
539
- // Only accept absolute in-app paths to prevent open redirects
540
- next = nextParam;
541
- } else if (!url.pathname.startsWith(`${base}/login`)) {
542
- // For automatic login on protected pages, return to the page the user was on
543
- next = sanitizeReturnPath(`${url.pathname}${url.search}`) ?? `${base}/`;
544
- } else {
545
- next = sanitizeReturnPath(`${base}/`) ?? "/";
546
- }
547
-
548
- const authorizationUrl = await getOIDCAuthorizationUrl(
549
- { redirectURI },
550
- { sessionId: locals.sessionId, next, url, cookies }
551
- );
552
-
553
- throw redirect(302, authorizationUrl);
554
- }
1
+ import {
2
+ Issuer,
3
+ type BaseClient,
4
+ type UserinfoResponse,
5
+ type TokenSet,
6
+ custom,
7
+ generators,
8
+ } from "openid-client";
9
+ import type { RequestEvent } from "@sveltejs/kit";
10
+ import { addHours, addWeeks, differenceInMinutes, subMinutes } from "date-fns";
11
+ import { config } from "$lib/server/config";
12
+ import { sha256 } from "$lib/utils/sha256";
13
+ import { z } from "zod";
14
+ import { dev } from "$app/environment";
15
+ import { redirect, type Cookies } from "@sveltejs/kit";
16
+ import { collections } from "$lib/server/database";
17
+ import JSON5 from "json5";
18
+ import { logger } from "$lib/server/logger";
19
+ import { ObjectId } from "mongodb";
20
+ import { adminTokenManager } from "./adminToken";
21
+ import type { User } from "$lib/types/User";
22
+ import type { Session } from "$lib/types/Session";
23
+ import { base } from "$app/paths";
24
+ import { acquireLock, isDBLocked, releaseLock } from "$lib/migrations/lock";
25
+ import { Semaphores } from "$lib/types/Semaphore";
26
+
27
+ export interface OIDCSettings {
28
+ redirectURI: string;
29
+ }
30
+
31
+ export interface OIDCUserInfo {
32
+ token: TokenSet;
33
+ userData: UserinfoResponse;
34
+ }
35
+
36
+ const stringWithDefault = (value: string) =>
37
+ z
38
+ .string()
39
+ .default(value)
40
+ .transform((el) => (el ? el : value));
41
+
42
+ export const OIDConfig = z
43
+ .object({
44
+ CLIENT_ID: stringWithDefault(config.OPENID_CLIENT_ID),
45
+ CLIENT_SECRET: stringWithDefault(config.OPENID_CLIENT_SECRET),
46
+ PROVIDER_URL: stringWithDefault(config.OPENID_PROVIDER_URL),
47
+ SCOPES: stringWithDefault(config.OPENID_SCOPES),
48
+ NAME_CLAIM: stringWithDefault(config.OPENID_NAME_CLAIM).refine(
49
+ (el) => !["preferred_username", "email", "picture", "sub"].includes(el),
50
+ { message: "nameClaim cannot be one of the restricted keys." }
51
+ ),
52
+ TOLERANCE: stringWithDefault(config.OPENID_TOLERANCE),
53
+ RESOURCE: stringWithDefault(config.OPENID_RESOURCE),
54
+ ID_TOKEN_SIGNED_RESPONSE_ALG: z.string().optional(),
55
+ })
56
+ .parse(JSON5.parse(config.OPENID_CONFIG || "{}"));
57
+
58
+ export const loginEnabled = !!OIDConfig.CLIENT_ID;
59
+
60
+ const sameSite = z
61
+ .enum(["lax", "none", "strict"])
62
+ .default(dev || config.ALLOW_INSECURE_COOKIES === "true" ? "lax" : "none")
63
+ .parse(config.COOKIE_SAMESITE === "" ? undefined : config.COOKIE_SAMESITE);
64
+
65
+ const secure = z
66
+ .boolean()
67
+ .default(!(dev || config.ALLOW_INSECURE_COOKIES === "true"))
68
+ .parse(config.COOKIE_SECURE === "" ? undefined : config.COOKIE_SECURE === "true");
69
+
70
+ function sanitizeReturnPath(path: string | undefined | null): string | undefined {
71
+ if (!path) {
72
+ return undefined;
73
+ }
74
+ if (path.startsWith("//")) {
75
+ return undefined;
76
+ }
77
+ if (!path.startsWith("/")) {
78
+ return undefined;
79
+ }
80
+ return path;
81
+ }
82
+
83
+ export function refreshSessionCookie(cookies: Cookies, sessionId: string) {
84
+ cookies.set(config.COOKIE_NAME, sessionId, {
85
+ path: "/",
86
+ // So that it works inside the space's iframe
87
+ sameSite,
88
+ secure,
89
+ httpOnly: true,
90
+ expires: addWeeks(new Date(), 2),
91
+ });
92
+ }
93
+
94
+ export async function findUser(
95
+ sessionId: string,
96
+ coupledCookieHash: string | undefined,
97
+ url: URL
98
+ ): Promise<{
99
+ user: User | null;
100
+ invalidateSession: boolean;
101
+ oauth?: Session["oauth"];
102
+ }> {
103
+ const session = await collections.sessions.findOne({ sessionId });
104
+
105
+ if (!session) {
106
+ return { user: null, invalidateSession: false };
107
+ }
108
+
109
+ if (coupledCookieHash && session.coupledCookieHash !== coupledCookieHash) {
110
+ return { user: null, invalidateSession: true };
111
+ }
112
+
113
+ // Check if OAuth token needs refresh
114
+ if (session.oauth?.token && session.oauth.refreshToken) {
115
+ // If token expires in less than 5 minutes, refresh it
116
+ if (differenceInMinutes(session.oauth.token.expiresAt, new Date()) < 5) {
117
+ const lockKey = `${Semaphores.OAUTH_TOKEN_REFRESH}:${sessionId}`;
118
+
119
+ // Acquire lock for token refresh
120
+ const lockId = await acquireLock(lockKey);
121
+ if (lockId) {
122
+ try {
123
+ // Attempt to refresh the token
124
+ const newTokenSet = await refreshOAuthToken(
125
+ { redirectURI: `${config.PUBLIC_ORIGIN}${base}/login/callback` },
126
+ session.oauth.refreshToken,
127
+ url
128
+ );
129
+
130
+ if (!newTokenSet || !newTokenSet.access_token) {
131
+ // Token refresh failed, invalidate session
132
+ return { user: null, invalidateSession: true };
133
+ }
134
+
135
+ // Update session with new token information
136
+ const updatedOAuth = tokenSetToSessionOauth(newTokenSet);
137
+
138
+ if (!updatedOAuth) {
139
+ // Token refresh failed, invalidate session
140
+ return { user: null, invalidateSession: true };
141
+ }
142
+
143
+ await collections.sessions.updateOne(
144
+ { sessionId },
145
+ {
146
+ $set: {
147
+ oauth: updatedOAuth,
148
+ updatedAt: new Date(),
149
+ },
150
+ }
151
+ );
152
+
153
+ session.oauth = updatedOAuth;
154
+ } catch (err) {
155
+ logger.error(err, "Error during token refresh:");
156
+ return { user: null, invalidateSession: true };
157
+ } finally {
158
+ await releaseLock(lockKey, lockId);
159
+ }
160
+ } else if (new Date() > session.oauth.token.expiresAt) {
161
+ // If the token has expired, we need to wait for the token refresh to complete
162
+ let attempts = 0;
163
+ do {
164
+ await new Promise((resolve) => setTimeout(resolve, 200));
165
+ attempts++;
166
+ if (attempts > 20) {
167
+ return { user: null, invalidateSession: true };
168
+ }
169
+ } while (await isDBLocked(lockKey));
170
+
171
+ const updatedSession = await collections.sessions.findOne({ sessionId });
172
+ if (!updatedSession || updatedSession.oauth?.token === session.oauth.token) {
173
+ return { user: null, invalidateSession: true };
174
+ }
175
+
176
+ session.oauth = updatedSession.oauth;
177
+ }
178
+ }
179
+ }
180
+
181
+ return {
182
+ user: await collections.users.findOne({ _id: session.userId }),
183
+ invalidateSession: false,
184
+ oauth: session.oauth,
185
+ };
186
+ }
187
+ export const authCondition = (locals: App.Locals) => {
188
+ if (!locals.user && !locals.sessionId) {
189
+ throw new Error("User or sessionId is required");
190
+ }
191
+
192
+ return locals.user
193
+ ? { userId: locals.user._id }
194
+ : { sessionId: locals.sessionId, userId: { $exists: false } };
195
+ };
196
+
197
+ export function tokenSetToSessionOauth(tokenSet: TokenSet): Session["oauth"] {
198
+ if (!tokenSet.access_token) {
199
+ return undefined;
200
+ }
201
+
202
+ return {
203
+ token: {
204
+ value: tokenSet.access_token,
205
+ expiresAt: tokenSet.expires_at
206
+ ? subMinutes(new Date(tokenSet.expires_at * 1000), 1)
207
+ : addWeeks(new Date(), 2),
208
+ },
209
+ refreshToken: tokenSet.refresh_token || undefined,
210
+ };
211
+ }
212
+
213
+ /**
214
+ * Generates a CSRF token using the user sessionId. Note that we don't need a secret because sessionId is enough.
215
+ */
216
+ export async function generateCsrfToken(
217
+ sessionId: string,
218
+ redirectUrl: string,
219
+ next?: string
220
+ ): Promise<string> {
221
+ const sanitizedNext = sanitizeReturnPath(next);
222
+ const data = {
223
+ expiration: addHours(new Date(), 1).getTime(),
224
+ redirectUrl,
225
+ ...(sanitizedNext ? { next: sanitizedNext } : {}),
226
+ } as {
227
+ expiration: number;
228
+ redirectUrl: string;
229
+ next?: string;
230
+ };
231
+
232
+ return Buffer.from(
233
+ JSON.stringify({
234
+ data,
235
+ signature: await sha256(JSON.stringify(data) + "##" + sessionId),
236
+ })
237
+ ).toString("base64");
238
+ }
239
+
240
+ let lastIssuer: Issuer<BaseClient> | null = null;
241
+ let lastIssuerFetchedAt: Date | null = null;
242
+ async function getOIDCClient(settings: OIDCSettings, url: URL): Promise<BaseClient> {
243
+ if (
244
+ lastIssuer &&
245
+ lastIssuerFetchedAt &&
246
+ differenceInMinutes(new Date(), lastIssuerFetchedAt) >= 10
247
+ ) {
248
+ lastIssuer = null;
249
+ lastIssuerFetchedAt = null;
250
+ }
251
+ if (!lastIssuer) {
252
+ lastIssuer = await Issuer.discover(OIDConfig.PROVIDER_URL);
253
+ lastIssuerFetchedAt = new Date();
254
+ }
255
+
256
+ const issuer = lastIssuer;
257
+
258
+ const client_config: ConstructorParameters<typeof issuer.Client>[0] = {
259
+ client_id: OIDConfig.CLIENT_ID,
260
+ client_secret: OIDConfig.CLIENT_SECRET,
261
+ redirect_uris: [settings.redirectURI],
262
+ response_types: ["code"],
263
+ [custom.clock_tolerance]: OIDConfig.TOLERANCE || undefined,
264
+ id_token_signed_response_alg: OIDConfig.ID_TOKEN_SIGNED_RESPONSE_ALG || undefined,
265
+ };
266
+
267
+ if (OIDConfig.CLIENT_ID === "__CIMD__") {
268
+ // See https://datatracker.ietf.org/doc/draft-ietf-oauth-client-id-metadata-document/
269
+ client_config.client_id = new URL(
270
+ `${base}/.well-known/oauth-cimd`,
271
+ config.PUBLIC_ORIGIN || url.origin
272
+ ).toString();
273
+ }
274
+
275
+ const alg_supported = issuer.metadata["id_token_signing_alg_values_supported"];
276
+
277
+ if (Array.isArray(alg_supported)) {
278
+ client_config.id_token_signed_response_alg ??= alg_supported[0];
279
+ }
280
+
281
+ return new issuer.Client(client_config);
282
+ }
283
+
284
+ export async function getOIDCAuthorizationUrl(
285
+ settings: OIDCSettings,
286
+ params: { sessionId: string; next?: string; url: URL; cookies: Cookies }
287
+ ): Promise<string> {
288
+ const client = await getOIDCClient(settings, params.url);
289
+ const csrfToken = await generateCsrfToken(
290
+ params.sessionId,
291
+ settings.redirectURI,
292
+ sanitizeReturnPath(params.next)
293
+ );
294
+
295
+ const codeVerifier = generators.codeVerifier();
296
+ const codeChallenge = generators.codeChallenge(codeVerifier);
297
+
298
+ params.cookies.set("hfChat-codeVerifier", codeVerifier, {
299
+ path: "/",
300
+ sameSite,
301
+ secure,
302
+ httpOnly: true,
303
+ expires: addHours(new Date(), 1),
304
+ });
305
+
306
+ return client.authorizationUrl({
307
+ code_challenge_method: "S256",
308
+ code_challenge: codeChallenge,
309
+ scope: OIDConfig.SCOPES,
310
+ state: csrfToken,
311
+ resource: OIDConfig.RESOURCE || undefined,
312
+ });
313
+ }
314
+
315
+ export async function getOIDCUserData(
316
+ settings: OIDCSettings,
317
+ code: string,
318
+ codeVerifier: string,
319
+ iss: string | undefined,
320
+ url: URL
321
+ ): Promise<OIDCUserInfo> {
322
+ const client = await getOIDCClient(settings, url);
323
+ const token = await client.callback(
324
+ settings.redirectURI,
325
+ {
326
+ code,
327
+ iss,
328
+ },
329
+ { code_verifier: codeVerifier }
330
+ );
331
+ const userData = await client.userinfo(token);
332
+
333
+ return { token, userData };
334
+ }
335
+
336
+ /**
337
+ * Refreshes an OAuth token using the refresh token
338
+ */
339
+ export async function refreshOAuthToken(
340
+ settings: OIDCSettings,
341
+ refreshToken: string,
342
+ url: URL
343
+ ): Promise<TokenSet | null> {
344
+ const client = await getOIDCClient(settings, url);
345
+ const tokenSet = await client.refresh(refreshToken);
346
+ return tokenSet;
347
+ }
348
+
349
+ export async function validateAndParseCsrfToken(
350
+ token: string,
351
+ sessionId: string
352
+ ): Promise<{
353
+ /** This is the redirect url that was passed to the OIDC provider */
354
+ redirectUrl: string;
355
+ /** Relative path (within this app) to return to after login */
356
+ next?: string;
357
+ } | null> {
358
+ try {
359
+ const { data, signature } = z
360
+ .object({
361
+ data: z.object({
362
+ expiration: z.number().int(),
363
+ redirectUrl: z.string().url(),
364
+ next: z.string().optional(),
365
+ }),
366
+ signature: z.string().length(64),
367
+ })
368
+ .parse(JSON.parse(token));
369
+
370
+ const reconstructSign = await sha256(JSON.stringify(data) + "##" + sessionId);
371
+
372
+ if (data.expiration > Date.now() && signature === reconstructSign) {
373
+ return { redirectUrl: data.redirectUrl, next: sanitizeReturnPath(data.next) };
374
+ }
375
+ } catch (e) {
376
+ logger.error(e, "Error validating and parsing CSRF token");
377
+ }
378
+ return null;
379
+ }
380
+
381
+ type CookieRecord = Cookies;
382
+ type HeaderRecord = Headers;
383
+
384
+ export async function getCoupledCookieHash(cookie: CookieRecord): Promise<string | undefined> {
385
+ if (!config.COUPLE_SESSION_WITH_COOKIE_NAME) {
386
+ return undefined;
387
+ }
388
+
389
+ const cookieValue = cookie.get(config.COUPLE_SESSION_WITH_COOKIE_NAME);
390
+
391
+ if (!cookieValue) {
392
+ return "no-cookie";
393
+ }
394
+
395
+ return await sha256(cookieValue);
396
+ }
397
+
398
+ export async function authenticateRequest(
399
+ headers: HeaderRecord,
400
+ cookie: CookieRecord,
401
+ url: URL,
402
+ isApi?: boolean
403
+ ): Promise<App.Locals & { secretSessionId: string }> {
404
+ const token = cookie.get(config.COOKIE_NAME);
405
+
406
+ let email = null;
407
+ if (config.TRUSTED_EMAIL_HEADER) {
408
+ email = headers.get(config.TRUSTED_EMAIL_HEADER);
409
+ }
410
+
411
+ let secretSessionId: string | null = null;
412
+ let sessionId: string | null = null;
413
+
414
+ if (email) {
415
+ secretSessionId = sessionId = await sha256(email);
416
+ return {
417
+ user: {
418
+ _id: new ObjectId(sessionId.slice(0, 24)),
419
+ name: email,
420
+ email,
421
+ createdAt: new Date(),
422
+ updatedAt: new Date(),
423
+ hfUserId: email,
424
+ avatarUrl: "",
425
+ },
426
+ sessionId,
427
+ secretSessionId,
428
+ isAdmin: adminTokenManager.isAdmin(sessionId),
429
+ };
430
+ }
431
+
432
+ if (token) {
433
+ secretSessionId = token;
434
+ sessionId = await sha256(token);
435
+
436
+ const result = await findUser(sessionId, await getCoupledCookieHash(cookie), url);
437
+
438
+ if (result.invalidateSession) {
439
+ secretSessionId = crypto.randomUUID();
440
+ sessionId = await sha256(secretSessionId);
441
+
442
+ if (await collections.sessions.findOne({ sessionId })) {
443
+ throw new Error("Session ID collision");
444
+ }
445
+ }
446
+
447
+ return {
448
+ user: result.user ?? undefined,
449
+ token: result.oauth?.token?.value,
450
+ sessionId,
451
+ secretSessionId,
452
+ isAdmin: result.user?.isAdmin || adminTokenManager.isAdmin(sessionId),
453
+ };
454
+ }
455
+
456
+ if (isApi) {
457
+ const authorization = headers.get("Authorization");
458
+ if (authorization?.startsWith("Bearer ")) {
459
+ const token = authorization.slice(7);
460
+ const hash = await sha256(token);
461
+ sessionId = secretSessionId = hash;
462
+
463
+ const cacheHit = await collections.tokenCaches.findOne({ tokenHash: hash });
464
+ if (cacheHit) {
465
+ const user = await collections.users.findOne({ hfUserId: cacheHit.userId });
466
+ if (!user) {
467
+ throw new Error("User not found");
468
+ }
469
+ return {
470
+ user,
471
+ sessionId,
472
+ token,
473
+ secretSessionId,
474
+ isAdmin: user.isAdmin || adminTokenManager.isAdmin(sessionId),
475
+ };
476
+ }
477
+
478
+ const response = await fetch("https://huggingface.co/api/whoami-v2", {
479
+ headers: { Authorization: `Bearer ${token}` },
480
+ });
481
+
482
+ if (!response.ok) {
483
+ throw new Error("Unauthorized");
484
+ }
485
+
486
+ const data = await response.json();
487
+ const user = await collections.users.findOne({ hfUserId: data.id });
488
+ if (!user) {
489
+ throw new Error("User not found");
490
+ }
491
+
492
+ await collections.tokenCaches.insertOne({
493
+ tokenHash: hash,
494
+ userId: data.id,
495
+ createdAt: new Date(),
496
+ updatedAt: new Date(),
497
+ });
498
+
499
+ return {
500
+ user,
501
+ sessionId,
502
+ secretSessionId,
503
+ token,
504
+ isAdmin: user.isAdmin || adminTokenManager.isAdmin(sessionId),
505
+ };
506
+ }
507
+ }
508
+
509
+ // Generate new session if none exists
510
+ secretSessionId = crypto.randomUUID();
511
+ sessionId = await sha256(secretSessionId);
512
+
513
+ if (await collections.sessions.findOne({ sessionId })) {
514
+ throw new Error("Session ID collision");
515
+ }
516
+
517
+ return { user: undefined, sessionId, secretSessionId, isAdmin: false };
518
+ }
519
+
520
+ export async function triggerOauthFlow({ url, locals, cookies }: RequestEvent): Promise<Response> {
521
+ // const referer = request.headers.get("referer");
522
+ // let redirectURI = `${(referer ? new URL(referer) : url).origin}${base}/login/callback`;
523
+ let redirectURI = `${url.origin}${base}/login/callback`;
524
+
525
+ // TODO: Handle errors if provider is not responding
526
+
527
+ if (url.searchParams.has("callback")) {
528
+ const callback = url.searchParams.get("callback") || redirectURI;
529
+ if (config.ALTERNATIVE_REDIRECT_URLS.includes(callback)) {
530
+ redirectURI = callback;
531
+ }
532
+ }
533
+
534
+ // Preserve a safe in-app return path after login.
535
+ // Priority: explicit ?next=... (must be an absolute path), else the current path (when auto-login kicks in).
536
+ let next: string | undefined = undefined;
537
+ const nextParam = sanitizeReturnPath(url.searchParams.get("next"));
538
+ if (nextParam) {
539
+ // Only accept absolute in-app paths to prevent open redirects
540
+ next = nextParam;
541
+ } else if (!url.pathname.startsWith(`${base}/login`)) {
542
+ // For automatic login on protected pages, return to the page the user was on
543
+ next = sanitizeReturnPath(`${url.pathname}${url.search}`) ?? `${base}/`;
544
+ } else {
545
+ next = sanitizeReturnPath(`${base}/`) ?? "/";
546
+ }
547
+
548
+ const authorizationUrl = await getOIDCAuthorizationUrl(
549
+ { redirectURI },
550
+ { sessionId: locals.sessionId, next, url, cookies }
551
+ );
552
+
553
+ throw redirect(302, authorizationUrl);
554
+ }