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.
- package/README.md +393 -393
- package/bin/ruflo.js +57 -57
- package/package.json +1 -1
- package/src/chat-ui/Dockerfile +25 -25
- package/src/chat-ui/patch-mcp-url-safety.sh +28 -28
- package/src/config/config.example.json +76 -76
- package/src/mcp-bridge/Dockerfile +45 -45
- package/src/mcp-bridge/index.js +1668 -1668
- package/src/mcp-bridge/mcp-stdio-kernel.js +159 -159
- package/src/mcp-bridge/package.json +17 -17
- package/src/mcp-bridge/test-harness.js +470 -470
- package/src/nginx/Dockerfile +10 -10
- package/src/nginx/nginx.conf +67 -67
- package/src/nginx/static/favicon-dark.svg +4 -4
- package/src/nginx/static/favicon.svg +4 -4
- package/src/nginx/static/icon.svg +5 -5
- package/src/nginx/static/logo.svg +9 -9
- package/src/nginx/static/manifest.json +22 -22
- package/src/nginx/static/welcome.js +184 -184
- package/src/ruvocal/.claude/skills/add-model-descriptions/SKILL.md +73 -73
- package/src/ruvocal/.claude-flow/daemon-state.json +135 -0
- package/src/ruvocal/.claude-flow/data/pending-insights.jsonl +0 -0
- package/src/ruvocal/.claude-flow/data/ranked-context.json +5 -0
- package/src/ruvocal/.claude-flow/logs/daemon.log +31 -0
- package/src/ruvocal/.claude-flow/logs/headless/audit_1777949411822_juxau0_prompt.log +989 -0
- package/src/ruvocal/.claude-flow/logs/headless/audit_1777949411822_juxau0_result.log +67 -0
- package/src/ruvocal/.claude-flow/logs/headless/audit_1777950042278_jvj5xq_prompt.log +989 -0
- package/src/ruvocal/.claude-flow/logs/headless/audit_1777950042278_jvj5xq_result.log +93 -0
- package/src/ruvocal/.claude-flow/logs/headless/optimize_1777949531823_yt5yc2_prompt.log +1498 -0
- package/src/ruvocal/.claude-flow/logs/headless/optimize_1777949531823_yt5yc2_result.log +93 -0
- package/src/ruvocal/.claude-flow/logs/headless/testgaps_1777949771821_elw1j4_prompt.log +1498 -0
- package/src/ruvocal/.claude-flow/logs/headless/testgaps_1777949771821_elw1j4_result.log +100 -0
- package/src/ruvocal/.claude-flow/metrics/codebase-map.json +11 -0
- package/src/ruvocal/.claude-flow/metrics/consolidation.json +6 -0
- package/src/ruvocal/.claude-flow/neural/stats.json +6 -0
- package/src/ruvocal/.claude-flow/sessions/current.json +13 -0
- package/src/ruvocal/.devcontainer/Dockerfile +9 -9
- package/src/ruvocal/.devcontainer/devcontainer.json +36 -36
- package/src/ruvocal/.dockerignore +16 -16
- package/src/ruvocal/.eslintignore +13 -13
- package/src/ruvocal/.eslintrc.cjs +45 -45
- package/src/ruvocal/.gcloudignore +18 -18
- package/src/ruvocal/.github/ISSUE_TEMPLATE/bug-report--chat-ui-.md +43 -43
- package/src/ruvocal/.github/ISSUE_TEMPLATE/config-support.md +9 -9
- package/src/ruvocal/.github/ISSUE_TEMPLATE/feature-request--chat-ui-.md +17 -17
- package/src/ruvocal/.github/ISSUE_TEMPLATE/huggingchat.md +11 -11
- package/src/ruvocal/.github/release.yml +16 -16
- package/src/ruvocal/.github/workflows/build-docs.yml +18 -18
- package/src/ruvocal/.github/workflows/build-image.yml +142 -142
- package/src/ruvocal/.github/workflows/build-pr-docs.yml +20 -20
- package/src/ruvocal/.github/workflows/deploy-dev.yml +63 -63
- package/src/ruvocal/.github/workflows/deploy-prod.yml +78 -78
- package/src/ruvocal/.github/workflows/lint-and-test.yml +84 -84
- package/src/ruvocal/.github/workflows/slugify.yaml +72 -72
- package/src/ruvocal/.github/workflows/trufflehog.yml +17 -17
- package/src/ruvocal/.github/workflows/upload-pr-documentation.yml +16 -16
- package/src/ruvocal/.husky/lint-stage-config.js +4 -4
- package/src/ruvocal/.husky/pre-commit +2 -2
- package/src/ruvocal/.prettierignore +14 -14
- package/src/ruvocal/.prettierrc +7 -7
- package/src/ruvocal/.swarm/attestation.db +0 -0
- package/src/ruvocal/.swarm/hnsw.index +0 -0
- package/src/ruvocal/.swarm/hnsw.metadata.json +1 -0
- package/src/ruvocal/.swarm/memory.db +0 -0
- package/src/ruvocal/.swarm/schema.sql +305 -0
- package/src/ruvocal/CLAUDE.md +126 -126
- package/src/ruvocal/Dockerfile +96 -96
- package/src/ruvocal/LICENSE +202 -202
- package/src/ruvocal/PRIVACY.md +41 -41
- package/src/ruvocal/README.md +164 -164
- package/src/ruvocal/chart/Chart.yaml +5 -5
- package/src/ruvocal/chart/env/dev.yaml +260 -260
- package/src/ruvocal/chart/env/prod.yaml +273 -273
- package/src/ruvocal/chart/templates/_helpers.tpl +22 -22
- package/src/ruvocal/chart/templates/config.yaml +10 -10
- package/src/ruvocal/chart/templates/deployment.yaml +81 -81
- package/src/ruvocal/chart/templates/hpa.yaml +45 -45
- package/src/ruvocal/chart/templates/infisical.yaml +24 -24
- package/src/ruvocal/chart/templates/ingress-internal.yaml +32 -32
- package/src/ruvocal/chart/templates/ingress.yaml +32 -32
- package/src/ruvocal/chart/templates/network-policy.yaml +36 -36
- package/src/ruvocal/chart/templates/service-account.yaml +13 -13
- package/src/ruvocal/chart/templates/service-monitor.yaml +17 -17
- package/src/ruvocal/chart/templates/service.yaml +21 -21
- package/src/ruvocal/chart/values.yaml +73 -73
- package/src/ruvocal/cloudbuild.yaml +68 -68
- package/src/ruvocal/config/branding.env.example +19 -19
- package/src/ruvocal/docker-compose.yml +21 -21
- package/src/ruvocal/docs/adr/ADR-029-HUGGINGFACE-CHAT-UI-CLOUD-RUN.md +1236 -1236
- package/src/ruvocal/docs/adr/ADR-033-RUVECTOR-RUFLO-MCP-INTEGRATION.md +111 -111
- package/src/ruvocal/docs/adr/ADR-034-OPTIONAL-MCP-BACKENDS.md +117 -117
- package/src/ruvocal/docs/adr/ADR-035-MCP-TOOL-GROUPS.md +186 -186
- package/src/ruvocal/docs/adr/ADR-037-AUTOPILOT-CHAT-MODE.md +1500 -1500
- package/src/ruvocal/docs/adr/ADR-038-RUVOCAL-FORK.md +286 -286
- package/src/ruvocal/docs/source/_toctree.yml +30 -30
- package/src/ruvocal/docs/source/configuration/common-issues.md +38 -38
- package/src/ruvocal/docs/source/configuration/llm-router.md +105 -105
- package/src/ruvocal/docs/source/configuration/mcp-tools.md +84 -84
- package/src/ruvocal/docs/source/configuration/metrics.md +9 -9
- package/src/ruvocal/docs/source/configuration/open-id.md +57 -57
- package/src/ruvocal/docs/source/configuration/overview.md +89 -89
- package/src/ruvocal/docs/source/configuration/theming.md +20 -20
- package/src/ruvocal/docs/source/developing/architecture.md +48 -48
- package/src/ruvocal/docs/source/index.md +53 -53
- package/src/ruvocal/docs/source/installation/docker.md +43 -43
- package/src/ruvocal/docs/source/installation/helm.md +43 -43
- package/src/ruvocal/docs/source/installation/local.md +62 -62
- package/src/ruvocal/entrypoint.sh +18 -18
- package/src/ruvocal/mcp-bridge/Dockerfile +45 -45
- package/src/ruvocal/mcp-bridge/cloudbuild.yaml +49 -49
- package/src/ruvocal/mcp-bridge/index.js +1878 -1878
- package/src/ruvocal/mcp-bridge/mcp-stdio-kernel.js +159 -159
- package/src/ruvocal/mcp-bridge/package-lock.json +762 -762
- package/src/ruvocal/mcp-bridge/package.json +17 -17
- package/src/ruvocal/mcp-bridge/test-harness.js +470 -470
- package/src/ruvocal/package-lock.json +11741 -11741
- package/src/ruvocal/package.json +121 -121
- package/src/ruvocal/postcss.config.js +6 -6
- package/src/ruvocal/rvf.manifest.json +204 -204
- package/src/ruvocal/scripts/config.ts +64 -64
- package/src/ruvocal/scripts/generate-welcome.mjs +181 -181
- package/src/ruvocal/scripts/populate.ts +288 -288
- package/src/ruvocal/scripts/samples.txt +194 -194
- package/src/ruvocal/scripts/setups/vitest-setup-server.ts +44 -44
- package/src/ruvocal/scripts/updateLocalEnv.ts +48 -48
- package/src/ruvocal/src/ambient.d.ts +7 -7
- package/src/ruvocal/src/app.d.ts +29 -29
- package/src/ruvocal/src/app.html +53 -53
- package/src/ruvocal/src/hooks.server.ts +32 -32
- package/src/ruvocal/src/hooks.ts +6 -6
- package/src/ruvocal/src/lib/APIClient.ts +148 -148
- package/src/ruvocal/src/lib/actions/clickOutside.ts +18 -18
- package/src/ruvocal/src/lib/actions/snapScrollToBottom.ts +346 -346
- package/src/ruvocal/src/lib/buildPrompt.ts +33 -33
- package/src/ruvocal/src/lib/components/AnnouncementBanner.svelte +20 -20
- package/src/ruvocal/src/lib/components/BackgroundGenerationPoller.svelte +168 -168
- package/src/ruvocal/src/lib/components/CodeBlock.svelte +73 -73
- package/src/ruvocal/src/lib/components/CopyToClipBoardBtn.svelte +92 -92
- package/src/ruvocal/src/lib/components/DeleteConversationModal.svelte +75 -75
- package/src/ruvocal/src/lib/components/EditConversationModal.svelte +100 -100
- package/src/ruvocal/src/lib/components/ExpandNavigation.svelte +22 -22
- package/src/ruvocal/src/lib/components/FoundationBackground.svelte +242 -242
- package/src/ruvocal/src/lib/components/HoverTooltip.svelte +44 -44
- package/src/ruvocal/src/lib/components/HtmlPreviewModal.svelte +143 -143
- package/src/ruvocal/src/lib/components/InfiniteScroll.svelte +50 -50
- package/src/ruvocal/src/lib/components/MobileNav.svelte +300 -300
- package/src/ruvocal/src/lib/components/Modal.svelte +115 -115
- package/src/ruvocal/src/lib/components/ModelCardMetadata.svelte +71 -71
- package/src/ruvocal/src/lib/components/NavConversationItem.svelte +151 -151
- package/src/ruvocal/src/lib/components/NavMenu.svelte +313 -313
- package/src/ruvocal/src/lib/components/Pagination.svelte +97 -97
- package/src/ruvocal/src/lib/components/PaginationArrow.svelte +27 -27
- package/src/ruvocal/src/lib/components/Portal.svelte +24 -24
- package/src/ruvocal/src/lib/components/RetryBtn.svelte +18 -18
- package/src/ruvocal/src/lib/components/RuFloUniverse.svelte +185 -185
- package/src/ruvocal/src/lib/components/RufloHelpModal.svelte +411 -411
- package/src/ruvocal/src/lib/components/ScrollToBottomBtn.svelte +47 -47
- package/src/ruvocal/src/lib/components/ScrollToPreviousBtn.svelte +77 -77
- package/src/ruvocal/src/lib/components/ShareConversationModal.svelte +182 -182
- package/src/ruvocal/src/lib/components/StopGeneratingBtn.svelte +69 -69
- package/src/ruvocal/src/lib/components/SubscribeModal.svelte +87 -87
- package/src/ruvocal/src/lib/components/Switch.svelte +36 -36
- package/src/ruvocal/src/lib/components/SystemPromptModal.svelte +44 -44
- package/src/ruvocal/src/lib/components/Toast.svelte +27 -27
- package/src/ruvocal/src/lib/components/Tooltip.svelte +30 -30
- package/src/ruvocal/src/lib/components/WelcomeModal.svelte +46 -46
- package/src/ruvocal/src/lib/components/chat/Alternatives.svelte +77 -77
- package/src/ruvocal/src/lib/components/chat/BlockWrapper.svelte +72 -72
- package/src/ruvocal/src/lib/components/chat/ChatInput.svelte +490 -490
- package/src/ruvocal/src/lib/components/chat/ChatIntroduction.svelte +123 -123
- package/src/ruvocal/src/lib/components/chat/ChatMessage.svelte +548 -548
- package/src/ruvocal/src/lib/components/chat/ChatWindow.svelte +1057 -1057
- package/src/ruvocal/src/lib/components/chat/FileDropzone.svelte +92 -92
- package/src/ruvocal/src/lib/components/chat/ImageLightbox.svelte +66 -66
- package/src/ruvocal/src/lib/components/chat/MarkdownBlock.svelte +23 -23
- package/src/ruvocal/src/lib/components/chat/MarkdownRenderer.svelte +69 -69
- package/src/ruvocal/src/lib/components/chat/MarkdownRenderer.svelte.test.ts +58 -58
- package/src/ruvocal/src/lib/components/chat/MessageAvatar.svelte +103 -103
- package/src/ruvocal/src/lib/components/chat/ModelSwitch.svelte +64 -64
- package/src/ruvocal/src/lib/components/chat/OpenReasoningResults.svelte +81 -81
- package/src/ruvocal/src/lib/components/chat/TaskGroup.svelte +88 -88
- package/src/ruvocal/src/lib/components/chat/ToolUpdate.svelte +273 -273
- package/src/ruvocal/src/lib/components/chat/UploadedFile.svelte +253 -253
- package/src/ruvocal/src/lib/components/chat/UrlFetchModal.svelte +203 -203
- package/src/ruvocal/src/lib/components/chat/VoiceRecorder.svelte +214 -214
- package/src/ruvocal/src/lib/components/icons/IconBurger.svelte +20 -20
- package/src/ruvocal/src/lib/components/icons/IconCheap.svelte +20 -20
- package/src/ruvocal/src/lib/components/icons/IconChevron.svelte +24 -24
- package/src/ruvocal/src/lib/components/icons/IconDazzled.svelte +40 -40
- package/src/ruvocal/src/lib/components/icons/IconFast.svelte +20 -20
- package/src/ruvocal/src/lib/components/icons/IconLoading.svelte +22 -22
- package/src/ruvocal/src/lib/components/icons/IconMCP.svelte +28 -28
- package/src/ruvocal/src/lib/components/icons/IconMoon.svelte +21 -21
- package/src/ruvocal/src/lib/components/icons/IconNew.svelte +20 -20
- package/src/ruvocal/src/lib/components/icons/IconOmni.svelte +90 -90
- package/src/ruvocal/src/lib/components/icons/IconPaperclip.svelte +24 -24
- package/src/ruvocal/src/lib/components/icons/IconPro.svelte +37 -37
- package/src/ruvocal/src/lib/components/icons/IconShare.svelte +21 -21
- package/src/ruvocal/src/lib/components/icons/IconSun.svelte +93 -93
- package/src/ruvocal/src/lib/components/icons/Logo.svelte +68 -68
- package/src/ruvocal/src/lib/components/icons/LogoHuggingFaceBorderless.svelte +54 -54
- package/src/ruvocal/src/lib/components/mcp/AddServerForm.svelte +250 -250
- package/src/ruvocal/src/lib/components/mcp/MCPServerManager.svelte +185 -185
- package/src/ruvocal/src/lib/components/mcp/ServerCard.svelte +203 -203
- package/src/ruvocal/src/lib/components/players/AudioPlayer.svelte +82 -82
- package/src/ruvocal/src/lib/components/voice/AudioWaveform.svelte +96 -96
- package/src/ruvocal/src/lib/components/wasm/GalleryPanel.svelte +357 -357
- package/src/ruvocal/src/lib/constants/mcpExamples.ts +114 -114
- package/src/ruvocal/src/lib/constants/mime.ts +11 -11
- package/src/ruvocal/src/lib/constants/pagination.ts +1 -1
- package/src/ruvocal/src/lib/constants/publicSepToken.ts +1 -1
- package/src/ruvocal/src/lib/constants/routerExamples.ts +133 -133
- package/src/ruvocal/src/lib/constants/rvagentPresets.ts +206 -206
- package/src/ruvocal/src/lib/createShareLink.ts +27 -27
- package/src/ruvocal/src/lib/jobs/refresh-conversation-stats.ts +297 -297
- package/src/ruvocal/src/lib/migrations/lock.ts +56 -56
- package/src/ruvocal/src/lib/migrations/migrations.spec.ts +74 -74
- package/src/ruvocal/src/lib/migrations/migrations.ts +109 -109
- package/src/ruvocal/src/lib/migrations/routines/01-update-search-assistants.ts +50 -50
- package/src/ruvocal/src/lib/migrations/routines/02-update-assistants-models.ts +48 -48
- package/src/ruvocal/src/lib/migrations/routines/04-update-message-updates.ts +151 -151
- package/src/ruvocal/src/lib/migrations/routines/05-update-message-files.ts +56 -56
- package/src/ruvocal/src/lib/migrations/routines/06-trim-message-updates.ts +56 -56
- package/src/ruvocal/src/lib/migrations/routines/08-update-featured-to-review.ts +32 -32
- package/src/ruvocal/src/lib/migrations/routines/09-delete-empty-conversations.spec.ts +214 -214
- package/src/ruvocal/src/lib/migrations/routines/09-delete-empty-conversations.ts +88 -88
- package/src/ruvocal/src/lib/migrations/routines/10-update-reports-assistantid.ts +29 -29
- package/src/ruvocal/src/lib/migrations/routines/index.ts +15 -15
- package/src/ruvocal/src/lib/server/__tests__/conversation-stop-generating.spec.ts +103 -103
- package/src/ruvocal/src/lib/server/abortRegistry.ts +57 -57
- package/src/ruvocal/src/lib/server/abortedGenerations.ts +43 -43
- package/src/ruvocal/src/lib/server/adminToken.ts +62 -62
- package/src/ruvocal/src/lib/server/api/__tests__/conversations-id.spec.ts +296 -296
- package/src/ruvocal/src/lib/server/api/__tests__/conversations-message.spec.ts +216 -216
- package/src/ruvocal/src/lib/server/api/__tests__/conversations.spec.ts +235 -235
- package/src/ruvocal/src/lib/server/api/__tests__/misc.spec.ts +72 -72
- package/src/ruvocal/src/lib/server/api/__tests__/testHelpers.ts +86 -86
- package/src/ruvocal/src/lib/server/api/__tests__/user-reports.spec.ts +78 -78
- package/src/ruvocal/src/lib/server/api/__tests__/user.spec.ts +239 -239
- package/src/ruvocal/src/lib/server/api/types.ts +37 -37
- package/src/ruvocal/src/lib/server/api/utils/requireAuth.ts +22 -22
- package/src/ruvocal/src/lib/server/api/utils/resolveConversation.ts +69 -69
- package/src/ruvocal/src/lib/server/api/utils/resolveModel.ts +27 -27
- package/src/ruvocal/src/lib/server/api/utils/superjsonResponse.ts +15 -15
- package/src/ruvocal/src/lib/server/apiToken.ts +11 -11
- package/src/ruvocal/src/lib/server/auth.ts +554 -554
- package/src/ruvocal/src/lib/server/config.ts +187 -187
- package/src/ruvocal/src/lib/server/conversation.ts +83 -83
- package/src/ruvocal/src/lib/server/database/__tests__/rvf.spec.ts +709 -709
- package/src/ruvocal/src/lib/server/database/postgres.ts +700 -700
- package/src/ruvocal/src/lib/server/database/rvf.ts +1078 -1078
- package/src/ruvocal/src/lib/server/database.ts +145 -145
- package/src/ruvocal/src/lib/server/endpoints/document.ts +68 -68
- package/src/ruvocal/src/lib/server/endpoints/endpoints.ts +43 -43
- package/src/ruvocal/src/lib/server/endpoints/images.ts +211 -211
- package/src/ruvocal/src/lib/server/endpoints/openai/endpointOai.ts +266 -266
- package/src/ruvocal/src/lib/server/endpoints/openai/openAIChatToTextGenerationStream.ts +212 -212
- package/src/ruvocal/src/lib/server/endpoints/openai/openAICompletionToTextGenerationStream.ts +32 -32
- package/src/ruvocal/src/lib/server/endpoints/preprocessMessages.ts +61 -61
- package/src/ruvocal/src/lib/server/exitHandler.ts +59 -59
- package/src/ruvocal/src/lib/server/files/downloadFile.ts +34 -34
- package/src/ruvocal/src/lib/server/files/uploadFile.ts +29 -29
- package/src/ruvocal/src/lib/server/findRepoRoot.ts +13 -13
- package/src/ruvocal/src/lib/server/generateFromDefaultEndpoint.ts +46 -46
- package/src/ruvocal/src/lib/server/hooks/error.ts +37 -37
- package/src/ruvocal/src/lib/server/hooks/fetch.ts +22 -22
- package/src/ruvocal/src/lib/server/hooks/handle.ts +250 -250
- package/src/ruvocal/src/lib/server/hooks/init.ts +51 -51
- package/src/ruvocal/src/lib/server/isURLLocal.spec.ts +31 -31
- package/src/ruvocal/src/lib/server/isURLLocal.ts +74 -74
- package/src/ruvocal/src/lib/server/logger.ts +42 -42
- package/src/ruvocal/src/lib/server/mcp/clientPool.spec.ts +175 -175
- package/src/ruvocal/src/lib/server/mcp/hf.ts +32 -32
- package/src/ruvocal/src/lib/server/mcp/httpClient.ts +122 -122
- package/src/ruvocal/src/lib/server/mcp/registry.ts +76 -76
- package/src/ruvocal/src/lib/server/mcp/tools.ts +196 -196
- package/src/ruvocal/src/lib/server/metrics.ts +255 -255
- package/src/ruvocal/src/lib/server/models.ts +518 -518
- package/src/ruvocal/src/lib/server/requestContext.ts +55 -55
- package/src/ruvocal/src/lib/server/router/arch.ts +230 -230
- package/src/ruvocal/src/lib/server/router/endpoint.ts +316 -316
- package/src/ruvocal/src/lib/server/router/multimodal.ts +28 -28
- package/src/ruvocal/src/lib/server/router/policy.ts +49 -49
- package/src/ruvocal/src/lib/server/router/toolsRoute.ts +51 -51
- package/src/ruvocal/src/lib/server/router/types.ts +21 -21
- package/src/ruvocal/src/lib/server/sendSlack.ts +23 -23
- package/src/ruvocal/src/lib/server/textGeneration/generate.ts +258 -258
- package/src/ruvocal/src/lib/server/textGeneration/index.ts +96 -96
- package/src/ruvocal/src/lib/server/textGeneration/mcp/fileRefs.ts +155 -155
- package/src/ruvocal/src/lib/server/textGeneration/mcp/routerResolution.ts +108 -108
- package/src/ruvocal/src/lib/server/textGeneration/mcp/runMcpFlow.ts +831 -831
- package/src/ruvocal/src/lib/server/textGeneration/mcp/toolInvocation.ts +349 -349
- package/src/ruvocal/src/lib/server/textGeneration/mcp/wasmTools.test.ts +633 -633
- package/src/ruvocal/src/lib/server/textGeneration/reasoning.ts +23 -23
- package/src/ruvocal/src/lib/server/textGeneration/title.ts +83 -83
- package/src/ruvocal/src/lib/server/textGeneration/types.ts +28 -28
- package/src/ruvocal/src/lib/server/textGeneration/utils/prepareFiles.ts +88 -88
- package/src/ruvocal/src/lib/server/textGeneration/utils/routing.ts +21 -21
- package/src/ruvocal/src/lib/server/textGeneration/utils/toolPrompt.ts +49 -49
- package/src/ruvocal/src/lib/server/urlSafety.ts +77 -77
- package/src/ruvocal/src/lib/server/usageLimits.ts +30 -30
- package/src/ruvocal/src/lib/stores/autopilotStore.svelte.ts +175 -175
- package/src/ruvocal/src/lib/stores/backgroundGenerations.svelte.ts +32 -32
- package/src/ruvocal/src/lib/stores/backgroundGenerations.ts +1 -1
- package/src/ruvocal/src/lib/stores/errors.ts +9 -9
- package/src/ruvocal/src/lib/stores/isAborted.ts +3 -3
- package/src/ruvocal/src/lib/stores/isPro.ts +4 -4
- package/src/ruvocal/src/lib/stores/loading.ts +3 -3
- package/src/ruvocal/src/lib/stores/mcpServers.ts +534 -534
- package/src/ruvocal/src/lib/stores/pendingChatInput.ts +3 -3
- package/src/ruvocal/src/lib/stores/pendingMessage.ts +9 -9
- package/src/ruvocal/src/lib/stores/settings.ts +182 -182
- package/src/ruvocal/src/lib/stores/shareModal.ts +13 -13
- package/src/ruvocal/src/lib/stores/titleUpdate.ts +8 -8
- package/src/ruvocal/src/lib/stores/wasmMcp.ts +472 -472
- package/src/ruvocal/src/lib/switchTheme.ts +124 -124
- package/src/ruvocal/src/lib/types/AbortedGeneration.ts +8 -8
- package/src/ruvocal/src/lib/types/Assistant.ts +31 -31
- package/src/ruvocal/src/lib/types/AssistantStats.ts +11 -11
- package/src/ruvocal/src/lib/types/ConfigKey.ts +4 -4
- package/src/ruvocal/src/lib/types/ConvSidebar.ts +9 -9
- package/src/ruvocal/src/lib/types/Conversation.ts +27 -27
- package/src/ruvocal/src/lib/types/ConversationStats.ts +13 -13
- package/src/ruvocal/src/lib/types/Message.ts +41 -41
- package/src/ruvocal/src/lib/types/MessageEvent.ts +10 -10
- package/src/ruvocal/src/lib/types/MessageUpdate.ts +139 -139
- package/src/ruvocal/src/lib/types/MigrationResult.ts +7 -7
- package/src/ruvocal/src/lib/types/Model.ts +23 -23
- package/src/ruvocal/src/lib/types/Report.ts +12 -12
- package/src/ruvocal/src/lib/types/Review.ts +6 -6
- package/src/ruvocal/src/lib/types/Semaphore.ts +19 -19
- package/src/ruvocal/src/lib/types/Session.ts +22 -22
- package/src/ruvocal/src/lib/types/Settings.ts +93 -93
- package/src/ruvocal/src/lib/types/SharedConversation.ts +9 -9
- package/src/ruvocal/src/lib/types/Template.ts +6 -6
- package/src/ruvocal/src/lib/types/Timestamps.ts +4 -4
- package/src/ruvocal/src/lib/types/TokenCache.ts +6 -6
- package/src/ruvocal/src/lib/types/Tool.ts +77 -77
- package/src/ruvocal/src/lib/types/UrlDependency.ts +5 -5
- package/src/ruvocal/src/lib/types/User.ts +14 -14
- package/src/ruvocal/src/lib/utils/PublicConfig.svelte.ts +75 -75
- package/src/ruvocal/src/lib/utils/auth.ts +17 -17
- package/src/ruvocal/src/lib/utils/chunk.ts +33 -33
- package/src/ruvocal/src/lib/utils/cookiesAreEnabled.ts +13 -13
- package/src/ruvocal/src/lib/utils/debounce.ts +17 -17
- package/src/ruvocal/src/lib/utils/deepestChild.ts +6 -6
- package/src/ruvocal/src/lib/utils/favicon.ts +21 -21
- package/src/ruvocal/src/lib/utils/fetchJSON.ts +23 -23
- package/src/ruvocal/src/lib/utils/file2base64.ts +14 -14
- package/src/ruvocal/src/lib/utils/formatUserCount.ts +37 -37
- package/src/ruvocal/src/lib/utils/generationState.spec.ts +75 -75
- package/src/ruvocal/src/lib/utils/generationState.ts +26 -26
- package/src/ruvocal/src/lib/utils/getHref.ts +41 -41
- package/src/ruvocal/src/lib/utils/getReturnFromGenerator.ts +7 -7
- package/src/ruvocal/src/lib/utils/haptics.ts +64 -64
- package/src/ruvocal/src/lib/utils/hashConv.ts +12 -12
- package/src/ruvocal/src/lib/utils/hf.ts +17 -17
- package/src/ruvocal/src/lib/utils/isDesktop.ts +7 -7
- package/src/ruvocal/src/lib/utils/isUrl.ts +8 -8
- package/src/ruvocal/src/lib/utils/isVirtualKeyboard.ts +16 -16
- package/src/ruvocal/src/lib/utils/loadAttachmentsFromUrls.ts +115 -115
- package/src/ruvocal/src/lib/utils/marked.spec.ts +96 -96
- package/src/ruvocal/src/lib/utils/marked.ts +531 -531
- package/src/ruvocal/src/lib/utils/mcpValidation.ts +147 -147
- package/src/ruvocal/src/lib/utils/mergeAsyncGenerators.ts +38 -38
- package/src/ruvocal/src/lib/utils/messageUpdates.spec.ts +262 -262
- package/src/ruvocal/src/lib/utils/messageUpdates.ts +324 -324
- package/src/ruvocal/src/lib/utils/mime.ts +56 -56
- package/src/ruvocal/src/lib/utils/models.ts +14 -14
- package/src/ruvocal/src/lib/utils/parseBlocks.ts +120 -120
- package/src/ruvocal/src/lib/utils/parseIncompleteMarkdown.ts +644 -644
- package/src/ruvocal/src/lib/utils/parseStringToList.ts +10 -10
- package/src/ruvocal/src/lib/utils/randomUuid.ts +14 -14
- package/src/ruvocal/src/lib/utils/searchTokens.ts +33 -33
- package/src/ruvocal/src/lib/utils/sha256.ts +7 -7
- package/src/ruvocal/src/lib/utils/stringifyError.ts +12 -12
- package/src/ruvocal/src/lib/utils/sum.ts +3 -3
- package/src/ruvocal/src/lib/utils/template.spec.ts +59 -59
- package/src/ruvocal/src/lib/utils/template.ts +53 -53
- package/src/ruvocal/src/lib/utils/timeout.ts +9 -9
- package/src/ruvocal/src/lib/utils/toolProgress.spec.ts +46 -46
- package/src/ruvocal/src/lib/utils/toolProgress.ts +11 -11
- package/src/ruvocal/src/lib/utils/tree/addChildren.spec.ts +102 -102
- package/src/ruvocal/src/lib/utils/tree/addChildren.ts +48 -48
- package/src/ruvocal/src/lib/utils/tree/addSibling.spec.ts +81 -81
- package/src/ruvocal/src/lib/utils/tree/addSibling.ts +41 -41
- package/src/ruvocal/src/lib/utils/tree/buildSubtree.spec.ts +110 -110
- package/src/ruvocal/src/lib/utils/tree/buildSubtree.ts +24 -24
- package/src/ruvocal/src/lib/utils/tree/convertLegacyConversation.spec.ts +31 -31
- package/src/ruvocal/src/lib/utils/tree/convertLegacyConversation.ts +36 -36
- package/src/ruvocal/src/lib/utils/tree/isMessageId.spec.ts +15 -15
- package/src/ruvocal/src/lib/utils/tree/isMessageId.ts +5 -5
- package/src/ruvocal/src/lib/utils/tree/tree.d.ts +14 -14
- package/src/ruvocal/src/lib/utils/tree/treeHelpers.spec.ts +167 -167
- package/src/ruvocal/src/lib/utils/updates.ts +39 -39
- package/src/ruvocal/src/lib/utils/urlParams.ts +13 -13
- package/src/ruvocal/src/lib/wasm/idb.ts +438 -438
- package/src/ruvocal/src/lib/wasm/index.ts +1213 -1213
- package/src/ruvocal/src/lib/wasm/tests/wasm-capabilities.test.ts +565 -565
- package/src/ruvocal/src/lib/wasm/wasm.worker.ts +332 -332
- package/src/ruvocal/src/lib/wasm/workerClient.ts +166 -166
- package/src/ruvocal/src/lib/workers/autopilotWorker.ts +221 -221
- package/src/ruvocal/src/lib/workers/detailFetchWorker.ts +100 -100
- package/src/ruvocal/src/lib/workers/markdownWorker.ts +61 -61
- package/src/ruvocal/src/routes/+error.svelte +20 -20
- package/src/ruvocal/src/routes/+layout.svelte +324 -324
- package/src/ruvocal/src/routes/+layout.ts +91 -91
- package/src/ruvocal/src/routes/+page.svelte +168 -168
- package/src/ruvocal/src/routes/.well-known/oauth-cimd/+server.ts +37 -37
- package/src/ruvocal/src/routes/__debug/openai/+server.ts +21 -21
- package/src/ruvocal/src/routes/admin/export/+server.ts +159 -159
- package/src/ruvocal/src/routes/admin/stats/compute/+server.ts +16 -16
- package/src/ruvocal/src/routes/api/conversation/[id]/+server.ts +40 -40
- package/src/ruvocal/src/routes/api/conversation/[id]/message/[messageId]/+server.ts +42 -42
- package/src/ruvocal/src/routes/api/conversations/+server.ts +48 -48
- package/src/ruvocal/src/routes/api/fetch-url/+server.ts +147 -147
- package/src/ruvocal/src/routes/api/mcp/health/+server.ts +292 -292
- package/src/ruvocal/src/routes/api/mcp/servers/+server.ts +32 -32
- package/src/ruvocal/src/routes/api/models/+server.ts +25 -25
- package/src/ruvocal/src/routes/api/transcribe/+server.ts +104 -104
- package/src/ruvocal/src/routes/api/user/+server.ts +15 -15
- package/src/ruvocal/src/routes/api/user/validate-token/+server.ts +20 -20
- package/src/ruvocal/src/routes/api/v2/conversations/+server.ts +48 -48
- package/src/ruvocal/src/routes/api/v2/conversations/[id]/+server.ts +94 -94
- package/src/ruvocal/src/routes/api/v2/conversations/[id]/message/[messageId]/+server.ts +43 -43
- package/src/ruvocal/src/routes/api/v2/conversations/import-share/+server.ts +23 -23
- package/src/ruvocal/src/routes/api/v2/debug/config/+server.ts +16 -16
- package/src/ruvocal/src/routes/api/v2/debug/refresh/+server.ts +30 -30
- package/src/ruvocal/src/routes/api/v2/export/+server.ts +196 -196
- package/src/ruvocal/src/routes/api/v2/feature-flags/+server.ts +14 -14
- package/src/ruvocal/src/routes/api/v2/models/+server.ts +38 -38
- package/src/ruvocal/src/routes/api/v2/models/[namespace]/+server.ts +8 -8
- package/src/ruvocal/src/routes/api/v2/models/[namespace]/[model]/+server.ts +8 -8
- package/src/ruvocal/src/routes/api/v2/models/[namespace]/[model]/subscribe/+server.ts +28 -28
- package/src/ruvocal/src/routes/api/v2/models/[namespace]/subscribe/+server.ts +28 -28
- package/src/ruvocal/src/routes/api/v2/models/old/+server.ts +7 -7
- package/src/ruvocal/src/routes/api/v2/models/refresh/+server.ts +33 -33
- package/src/ruvocal/src/routes/api/v2/public-config/+server.ts +7 -7
- package/src/ruvocal/src/routes/api/v2/user/+server.ts +17 -17
- package/src/ruvocal/src/routes/api/v2/user/billing-orgs/+server.ts +73 -73
- package/src/ruvocal/src/routes/api/v2/user/reports/+server.ts +17 -17
- package/src/ruvocal/src/routes/api/v2/user/settings/+server.ts +110 -110
- package/src/ruvocal/src/routes/conversation/+server.ts +115 -115
- package/src/ruvocal/src/routes/conversation/[id]/+page.svelte +586 -586
- package/src/ruvocal/src/routes/conversation/[id]/+page.ts +60 -60
- package/src/ruvocal/src/routes/conversation/[id]/+server.ts +740 -740
- package/src/ruvocal/src/routes/conversation/[id]/message/[messageId]/prompt/+server.ts +66 -66
- package/src/ruvocal/src/routes/conversation/[id]/share/+server.ts +69 -69
- package/src/ruvocal/src/routes/conversation/[id]/stop-generating/+server.ts +35 -35
- package/src/ruvocal/src/routes/healthcheck/+server.ts +3 -3
- package/src/ruvocal/src/routes/login/+server.ts +5 -5
- package/src/ruvocal/src/routes/login/callback/+server.ts +103 -103
- package/src/ruvocal/src/routes/login/callback/updateUser.spec.ts +157 -157
- package/src/ruvocal/src/routes/login/callback/updateUser.ts +215 -215
- package/src/ruvocal/src/routes/logout/+server.ts +18 -18
- package/src/ruvocal/src/routes/metrics/+server.ts +18 -18
- package/src/ruvocal/src/routes/models/+page.svelte +233 -233
- package/src/ruvocal/src/routes/models/[...model]/+page.svelte +161 -161
- package/src/ruvocal/src/routes/models/[...model]/+page.ts +14 -14
- package/src/ruvocal/src/routes/models/[...model]/thumbnail.png/+server.ts +64 -64
- package/src/ruvocal/src/routes/models/[...model]/thumbnail.png/ModelThumbnail.svelte +28 -28
- package/src/ruvocal/src/routes/privacy/+page.svelte +11 -11
- package/src/ruvocal/src/routes/r/[id]/+page.ts +34 -34
- package/src/ruvocal/src/routes/settings/(nav)/+layout.svelte +282 -282
- package/src/ruvocal/src/routes/settings/(nav)/+layout.ts +1 -1
- package/src/ruvocal/src/routes/settings/(nav)/+server.ts +59 -59
- package/src/ruvocal/src/routes/settings/(nav)/[...model]/+page.svelte +464 -464
- package/src/ruvocal/src/routes/settings/(nav)/[...model]/+page.ts +14 -14
- package/src/ruvocal/src/routes/settings/(nav)/application/+page.svelte +362 -362
- package/src/ruvocal/src/routes/settings/+layout.svelte +40 -40
- package/src/ruvocal/src/styles/highlight-js.css +195 -195
- package/src/ruvocal/src/styles/main.css +144 -144
- package/src/ruvocal/static/chatui/favicon-dark.svg +3 -3
- package/src/ruvocal/static/chatui/favicon-dev.svg +3 -3
- package/src/ruvocal/static/chatui/favicon.svg +3 -3
- package/src/ruvocal/static/chatui/icon.svg +3 -3
- package/src/ruvocal/static/chatui/logo.svg +7 -7
- package/src/ruvocal/static/chatui/manifest.json +54 -54
- package/src/ruvocal/static/chatui/welcome.js +184 -184
- package/src/ruvocal/static/huggingchat/favicon-dark.svg +4 -4
- package/src/ruvocal/static/huggingchat/favicon-dev.svg +4 -4
- package/src/ruvocal/static/huggingchat/favicon.svg +4 -4
- package/src/ruvocal/static/huggingchat/fulltext-logo.svg +1 -1
- package/src/ruvocal/static/huggingchat/icon.svg +4 -4
- package/src/ruvocal/static/huggingchat/logo.svg +4 -4
- package/src/ruvocal/static/huggingchat/manifest.json +54 -54
- package/src/ruvocal/static/huggingchat/routes.chat.json +226 -226
- package/src/ruvocal/static/robots.txt +10 -10
- package/src/ruvocal/static/wasm/rvagent_wasm.js +1539 -1539
- package/src/ruvocal/stub/@reflink/reflink/package.json +5 -5
- package/src/ruvocal/svelte.config.js +53 -53
- package/src/ruvocal/tailwind.config.cjs +30 -30
- package/src/ruvocal/tsconfig.json +19 -19
- package/src/ruvocal/vite.config.ts +87 -87
- package/src/scripts/deploy.sh +116 -116
- package/src/scripts/generate-config.js +245 -245
- package/src/scripts/generate-welcome.js +187 -187
- 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
|
+
}
|