@softtechai/quickmcp 1.0.16 → 1.1.0
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/dist/auth/auth-utils.d.ts +130 -0
- package/dist/auth/auth-utils.d.ts.map +1 -0
- package/dist/auth/auth-utils.js +600 -0
- package/dist/auth/auth-utils.js.map +1 -0
- package/dist/auth/jwks-provider.d.ts +9 -0
- package/dist/auth/jwks-provider.d.ts.map +1 -0
- package/dist/auth/jwks-provider.js +56 -0
- package/dist/auth/jwks-provider.js.map +1 -0
- package/dist/auth/token-utils.d.ts +40 -0
- package/dist/auth/token-utils.d.ts.map +1 -0
- package/dist/auth/token-utils.js +162 -0
- package/dist/auth/token-utils.js.map +1 -0
- package/dist/client/MCPClient.js +5 -4
- package/dist/client/MCPClient.js.map +1 -1
- package/dist/config/auth-config.d.ts +16 -0
- package/dist/config/auth-config.d.ts.map +1 -0
- package/dist/config/auth-config.js +107 -0
- package/dist/config/auth-config.js.map +1 -0
- package/dist/constant/constant.d.ts +20 -0
- package/dist/constant/constant.d.ts.map +1 -0
- package/dist/constant/constant.js +24 -0
- package/dist/constant/constant.js.map +1 -0
- package/dist/database/async-datastore.d.ts +6 -0
- package/dist/database/async-datastore.d.ts.map +1 -0
- package/dist/database/async-datastore.js +15 -0
- package/dist/database/async-datastore.js.map +1 -0
- package/dist/database/database-utils.d.ts +6 -0
- package/dist/database/database-utils.d.ts.map +1 -0
- package/dist/database/database-utils.js +29 -0
- package/dist/database/database-utils.js.map +1 -0
- package/dist/database/datastore.d.ts +164 -0
- package/dist/database/datastore.d.ts.map +1 -0
- package/dist/{parsers/types/index.js → database/datastore.js} +1 -0
- package/dist/database/datastore.js.map +1 -0
- package/dist/database/factory.d.ts +4 -0
- package/dist/database/factory.d.ts.map +1 -0
- package/dist/database/factory.js +32 -0
- package/dist/database/factory.js.map +1 -0
- package/dist/database/jdbc-manager.d.ts +49 -0
- package/dist/database/jdbc-manager.d.ts.map +1 -0
- package/dist/database/jdbc-manager.js +50 -0
- package/dist/database/jdbc-manager.js.map +1 -0
- package/dist/database/sqlite-manager.d.ts +46 -44
- package/dist/database/sqlite-manager.d.ts.map +1 -1
- package/dist/database/sqlite-manager.js +492 -42
- package/dist/database/sqlite-manager.js.map +1 -1
- package/dist/database/supabase-manager.d.ts +58 -0
- package/dist/database/supabase-manager.d.ts.map +1 -0
- package/dist/database/supabase-manager.js +432 -0
- package/dist/database/supabase-manager.js.map +1 -0
- package/dist/generators/MCPServerGenerator.d.ts +103 -20
- package/dist/generators/MCPServerGenerator.d.ts.map +1 -1
- package/dist/generators/MCPServerGenerator.js +6930 -128
- package/dist/generators/MCPServerGenerator.js.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -1
- package/dist/index.js.map +1 -1
- package/dist/integrated-mcp-server-new.d.ts +14 -2
- package/dist/integrated-mcp-server-new.d.ts.map +1 -1
- package/dist/integrated-mcp-server-new.js +270 -180
- package/dist/integrated-mcp-server-new.js.map +1 -1
- package/dist/mcp-core/McpCoreService.d.ts +63 -0
- package/dist/mcp-core/McpCoreService.d.ts.map +1 -0
- package/dist/mcp-core/McpCoreService.js +492 -0
- package/dist/mcp-core/McpCoreService.js.map +1 -0
- package/dist/parsers/CsvParser.d.ts +1 -1
- package/dist/parsers/CsvParser.d.ts.map +1 -1
- package/dist/parsers/CsvParser.js +3 -2
- package/dist/parsers/CsvParser.js.map +1 -1
- package/dist/parsers/DatabaseParser.d.ts.map +1 -1
- package/dist/parsers/DatabaseParser.js +9 -8
- package/dist/parsers/DatabaseParser.js.map +1 -1
- package/dist/parsers/ExcelParser.d.ts +15 -0
- package/dist/parsers/ExcelParser.d.ts.map +1 -1
- package/dist/parsers/ExcelParser.js +287 -21
- package/dist/parsers/ExcelParser.js.map +1 -1
- package/dist/parsers/WebPageParser.d.ts +5 -0
- package/dist/parsers/WebPageParser.d.ts.map +1 -0
- package/dist/parsers/WebPageParser.js +35 -0
- package/dist/parsers/WebPageParser.js.map +1 -0
- package/dist/parsers/index.d.ts +3 -2
- package/dist/parsers/index.d.ts.map +1 -1
- package/dist/parsers/index.js +19 -16
- package/dist/parsers/index.js.map +1 -1
- package/dist/server/api/askApi.d.ts +41 -0
- package/dist/server/api/askApi.d.ts.map +1 -0
- package/dist/server/api/askApi.js +479 -0
- package/dist/server/api/askApi.js.map +1 -0
- package/dist/server/api/authApi.d.ts +101 -0
- package/dist/server/api/authApi.d.ts.map +1 -0
- package/dist/server/api/authApi.js +1472 -0
- package/dist/server/api/authApi.js.map +1 -0
- package/dist/server/api/authProperty.d.ts +18 -0
- package/dist/server/api/authProperty.d.ts.map +1 -0
- package/dist/server/api/authProperty.js +41 -0
- package/dist/server/api/authProperty.js.map +1 -0
- package/dist/server/api/configApi.d.ts +15 -0
- package/dist/server/api/configApi.d.ts.map +1 -0
- package/dist/server/api/configApi.js +42 -0
- package/dist/server/api/configApi.js.map +1 -0
- package/dist/server/api/databaseApi.d.ts +14 -0
- package/dist/server/api/databaseApi.d.ts.map +1 -0
- package/dist/server/api/databaseApi.js +111 -0
- package/dist/server/api/databaseApi.js.map +1 -0
- package/dist/server/api/directoryApi.d.ts +9 -0
- package/dist/server/api/directoryApi.d.ts.map +1 -0
- package/dist/server/api/directoryApi.js +103 -0
- package/dist/server/api/directoryApi.js.map +1 -0
- package/dist/server/api/generateApi.d.ts +24 -0
- package/dist/server/api/generateApi.d.ts.map +1 -0
- package/dist/server/api/generateApi.js +457 -0
- package/dist/server/api/generateApi.js.map +1 -0
- package/dist/server/api/healthApi.d.ts +9 -0
- package/dist/server/api/healthApi.d.ts.map +1 -0
- package/dist/server/api/healthApi.js +15 -0
- package/dist/server/api/healthApi.js.map +1 -0
- package/dist/server/api/indexApi.d.ts +21 -0
- package/dist/server/api/indexApi.d.ts.map +1 -0
- package/dist/server/api/indexApi.js +61 -0
- package/dist/server/api/indexApi.js.map +1 -0
- package/dist/server/api/logsApi.d.ts +12 -0
- package/dist/server/api/logsApi.d.ts.map +1 -0
- package/dist/server/api/logsApi.js +37 -0
- package/dist/server/api/logsApi.js.map +1 -0
- package/dist/server/api/mcpApi.d.ts +20 -0
- package/dist/server/api/mcpApi.d.ts.map +1 -0
- package/dist/server/api/mcpApi.js +120 -0
- package/dist/server/api/mcpApi.js.map +1 -0
- package/dist/server/api/nameApi.d.ts +21 -0
- package/dist/server/api/nameApi.d.ts.map +1 -0
- package/dist/server/api/nameApi.js +42 -0
- package/dist/server/api/nameApi.js.map +1 -0
- package/dist/server/api/parseApi.d.ts +9 -0
- package/dist/server/api/parseApi.d.ts.map +1 -0
- package/dist/server/api/parseApi.js +3245 -0
- package/dist/server/api/parseApi.js.map +1 -0
- package/dist/server/api/serverApi.d.ts +44 -0
- package/dist/server/api/serverApi.d.ts.map +1 -0
- package/dist/server/api/serverApi.js +417 -0
- package/dist/server/api/serverApi.js.map +1 -0
- package/dist/{dynamic-mcp-executor.d.ts → server/dynamic-mcp-executor.d.ts} +4 -5
- package/dist/server/dynamic-mcp-executor.d.ts.map +1 -0
- package/dist/server/dynamic-mcp-executor.js +62 -0
- package/dist/server/dynamic-mcp-executor.js.map +1 -0
- package/dist/server/port-utils.d.ts +14 -0
- package/dist/server/port-utils.d.ts.map +1 -0
- package/dist/server/port-utils.js +31 -0
- package/dist/server/port-utils.js.map +1 -0
- package/dist/server/server-utils.d.ts +13 -0
- package/dist/server/server-utils.d.ts.map +1 -0
- package/dist/server/server-utils.js +72 -0
- package/dist/server/server-utils.js.map +1 -0
- package/dist/{web → server}/server.d.ts +1 -0
- package/dist/server/server.d.ts.map +1 -0
- package/dist/server/server.js +535 -0
- package/dist/server/server.js.map +1 -0
- package/dist/server/tool-executer.d.ts +101 -0
- package/dist/server/tool-executer.d.ts.map +1 -0
- package/dist/server/tool-executer.js +6198 -0
- package/dist/server/tool-executer.js.map +1 -0
- package/dist/types/index.d.ts +1197 -4
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +1028 -0
- package/dist/types/index.js.map +1 -1
- package/dist/upload/upload-utils.d.ts +4 -0
- package/dist/upload/upload-utils.d.ts.map +1 -0
- package/dist/upload/upload-utils.js +29 -0
- package/dist/upload/upload-utils.js.map +1 -0
- package/dist/utils/deployment-util.d.ts +14 -0
- package/dist/utils/deployment-util.d.ts.map +1 -0
- package/dist/utils/deployment-util.js +46 -0
- package/dist/utils/deployment-util.js.map +1 -0
- package/dist/utils/logger.d.ts +15 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +56 -0
- package/dist/utils/logger.js.map +1 -0
- package/package.json +18 -6
- package/quickmcp-direct-stdio.js +176 -187
- package/src/web/public/app.js +15370 -1471
- package/src/web/public/authorization.html +868 -0
- package/src/web/public/database-tables.html +283 -547
- package/src/web/public/how-to-use.html +446 -462
- package/src/web/public/how-to-use.js +4 -4
- package/src/web/public/images/app/activepieces.png +0 -0
- package/src/web/public/images/app/airtable.png +0 -0
- package/src/web/public/images/app/androidstudio.png +0 -0
- package/src/web/public/images/app/antigravity.png +0 -0
- package/src/web/public/images/app/applenotes.png +0 -0
- package/src/web/public/images/app/applereminders.png +0 -0
- package/src/web/public/images/app/asana.png +0 -0
- package/src/web/public/images/app/azureai.png +0 -0
- package/src/web/public/images/app/bash.png +0 -0
- package/src/web/public/images/app/bearnotes.png +0 -0
- package/src/web/public/images/app/bitbucket.png +0 -0
- package/src/web/public/images/app/claude.png +0 -0
- package/src/web/public/images/app/cli.png +0 -0
- package/src/web/public/images/app/clickup.png +0 -0
- package/src/web/public/images/app/cohere.png +0 -0
- package/src/web/public/images/app/confluence.png +0 -0
- package/src/web/public/images/app/confluence2.png +0 -0
- package/src/web/public/images/app/curl.png +0 -0
- package/src/web/public/images/app/curl_mini.png +0 -0
- package/src/web/public/images/app/cursor.png +0 -0
- package/src/web/public/images/app/db2.png +0 -0
- package/src/web/public/images/app/deepseek.png +0 -0
- package/src/web/public/images/app/discord.png +0 -0
- package/src/web/public/images/app/docker.png +0 -0
- package/src/web/public/images/app/dockerhub.png +0 -0
- package/src/web/public/images/app/dropbox.png +0 -0
- package/src/web/public/images/app/elasticsearch.png +0 -0
- package/src/web/public/images/app/facebook.png +0 -0
- package/src/web/public/images/app/falai.png +0 -0
- package/src/web/public/images/app/fireworks.png +0 -0
- package/src/web/public/images/app/gdrive.png +0 -0
- package/src/web/public/images/app/gemini.png +0 -0
- package/src/web/public/images/app/github.png +0 -0
- package/src/web/public/images/app/githubcopilot.png +0 -0
- package/src/web/public/images/app/gitlab.png +0 -0
- package/src/web/public/images/app/gmail.png +0 -0
- package/src/web/public/images/app/googlecalender.png +0 -0
- package/src/web/public/images/app/googledocs.png +0 -0
- package/src/web/public/images/app/googlesheets.png +0 -0
- package/src/web/public/images/app/gradle.png +0 -0
- package/src/web/public/images/app/grafana.png +0 -0
- package/src/web/public/images/app/graphql.png +0 -0
- package/src/web/public/images/app/grok.png +0 -0
- package/src/web/public/images/app/groq.png +0 -0
- package/src/web/public/images/app/hazelcast.png +0 -0
- package/src/web/public/images/app/huggingface.png +0 -0
- package/src/web/public/images/app/imessage.png +0 -0
- package/src/web/public/images/app/instagram.png +0 -0
- package/src/web/public/images/app/intellij.png +0 -0
- package/src/web/public/images/app/jenkins.png +0 -0
- package/src/web/public/images/app/jira.png +0 -0
- package/src/web/public/images/app/kafka.png +0 -0
- package/src/web/public/images/app/kubernetes.png +0 -0
- package/src/web/public/images/app/linear.png +0 -0
- package/src/web/public/images/app/linkedin.png +0 -0
- package/src/web/public/images/app/llama.png +0 -0
- package/src/web/public/images/app/make.png +0 -0
- package/src/web/public/images/app/maven.png +0 -0
- package/src/web/public/images/app/mcp.png +0 -0
- package/src/web/public/images/app/microsoftteams.png +0 -0
- package/src/web/public/images/app/mistral.png +0 -0
- package/src/web/public/images/app/monday.png +0 -0
- package/src/web/public/images/app/mongodb.png +0 -0
- package/src/web/public/images/app/mssql.png +0 -0
- package/src/web/public/images/app/mysql.png +0 -0
- package/src/web/public/images/app/n8n.png +0 -0
- package/src/web/public/images/app/notion.png +0 -0
- package/src/web/public/images/app/npm.png +0 -0
- package/src/web/public/images/app/nuget.png +0 -0
- package/src/web/public/images/app/obsidian.png +0 -0
- package/src/web/public/images/app/openai.png +0 -0
- package/src/web/public/images/app/openrouter.png +0 -0
- package/src/web/public/images/app/opensearch.png +0 -0
- package/src/web/public/images/app/openshift.png +0 -0
- package/src/web/public/images/app/oracle.png +0 -0
- package/src/web/public/images/app/perplexity.png +0 -0
- package/src/web/public/images/app/pipedream.png +0 -0
- package/src/web/public/images/app/postgresql.png +0 -0
- package/src/web/public/images/app/powershell.png +0 -0
- package/src/web/public/images/app/prometheus.png +0 -0
- package/src/web/public/images/app/reddit.png +0 -0
- package/src/web/public/images/app/redis.png +0 -0
- package/src/web/public/images/app/rss.png +0 -0
- package/src/web/public/images/app/signal.png +0 -0
- package/src/web/public/images/app/slack.png +0 -0
- package/src/web/public/images/app/soap.png +0 -0
- package/src/web/public/images/app/sqlite.png +0 -0
- package/src/web/public/images/app/supabase.png +0 -0
- package/src/web/public/images/app/telegram.png +0 -0
- package/src/web/public/images/app/things3.png +0 -0
- package/src/web/public/images/app/threads.png +0 -0
- package/src/web/public/images/app/tiktok.png +0 -0
- package/src/web/public/images/app/together.png +0 -0
- package/src/web/public/images/app/trello.png +0 -0
- package/src/web/public/images/app/vscode.png +0 -0
- package/src/web/public/images/app/webhook.png +0 -0
- package/src/web/public/images/app/webpage.png +0 -0
- package/src/web/public/images/app/whatsappbusiness.png +0 -0
- package/src/web/public/images/app/windsorf.png +0 -0
- package/src/web/public/images/app/x.png +0 -0
- package/src/web/public/images/app/youtube.png +0 -0
- package/src/web/public/images/app/zapier.png +0 -0
- package/src/web/public/images/app/zededitor.png +0 -0
- package/src/web/public/images/app/zoom.png +0 -0
- package/src/web/public/images/avatar-anon.svg +4 -0
- package/src/web/public/images/favicon.png +0 -0
- package/src/web/public/images/install/chatgpt-web/step0.png +0 -0
- package/src/web/public/images/install/chatgpt-web/step1.png +0 -0
- package/src/web/public/images/install/chatgpt-web/step2.png +0 -0
- package/src/web/public/images/install/chatgpt-web/step3.png +0 -0
- package/src/web/public/images/install/chatgpt-web/step4.png +0 -0
- package/src/web/public/images/install/chatgpt-web/step5.png +0 -0
- package/src/web/public/images/readme/1-generate-servers.png +0 -0
- package/src/web/public/images/readme/2-database-connection.png +0 -0
- package/src/web/public/images/readme/2-file-upload.png +0 -0
- package/src/web/public/images/readme/3-data-preview.png +0 -0
- package/src/web/public/images/readme/4-data-preview2.png +0 -0
- package/src/web/public/images/readme/5-server-configuration.png +0 -0
- package/src/web/public/images/readme/6-server-generated-modal.png +0 -0
- package/src/web/public/images/readme/7-generated-servers.png +0 -0
- package/src/web/public/images/readme/8-generated-servers-view-details.png +0 -0
- package/src/web/public/images/readme/Screenshot 2025-10-09 at 20.23.51.png +0 -0
- package/src/web/public/images/readme/Screenshot 2025-10-09 at 20.24.59.png +0 -0
- package/src/web/public/images/readme/Screenshot 2025-10-09 at 20.25.05.png +0 -0
- package/src/web/public/images/readme/Screenshot 2025-10-09 at 20.25.14.png +0 -0
- package/src/web/public/images/readme/Screenshot 2025-10-09 at 20.25.21.png +0 -0
- package/src/web/public/images/readme/Screenshot 2025-10-09 at 20.25.36.png +0 -0
- package/src/web/public/index.html +4685 -488
- package/src/web/public/landing.html +1638 -0
- package/src/web/public/logger.js +31 -0
- package/src/web/public/login.html +372 -0
- package/src/web/public/manage-servers.html +121 -188
- package/src/web/public/pricing.html +537 -0
- package/src/web/public/quick-ask.html +133 -0
- package/src/web/public/quickmcp-styles.css +708 -0
- package/src/web/public/roles.html +177 -0
- package/src/web/public/shared.js +736 -3
- package/src/web/public/sidebar.js +414 -0
- package/src/web/public/test-servers.html +605 -221
- package/src/web/public/users.html +191 -0
- package/dist/client/MCPClientUnified.d.ts +0 -31
- package/dist/client/MCPClientUnified.d.ts.map +0 -1
- package/dist/client/MCPClientUnified.js +0 -275
- package/dist/client/MCPClientUnified.js.map +0 -1
- package/dist/client/MCPTestRunnerUnified.d.ts +0 -48
- package/dist/client/MCPTestRunnerUnified.d.ts.map +0 -1
- package/dist/client/MCPTestRunnerUnified.js +0 -183
- package/dist/client/MCPTestRunnerUnified.js.map +0 -1
- package/dist/database/json-manager.d.ts +0 -55
- package/dist/database/json-manager.d.ts.map +0 -1
- package/dist/database/json-manager.js +0 -128
- package/dist/database/json-manager.js.map +0 -1
- package/dist/dynamic-mcp-executor.d.ts.map +0 -1
- package/dist/dynamic-mcp-executor.js +0 -274
- package/dist/dynamic-mcp-executor.js.map +0 -1
- package/dist/generators/MCPServerGenerator-new.d.ts +0 -37
- package/dist/generators/MCPServerGenerator-new.d.ts.map +0 -1
- package/dist/generators/MCPServerGenerator-new.js +0 -287
- package/dist/generators/MCPServerGenerator-new.js.map +0 -1
- package/dist/generators/database/sqlite-manager.d.ts +0 -52
- package/dist/generators/database/sqlite-manager.js +0 -143
- package/dist/generators/generators/MCPServerGenerator.d.ts +0 -37
- package/dist/generators/generators/MCPServerGenerator.js +0 -396
- package/dist/integrated-mcp-server.d.ts +0 -25
- package/dist/integrated-mcp-server.d.ts.map +0 -1
- package/dist/integrated-mcp-server.js +0 -541
- package/dist/integrated-mcp-server.js.map +0 -1
- package/dist/mcp-inspector-server.d.ts +0 -3
- package/dist/mcp-inspector-server.d.ts.map +0 -1
- package/dist/mcp-inspector-server.js +0 -119
- package/dist/mcp-inspector-server.js.map +0 -1
- package/dist/mcp-sdk-server.d.ts +0 -3
- package/dist/mcp-sdk-server.d.ts.map +0 -1
- package/dist/mcp-sdk-server.js +0 -90
- package/dist/mcp-sdk-server.js.map +0 -1
- package/dist/mcp-server.d.ts +0 -3
- package/dist/mcp-server.d.ts.map +0 -1
- package/dist/mcp-server.js +0 -300
- package/dist/mcp-server.js.map +0 -1
- package/dist/parsers/parsers/ExcelParser.js +0 -118
- package/dist/quickmcp-unified-bridge.d.ts +0 -13
- package/dist/quickmcp-unified-bridge.d.ts.map +0 -1
- package/dist/quickmcp-unified-bridge.js +0 -176
- package/dist/quickmcp-unified-bridge.js.map +0 -1
- package/dist/sqlite-manager.js +0 -145
- package/dist/test-app.d.ts +0 -2
- package/dist/test-app.d.ts.map +0 -1
- package/dist/test-app.js +0 -119
- package/dist/test-app.js.map +0 -1
- package/dist/transport/base-transport.d.ts +0 -21
- package/dist/transport/base-transport.d.ts.map +0 -1
- package/dist/transport/base-transport.js +0 -16
- package/dist/transport/base-transport.js.map +0 -1
- package/dist/transport/index.d.ts +0 -10
- package/dist/transport/index.d.ts.map +0 -1
- package/dist/transport/index.js +0 -12
- package/dist/transport/index.js.map +0 -1
- package/dist/transport/sse-transport.d.ts +0 -13
- package/dist/transport/sse-transport.d.ts.map +0 -1
- package/dist/transport/sse-transport.js +0 -106
- package/dist/transport/sse-transport.js.map +0 -1
- package/dist/transport/stdio-transport.d.ts +0 -8
- package/dist/transport/stdio-transport.d.ts.map +0 -1
- package/dist/transport/stdio-transport.js +0 -53
- package/dist/transport/stdio-transport.js.map +0 -1
- package/dist/transport/streamable-http-transport.d.ts +0 -15
- package/dist/transport/streamable-http-transport.d.ts.map +0 -1
- package/dist/transport/streamable-http-transport.js +0 -151
- package/dist/transport/streamable-http-transport.js.map +0 -1
- package/dist/web/client/MCPClient.js +0 -348
- package/dist/web/client/MCPTestRunner.js +0 -317
- package/dist/web/database/json-manager.js +0 -124
- package/dist/web/database/sqlite-manager.js +0 -146
- package/dist/web/dynamic-mcp-executor.js +0 -443
- package/dist/web/generators/MCPServerGenerator-new.js +0 -284
- package/dist/web/generators/MCPServerGenerator.js +0 -566
- package/dist/web/integrated-mcp-server-new.js +0 -394
- package/dist/web/parsers/CsvParser.js +0 -144
- package/dist/web/parsers/DatabaseParser.js +0 -637
- package/dist/web/parsers/ExcelParser.js +0 -180
- package/dist/web/parsers/index.js +0 -152
- package/dist/web/server.d.ts.map +0 -1
- package/dist/web/server.js +0 -790
- package/dist/web/server.js.map +0 -1
- package/dist/web/types/index.js +0 -2
- package/dist/web/web/server.js +0 -860
- package/src/web/public/modern-styles.css +0 -946
- package/src/web/public/shared-styles.css +0 -2091
- /package/src/web/public/images/{1-claude-quickmcp-stdio.png → readme/1-claude-quickmcp-stdio.png} +0 -0
- /package/src/web/public/images/{2-claude-tools.png → readme/2-claude-tools.png} +0 -0
- /package/src/web/public/images/{3-claude-developer-settings.png → readme/3-claude-developer-settings.png} +0 -0
- /package/src/web/public/images/{4-claude-config.png → readme/4-claude-config.png} +0 -0
- /package/src/web/public/images/{5-claude-config-edit.png → readme/5-claude-config-edit.png} +0 -0
|
@@ -0,0 +1,1472 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.AuthApi = void 0;
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
9
|
+
const token_utils_1 = require("../../auth/token-utils");
|
|
10
|
+
const jwks_provider_1 = require("../../auth/jwks-provider");
|
|
11
|
+
const logger_1 = require("../../utils/logger");
|
|
12
|
+
class AuthApi {
|
|
13
|
+
constructor(deps) {
|
|
14
|
+
this.deps = deps;
|
|
15
|
+
this.oauthRequestCookieName = 'quickmcp_oauth_req';
|
|
16
|
+
this.oauthNextCookieName = 'quickmcp_oauth_next';
|
|
17
|
+
this.oauthCodeTtlMs = 5 * 60 * 1000;
|
|
18
|
+
this.oauthAccessTokenTtlSec = Number(process.env.QUICKMCP_OAUTH_ACCESS_TTL_SEC || '3600');
|
|
19
|
+
this.oauthClients = new Map();
|
|
20
|
+
this.getJwks = (_req, res) => {
|
|
21
|
+
res.setHeader('Cache-Control', 'public, max-age=3600');
|
|
22
|
+
res.json((0, jwks_provider_1.getJwks)());
|
|
23
|
+
};
|
|
24
|
+
this.getOAuthAuthorizationServerMetadata = (req, res) => {
|
|
25
|
+
const issuer = this.resolveAppBaseUrl(req).replace(/\/+$/, '');
|
|
26
|
+
res.json({
|
|
27
|
+
issuer,
|
|
28
|
+
authorization_endpoint: `${issuer}/oauth/authorize`,
|
|
29
|
+
token_endpoint: `${issuer}/oauth/token`,
|
|
30
|
+
introspection_endpoint: `${issuer}/oauth/introspect`,
|
|
31
|
+
userinfo_endpoint: `${issuer}/oauth/userinfo`,
|
|
32
|
+
registration_endpoint: `${issuer}/oauth/register`,
|
|
33
|
+
jwks_uri: `${issuer}/oauth/jwks`,
|
|
34
|
+
response_types_supported: ['code'],
|
|
35
|
+
response_modes_supported: ['query'],
|
|
36
|
+
grant_types_supported: ['authorization_code', 'refresh_token'],
|
|
37
|
+
code_challenge_methods_supported: ['S256'],
|
|
38
|
+
scopes_supported: ['openid', 'profile', 'email', 'offline_access', 'mcp'],
|
|
39
|
+
token_endpoint_auth_methods_supported: ['none', 'client_secret_post', 'client_secret_basic']
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
this.getOAuthProtectedResourceMetadata = (req, res) => {
|
|
43
|
+
const issuer = this.resolveAppBaseUrl(req).replace(/\/+$/, '');
|
|
44
|
+
res.json({
|
|
45
|
+
resource: `${issuer}/mcp`,
|
|
46
|
+
authorization_servers: [issuer],
|
|
47
|
+
scopes_supported: ['openid', 'profile', 'email', 'offline_access', 'mcp'],
|
|
48
|
+
jwks_uri: `${issuer}/oauth/jwks`,
|
|
49
|
+
bearer_methods_supported: ['header', 'body'],
|
|
50
|
+
resource_documentation: 'https://www.quickmcp.ai/docs',
|
|
51
|
+
resource_registration: `${issuer}/oauth/register`
|
|
52
|
+
});
|
|
53
|
+
};
|
|
54
|
+
this.getOpenIdConfigurationMetadata = (req, res) => {
|
|
55
|
+
const issuer = this.resolveAppBaseUrl(req).replace(/\/+$/, '');
|
|
56
|
+
res.json({
|
|
57
|
+
issuer,
|
|
58
|
+
authorization_endpoint: `${issuer}/oauth/authorize`,
|
|
59
|
+
token_endpoint: `${issuer}/oauth/token`,
|
|
60
|
+
introspection_endpoint: `${issuer}/oauth/introspect`,
|
|
61
|
+
userinfo_endpoint: `${issuer}/oauth/userinfo`,
|
|
62
|
+
registration_endpoint: `${issuer}/oauth/register`,
|
|
63
|
+
jwks_uri: `${issuer}/oauth/jwks`,
|
|
64
|
+
response_types_supported: ['code'],
|
|
65
|
+
response_modes_supported: ['query'],
|
|
66
|
+
grant_types_supported: ['authorization_code', 'refresh_token'],
|
|
67
|
+
code_challenge_methods_supported: ['S256'],
|
|
68
|
+
scopes_supported: ['openid', 'profile', 'email', 'offline_access', 'mcp'],
|
|
69
|
+
token_endpoint_auth_methods_supported: ['none', 'client_secret_post', 'client_secret_basic'],
|
|
70
|
+
request_uri_parameter_supported: false,
|
|
71
|
+
require_request_uri_registration: false,
|
|
72
|
+
request_parameter_supported: false,
|
|
73
|
+
authorization_response_iss_parameter_supported: true
|
|
74
|
+
});
|
|
75
|
+
};
|
|
76
|
+
this.oauthRegister = (req, res) => {
|
|
77
|
+
const body = (req.body && typeof req.body === 'object') ? req.body : {};
|
|
78
|
+
const redirectUrisRaw = body.redirect_uris;
|
|
79
|
+
const redirectUris = Array.isArray(redirectUrisRaw)
|
|
80
|
+
? redirectUrisRaw.map((v) => String(v || '').trim()).filter(Boolean)
|
|
81
|
+
: [];
|
|
82
|
+
const clientName = String(body.client_name || 'QuickMCP ChatGPT Client').trim();
|
|
83
|
+
if (redirectUris.length === 0) {
|
|
84
|
+
res.status(400).json({ error: 'invalid_client_metadata', error_description: 'redirect_uris is required' });
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (redirectUris.some((uri) => !/^https?:\/\//i.test(uri))) {
|
|
88
|
+
res.status(400).json({ error: 'invalid_redirect_uri', error_description: 'redirect_uris must be absolute URLs' });
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const clientId = `quickmcp_${crypto_1.default.randomBytes(16).toString('hex')}`;
|
|
92
|
+
const createdAt = Math.floor(Date.now() / 1000);
|
|
93
|
+
this.oauthClients.set(clientId, {
|
|
94
|
+
clientId,
|
|
95
|
+
redirectUris,
|
|
96
|
+
clientName,
|
|
97
|
+
tokenEndpointAuthMethod: 'none',
|
|
98
|
+
createdAt
|
|
99
|
+
});
|
|
100
|
+
res.status(201).json({
|
|
101
|
+
client_id: clientId,
|
|
102
|
+
client_id_issued_at: createdAt,
|
|
103
|
+
client_name: clientName,
|
|
104
|
+
redirect_uris: redirectUris,
|
|
105
|
+
grant_types: ['authorization_code', 'refresh_token'],
|
|
106
|
+
response_types: ['code'],
|
|
107
|
+
token_endpoint_auth_method: 'none'
|
|
108
|
+
});
|
|
109
|
+
};
|
|
110
|
+
this.oauthAuthorize = (req, res) => {
|
|
111
|
+
const responseType = String(req.query.response_type || '').trim();
|
|
112
|
+
const clientId = String(req.query.client_id || 'chatgpt').trim();
|
|
113
|
+
const redirectUri = String(req.query.redirect_uri || '').trim();
|
|
114
|
+
const state = String(req.query.state || '').trim();
|
|
115
|
+
const scope = String(req.query.scope || '').trim();
|
|
116
|
+
const canonicalResource = `${this.resolveAppBaseUrl(req).replace(/\/+$/, '')}/mcp`;
|
|
117
|
+
const requestedResource = String(req.query.resource || '').trim();
|
|
118
|
+
// Force canonical resource host to prevent quickmcp.ai vs www.quickmcp.ai token-binding drift.
|
|
119
|
+
const resource = canonicalResource;
|
|
120
|
+
const codeChallenge = String(req.query.code_challenge || '').trim();
|
|
121
|
+
const codeChallengeMethod = this.normalizeOAuthCodeChallengeMethod(String(req.query.code_challenge_method || 'S256'));
|
|
122
|
+
const nonce = String(req.query.nonce || '').trim() || undefined;
|
|
123
|
+
if (responseType !== 'code') {
|
|
124
|
+
if (redirectUri) {
|
|
125
|
+
this.redirectOAuthError(res, redirectUri, state, 'unsupported_response_type', 'response_type must be code');
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
res.status(400).json({ error: 'unsupported_response_type', error_description: 'response_type must be code' });
|
|
129
|
+
}
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (!redirectUri) {
|
|
133
|
+
res.status(400).json({ error: 'invalid_request', error_description: 'redirect_uri is required' });
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (clientId) {
|
|
137
|
+
const registeredClient = this.oauthClients.get(clientId);
|
|
138
|
+
if (registeredClient && !registeredClient.redirectUris.includes(redirectUri)) {
|
|
139
|
+
this.redirectOAuthError(res, redirectUri, state, 'invalid_request', 'redirect_uri is not registered for client_id');
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (!codeChallenge) {
|
|
144
|
+
this.redirectOAuthError(res, redirectUri, state, 'invalid_request', 'code_challenge is required');
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const pending = {
|
|
148
|
+
clientId,
|
|
149
|
+
redirectUri,
|
|
150
|
+
state,
|
|
151
|
+
scope,
|
|
152
|
+
resource,
|
|
153
|
+
codeChallenge,
|
|
154
|
+
codeChallengeMethod,
|
|
155
|
+
nonce,
|
|
156
|
+
createdAt: Date.now()
|
|
157
|
+
};
|
|
158
|
+
if (requestedResource && requestedResource !== resource) {
|
|
159
|
+
logger_1.logger.info(`[oauth/authorize] normalize resource requested=${requestedResource} canonical=${resource}`);
|
|
160
|
+
}
|
|
161
|
+
const encoded = this.encodeOAuthPendingRequest(pending);
|
|
162
|
+
this.deps.setCookie(res, this.oauthRequestCookieName, encoded, 10 * 60);
|
|
163
|
+
res.redirect('/oauth/authorize/complete');
|
|
164
|
+
};
|
|
165
|
+
this.oauthAuthorizeComplete = async (req, res) => {
|
|
166
|
+
const cookies = this.deps.parseCookies(req);
|
|
167
|
+
const rawPending = cookies[this.oauthRequestCookieName];
|
|
168
|
+
const pending = this.decodeOAuthPendingRequest(rawPending || '');
|
|
169
|
+
if (!pending || !pending.redirectUri) {
|
|
170
|
+
res.status(400).json({ error: 'invalid_request', error_description: 'Missing or invalid OAuth authorization request' });
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (Date.now() - pending.createdAt > 10 * 60 * 1000) {
|
|
174
|
+
this.deps.clearCookie(res, this.oauthRequestCookieName);
|
|
175
|
+
this.redirectOAuthError(res, pending.redirectUri, pending.state, 'invalid_request', 'OAuth authorization request expired');
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const ctx = await this.deps.resolveAuthContext(req, res);
|
|
179
|
+
if (!ctx) {
|
|
180
|
+
const next = '/oauth/authorize/complete';
|
|
181
|
+
res.redirect(`/login?next=${encodeURIComponent(next)}`);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const code = this.createAuthorizationCode({
|
|
185
|
+
clientId: pending.clientId,
|
|
186
|
+
redirectUri: pending.redirectUri,
|
|
187
|
+
scope: pending.scope,
|
|
188
|
+
resource: pending.resource,
|
|
189
|
+
codeChallenge: pending.codeChallenge,
|
|
190
|
+
codeChallengeMethod: pending.codeChallengeMethod,
|
|
191
|
+
nonce: pending.nonce,
|
|
192
|
+
username: ctx.username,
|
|
193
|
+
workspaceId: ctx.workspaceId,
|
|
194
|
+
role: ctx.role
|
|
195
|
+
});
|
|
196
|
+
this.deps.clearCookie(res, this.oauthRequestCookieName);
|
|
197
|
+
let location = this.appendQueryParam(pending.redirectUri, 'code', code);
|
|
198
|
+
if (pending.state)
|
|
199
|
+
location = this.appendQueryParam(location, 'state', pending.state);
|
|
200
|
+
res.redirect(location);
|
|
201
|
+
};
|
|
202
|
+
this.oauthToken = async (req, res) => {
|
|
203
|
+
const body = (req.body && typeof req.body === 'object') ? req.body : {};
|
|
204
|
+
const grantType = String(body.grant_type || '').trim();
|
|
205
|
+
const code = String(body.code || '').trim();
|
|
206
|
+
const redirectUri = String(body.redirect_uri || '').trim();
|
|
207
|
+
const clientId = String(body.client_id || '').trim();
|
|
208
|
+
const codeVerifier = String(body.code_verifier || '').trim();
|
|
209
|
+
const requestId = crypto_1.default.randomBytes(6).toString('hex');
|
|
210
|
+
logger_1.logger.info(`[oauth/token:${requestId}] headers=${JSON.stringify(req.headers)}`);
|
|
211
|
+
logger_1.logger.info(`[oauth/token:${requestId}] body=${JSON.stringify(body)}`);
|
|
212
|
+
logger_1.logger.info(`[oauth/token:${requestId}] request grant_type=${grantType || '(empty)'} client_id=${clientId || '(empty)'}`);
|
|
213
|
+
// refresh_token grant: look up the token and issue a new access token
|
|
214
|
+
if (grantType === 'refresh_token') {
|
|
215
|
+
const refreshTokenValue = String(body.refresh_token || '').trim();
|
|
216
|
+
if (!refreshTokenValue) {
|
|
217
|
+
res.status(400).json({ error: 'invalid_request', error_description: 'refresh_token is required' });
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
try {
|
|
221
|
+
const store = this.deps.ensureDataStore();
|
|
222
|
+
const refreshHash = crypto_1.default.createHash('sha256').update(refreshTokenValue).digest('hex');
|
|
223
|
+
const existingToken = await store.getMcpTokenByHash(refreshHash);
|
|
224
|
+
if (!existingToken || existingToken.revokedAt) {
|
|
225
|
+
logger_1.logger.warn(`[oauth/token:${requestId}] refresh_token invalid or revoked`);
|
|
226
|
+
res.status(400).json({ error: 'invalid_grant', error_description: 'refresh_token is invalid or revoked' });
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const refreshTtlSec = Number.isFinite(this.oauthAccessTokenTtlSec) && this.oauthAccessTokenTtlSec > 0
|
|
230
|
+
? Math.floor(this.oauthAccessTokenTtlSec)
|
|
231
|
+
: 3600;
|
|
232
|
+
const resourceUrl = `${this.resolveAppBaseUrl(req).replace(/\/+$/, '')}/mcp`;
|
|
233
|
+
const issuerUrl = this.resolveAppBaseUrl(req).replace(/\/+$/, '');
|
|
234
|
+
const newTokenPack = this.deps.createMcpToken(existingToken.subjectUsername, existingToken.workspaceId, this.deps.getUserRole(existingToken.subjectUsername, existingToken.workspaceId), refreshTtlSec, resourceUrl, issuerUrl, 'mcp');
|
|
235
|
+
await store.createMcpToken({
|
|
236
|
+
id: newTokenPack.tokenId,
|
|
237
|
+
tokenName: `oauth:refresh:${clientId || 'chatgpt'}`,
|
|
238
|
+
workspaceId: existingToken.workspaceId,
|
|
239
|
+
subjectUsername: existingToken.subjectUsername,
|
|
240
|
+
createdBy: existingToken.subjectUsername,
|
|
241
|
+
tokenHash: newTokenPack.tokenHash,
|
|
242
|
+
tokenValue: newTokenPack.token,
|
|
243
|
+
allowAllServers: true,
|
|
244
|
+
allowAllTools: true,
|
|
245
|
+
allowAllResources: true,
|
|
246
|
+
serverIds: [],
|
|
247
|
+
allowedTools: [],
|
|
248
|
+
allowedResources: [],
|
|
249
|
+
serverRules: {},
|
|
250
|
+
toolRules: {},
|
|
251
|
+
resourceRules: {},
|
|
252
|
+
neverExpires: false,
|
|
253
|
+
expiresAt: newTokenPack.expiresAt
|
|
254
|
+
});
|
|
255
|
+
const newRefreshToken = crypto_1.default.randomBytes(48).toString('base64url');
|
|
256
|
+
const newRefreshHash = crypto_1.default.createHash('sha256').update(newRefreshToken).digest('hex');
|
|
257
|
+
await store.createMcpToken({
|
|
258
|
+
id: crypto_1.default.randomUUID(),
|
|
259
|
+
tokenName: `oauth:refresh-token:${clientId || 'chatgpt'}`,
|
|
260
|
+
workspaceId: existingToken.workspaceId,
|
|
261
|
+
subjectUsername: existingToken.subjectUsername,
|
|
262
|
+
createdBy: existingToken.subjectUsername,
|
|
263
|
+
tokenHash: newRefreshHash,
|
|
264
|
+
tokenValue: newRefreshToken,
|
|
265
|
+
allowAllServers: false,
|
|
266
|
+
allowAllTools: false,
|
|
267
|
+
allowAllResources: false,
|
|
268
|
+
serverIds: [],
|
|
269
|
+
allowedTools: [],
|
|
270
|
+
allowedResources: [],
|
|
271
|
+
serverRules: {},
|
|
272
|
+
toolRules: {},
|
|
273
|
+
resourceRules: {},
|
|
274
|
+
neverExpires: true,
|
|
275
|
+
expiresAt: null
|
|
276
|
+
});
|
|
277
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
278
|
+
res.setHeader('Pragma', 'no-cache');
|
|
279
|
+
logger_1.logger.info(`[oauth/token:${requestId}] refresh success user=${existingToken.subjectUsername}`);
|
|
280
|
+
const refreshIdToken = this.createIdToken({ sub: existingToken.subjectUsername, issuer: issuerUrl, clientId: clientId || 'chatgpt', ttlSec: refreshTtlSec });
|
|
281
|
+
const refreshResponse = {
|
|
282
|
+
access_token: newTokenPack.token,
|
|
283
|
+
token_type: 'Bearer',
|
|
284
|
+
expires_in: refreshTtlSec,
|
|
285
|
+
refresh_token: newRefreshToken,
|
|
286
|
+
id_token: refreshIdToken,
|
|
287
|
+
scope: 'mcp openid',
|
|
288
|
+
resource: resourceUrl
|
|
289
|
+
};
|
|
290
|
+
logger_1.logger.info(`[oauth/token:${requestId}] refresh response token_type=${refreshResponse.token_type} scope="${refreshResponse.scope}" resource="${refreshResponse.resource}" access_token=${this.maskTokenForLog(refreshResponse.access_token)} refresh_token=${this.maskTokenForLog(refreshResponse.refresh_token)} id_token=${this.maskTokenForLog(refreshResponse.id_token)}`);
|
|
291
|
+
res.json(refreshResponse);
|
|
292
|
+
}
|
|
293
|
+
catch (err) {
|
|
294
|
+
logger_1.logger.warn(`[oauth/token:${requestId}] refresh_token error: ${err instanceof Error ? err.message : String(err)}`);
|
|
295
|
+
res.status(400).json({ error: 'invalid_grant', error_description: 'refresh_token processing failed' });
|
|
296
|
+
}
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if (grantType !== 'authorization_code') {
|
|
300
|
+
logger_1.logger.warn(`[oauth/token:${requestId}] rejected unsupported grant_type`);
|
|
301
|
+
res.status(400).json({ error: 'unsupported_grant_type', error_description: 'grant_type must be authorization_code or refresh_token' });
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (!code) {
|
|
305
|
+
logger_1.logger.warn(`[oauth/token:${requestId}] rejected missing code`);
|
|
306
|
+
res.status(400).json({ error: 'invalid_request', error_description: 'code is required' });
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
const record = this.decodeOAuthCode(code);
|
|
310
|
+
if (!record || record.expiresAt <= Date.now()) {
|
|
311
|
+
logger_1.logger.warn(`[oauth/token:${requestId}] invalid or expired code`);
|
|
312
|
+
res.status(400).json({ error: 'invalid_grant', error_description: 'authorization code is invalid or expired' });
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
if (redirectUri && this.normalizeRedirectUri(record.redirectUri) !== this.normalizeRedirectUri(redirectUri)) {
|
|
316
|
+
logger_1.logger.warn(`[oauth/token:${requestId}] redirect mismatch expected=${record.redirectUri} got=${redirectUri}`);
|
|
317
|
+
res.status(400).json({ error: 'invalid_grant', error_description: 'redirect_uri does not match authorization request' });
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
if (clientId && record.clientId && record.clientId !== clientId) {
|
|
321
|
+
// Some clients can rotate/re-register IDs between registration and callback retries.
|
|
322
|
+
// Do not hard-fail here; rely on redirect_uri + PKCE for validation.
|
|
323
|
+
logger_1.logger.warn(`[oauth/token:${requestId}] client_id mismatch expected=${record.clientId} got=${clientId}`);
|
|
324
|
+
}
|
|
325
|
+
if (record.codeChallenge) {
|
|
326
|
+
if (!codeVerifier) {
|
|
327
|
+
logger_1.logger.warn(`[oauth/token:${requestId}] missing code_verifier`);
|
|
328
|
+
res.status(400).json({ error: 'invalid_request', error_description: 'code_verifier is required' });
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
const computed = record.codeChallengeMethod === 'plain'
|
|
332
|
+
? codeVerifier
|
|
333
|
+
: crypto_1.default.createHash('sha256').update(codeVerifier).digest('base64url');
|
|
334
|
+
if (computed !== record.codeChallenge) {
|
|
335
|
+
logger_1.logger.warn(`[oauth/token:${requestId}] invalid code_verifier`);
|
|
336
|
+
res.status(400).json({ error: 'invalid_grant', error_description: 'code_verifier is invalid' });
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
const ttlSec = Number.isFinite(this.oauthAccessTokenTtlSec) && this.oauthAccessTokenTtlSec > 0
|
|
341
|
+
? Math.floor(this.oauthAccessTokenTtlSec)
|
|
342
|
+
: 3600;
|
|
343
|
+
const resourceUrl = record.resource || `${this.resolveAppBaseUrl(req).replace(/\/+$/, '')}/mcp`;
|
|
344
|
+
const issuerUrl = this.resolveAppBaseUrl(req).replace(/\/+$/, '');
|
|
345
|
+
const tokenPack = this.deps.createMcpToken(record.username, record.workspaceId, record.role, ttlSec, resourceUrl, issuerUrl, record.scope || 'mcp');
|
|
346
|
+
// Generate a refresh token (stored as a special token record in the DB)
|
|
347
|
+
const refreshToken = crypto_1.default.randomBytes(48).toString('base64url');
|
|
348
|
+
const refreshTokenHash = crypto_1.default.createHash('sha256').update(refreshToken).digest('hex');
|
|
349
|
+
try {
|
|
350
|
+
const store = this.deps.ensureDataStore();
|
|
351
|
+
await store.createMcpToken({
|
|
352
|
+
id: tokenPack.tokenId,
|
|
353
|
+
tokenName: `oauth:${clientId || 'chatgpt'}`,
|
|
354
|
+
workspaceId: record.workspaceId,
|
|
355
|
+
subjectUsername: record.username,
|
|
356
|
+
createdBy: record.username,
|
|
357
|
+
tokenHash: tokenPack.tokenHash,
|
|
358
|
+
tokenValue: tokenPack.token,
|
|
359
|
+
allowAllServers: true,
|
|
360
|
+
allowAllTools: true,
|
|
361
|
+
allowAllResources: true,
|
|
362
|
+
serverIds: [],
|
|
363
|
+
allowedTools: [],
|
|
364
|
+
allowedResources: [],
|
|
365
|
+
serverRules: {},
|
|
366
|
+
toolRules: {},
|
|
367
|
+
resourceRules: {},
|
|
368
|
+
neverExpires: false,
|
|
369
|
+
expiresAt: tokenPack.expiresAt
|
|
370
|
+
});
|
|
371
|
+
// Store refresh token as a long-lived record so it can be looked up later
|
|
372
|
+
await store.createMcpToken({
|
|
373
|
+
id: crypto_1.default.randomUUID(),
|
|
374
|
+
tokenName: `oauth:refresh-token:${clientId || 'chatgpt'}`,
|
|
375
|
+
workspaceId: record.workspaceId,
|
|
376
|
+
subjectUsername: record.username,
|
|
377
|
+
createdBy: record.username,
|
|
378
|
+
tokenHash: refreshTokenHash,
|
|
379
|
+
tokenValue: refreshToken,
|
|
380
|
+
allowAllServers: false,
|
|
381
|
+
allowAllTools: false,
|
|
382
|
+
allowAllResources: false,
|
|
383
|
+
serverIds: [],
|
|
384
|
+
allowedTools: [],
|
|
385
|
+
allowedResources: [],
|
|
386
|
+
serverRules: {},
|
|
387
|
+
toolRules: {},
|
|
388
|
+
resourceRules: {},
|
|
389
|
+
neverExpires: true,
|
|
390
|
+
expiresAt: null
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
catch (err) {
|
|
394
|
+
logger_1.logger.warn(`[oauth/token:${requestId}] failed to persist token: ${err instanceof Error ? err.message : String(err)}`);
|
|
395
|
+
}
|
|
396
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
397
|
+
res.setHeader('Pragma', 'no-cache');
|
|
398
|
+
logger_1.logger.info(`[oauth/token:${requestId}] success user=${record.username} ws=${record.workspaceId}`);
|
|
399
|
+
const grantScopes = (record.scope || '').split(/\s+/);
|
|
400
|
+
const idToken = grantScopes.includes('openid')
|
|
401
|
+
? this.createIdToken({ sub: record.username, issuer: issuerUrl, clientId: clientId || record.clientId, ttlSec, nonce: record.nonce })
|
|
402
|
+
: undefined;
|
|
403
|
+
const responseScope = [...new Set([...grantScopes, 'openid'])].join(' ');
|
|
404
|
+
const tokenResponse = {
|
|
405
|
+
access_token: tokenPack.token,
|
|
406
|
+
token_type: 'Bearer',
|
|
407
|
+
expires_in: ttlSec,
|
|
408
|
+
refresh_token: refreshToken,
|
|
409
|
+
...(idToken ? { id_token: idToken } : {}),
|
|
410
|
+
scope: responseScope,
|
|
411
|
+
resource: resourceUrl
|
|
412
|
+
};
|
|
413
|
+
logger_1.logger.info(`[oauth/token:${requestId}] response token_type=${tokenResponse.token_type} scope="${tokenResponse.scope}" resource="${tokenResponse.resource}" access_token=${this.maskTokenForLog(tokenResponse.access_token)} refresh_token=${this.maskTokenForLog(tokenResponse.refresh_token)} id_token=${this.maskTokenForLog(tokenResponse.id_token)}`);
|
|
414
|
+
res.json(tokenResponse);
|
|
415
|
+
};
|
|
416
|
+
// RFC 7662 Token Introspection
|
|
417
|
+
this.oauthIntrospect = async (req, res) => {
|
|
418
|
+
const body = (req.body && typeof req.body === 'object') ? req.body : {};
|
|
419
|
+
const token = String(body.token || '').trim();
|
|
420
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
421
|
+
if (!token) {
|
|
422
|
+
res.json({ active: false });
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
try {
|
|
426
|
+
const tokenSecret = String(process.env.QUICKMCP_TOKEN_SECRET || process.env.AUTH_COOKIE_SECRET || 'change-me');
|
|
427
|
+
const rsaPublicKey = (0, jwks_provider_1.getRsaPublicKey)();
|
|
428
|
+
const payload = (rsaPublicKey ? (0, token_utils_1.verifyMcpTokenRS256)(token, rsaPublicKey) : null)
|
|
429
|
+
?? (0, token_utils_1.verifyMcpToken)(token, tokenSecret);
|
|
430
|
+
if (!payload) {
|
|
431
|
+
res.json({ active: false });
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
const issuer = this.resolveAppBaseUrl(req).replace(/\/+$/, '');
|
|
435
|
+
res.json({
|
|
436
|
+
active: true,
|
|
437
|
+
sub: payload.sub,
|
|
438
|
+
scope: payload.scope || 'mcp openid',
|
|
439
|
+
username: payload.sub,
|
|
440
|
+
token_type: 'Bearer',
|
|
441
|
+
exp: payload.exp,
|
|
442
|
+
iat: payload.iat,
|
|
443
|
+
iss: payload.iss || issuer,
|
|
444
|
+
aud: payload.aud,
|
|
445
|
+
jti: payload.jti
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
catch {
|
|
449
|
+
res.json({ active: false });
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
// OIDC UserInfo endpoint (RFC 7517)
|
|
453
|
+
this.oauthUserinfo = async (req, res) => {
|
|
454
|
+
const authHeader = String(req.headers.authorization || '').trim();
|
|
455
|
+
const bearerMatch = authHeader.match(/^Bearer\s+(.+)$/i);
|
|
456
|
+
const token = bearerMatch ? bearerMatch[1].trim() : '';
|
|
457
|
+
if (!token) {
|
|
458
|
+
res.setHeader('WWW-Authenticate', 'Bearer error="invalid_token"');
|
|
459
|
+
res.status(401).json({ error: 'invalid_token', error_description: 'Bearer token required' });
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
try {
|
|
463
|
+
const tokenSecret = String(process.env.QUICKMCP_TOKEN_SECRET || process.env.AUTH_COOKIE_SECRET || 'change-me');
|
|
464
|
+
const rsaPublicKey = (0, jwks_provider_1.getRsaPublicKey)();
|
|
465
|
+
const payload = (rsaPublicKey ? (0, token_utils_1.verifyMcpTokenRS256)(token, rsaPublicKey) : null)
|
|
466
|
+
?? (0, token_utils_1.verifyMcpToken)(token, tokenSecret);
|
|
467
|
+
if (!payload) {
|
|
468
|
+
res.setHeader('WWW-Authenticate', 'Bearer error="invalid_token"');
|
|
469
|
+
res.status(401).json({ error: 'invalid_token', error_description: 'Token is invalid or expired' });
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
res.json({
|
|
473
|
+
sub: payload.sub,
|
|
474
|
+
name: payload.sub,
|
|
475
|
+
preferred_username: payload.sub,
|
|
476
|
+
profile: `${this.resolveAppBaseUrl(req).replace(/\/+$/, '')}/users/${encodeURIComponent(payload.sub)}`
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
catch {
|
|
480
|
+
res.status(500).json({ error: 'server_error' });
|
|
481
|
+
}
|
|
482
|
+
};
|
|
483
|
+
this.getMe = async (req, res) => {
|
|
484
|
+
const ctx = await this.deps.resolveAuthContext(req, res);
|
|
485
|
+
if (!ctx) {
|
|
486
|
+
res.status(401).json({ success: false, error: 'Unauthorized' });
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
const cookies = this.deps.parseCookies(req);
|
|
490
|
+
const accessToken = cookies[this.deps.accessCookieName];
|
|
491
|
+
const payload = accessToken
|
|
492
|
+
? ((0, token_utils_1.verifyAccessToken)(accessToken, this.deps.authCookieSecret)
|
|
493
|
+
|| (0, token_utils_1.verifyAccessTokenAllowExpired)(accessToken, this.deps.authCookieSecret))
|
|
494
|
+
: null;
|
|
495
|
+
let storeUser = null;
|
|
496
|
+
try {
|
|
497
|
+
storeUser = await this.deps.ensureDataStore().getUser(ctx.username);
|
|
498
|
+
}
|
|
499
|
+
catch { }
|
|
500
|
+
const createdDate = payload?.createdDate || storeUser?.createdAt || '';
|
|
501
|
+
const lastSignInDate = payload?.lastSignInDate || (payload?.iat ? new Date(payload.iat * 1000).toISOString() : '');
|
|
502
|
+
res.json({
|
|
503
|
+
success: true,
|
|
504
|
+
data: {
|
|
505
|
+
username: ctx.username,
|
|
506
|
+
displayName: payload?.displayName || ctx.username,
|
|
507
|
+
workspaceId: ctx.workspaceId,
|
|
508
|
+
authMode: this.deps.authMode,
|
|
509
|
+
email: payload?.email || '',
|
|
510
|
+
avatarUrl: payload?.avatarUrl || '',
|
|
511
|
+
createdDate,
|
|
512
|
+
lastSignInDate,
|
|
513
|
+
role: ctx.role,
|
|
514
|
+
isAdmin: ctx.role === 'admin'
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
};
|
|
518
|
+
this.getRoles = async (req, res) => {
|
|
519
|
+
const ctx = await this.deps.resolveAuthContext(req, res);
|
|
520
|
+
if (!ctx) {
|
|
521
|
+
res.status(401).json({ success: false, error: 'Unauthorized' });
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
res.json({
|
|
525
|
+
success: true,
|
|
526
|
+
data: {
|
|
527
|
+
roles: [
|
|
528
|
+
{ id: 'admin', name: 'Admin', description: 'Can manage users and all servers.' },
|
|
529
|
+
{ id: 'user', name: 'User', description: 'Can use the app and manage own servers.' }
|
|
530
|
+
],
|
|
531
|
+
currentUser: { username: ctx.username, workspaceId: ctx.workspaceId, role: ctx.role, isAdmin: ctx.role === 'admin' }
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
};
|
|
535
|
+
this.getUsers = async (req, res) => {
|
|
536
|
+
if (this.isSaasMode()) {
|
|
537
|
+
res.status(404).json({ success: false, error: 'Users API is not available in SAAS mode' });
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
const actor = await this.deps.requireAdminApi(req, res);
|
|
541
|
+
if (!actor)
|
|
542
|
+
return;
|
|
543
|
+
try {
|
|
544
|
+
const users = await this.deps.ensureDataStore().getAllUsersByWorkspace(actor.workspaceId);
|
|
545
|
+
res.json({ success: true, data: { users, requestedBy: actor.username, workspaceId: actor.workspaceId } });
|
|
546
|
+
}
|
|
547
|
+
catch (error) {
|
|
548
|
+
res.status(500).json({ success: false, error: error instanceof Error ? error.message : 'Failed to load users' });
|
|
549
|
+
}
|
|
550
|
+
};
|
|
551
|
+
this.getAuthorizationConfig = async (req, res) => {
|
|
552
|
+
const ctx = await this.deps.resolveAuthContext(req, res);
|
|
553
|
+
if (!ctx) {
|
|
554
|
+
res.status(401).json({ success: false, error: 'Unauthorized' });
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
const isSaasMode = this.isSaasMode();
|
|
558
|
+
res.json({
|
|
559
|
+
success: true,
|
|
560
|
+
data: {
|
|
561
|
+
authMode: this.deps.authMode,
|
|
562
|
+
deployMode: String(this.deps.deployMode || '').trim().toUpperCase(),
|
|
563
|
+
isSaasMode,
|
|
564
|
+
mcpTokenRequired: this.deps.authMode !== 'NONE',
|
|
565
|
+
username: ctx.username,
|
|
566
|
+
workspaceId: ctx.workspaceId
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
};
|
|
570
|
+
this.getAuthorizationContext = async (req, res) => {
|
|
571
|
+
const ctx = await this.deps.resolveAuthContext(req, res);
|
|
572
|
+
if (!ctx) {
|
|
573
|
+
res.status(401).json({ success: false, error: 'Unauthorized' });
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
try {
|
|
577
|
+
const store = this.deps.ensureDataStore();
|
|
578
|
+
const workspaceUsers = await store.getAllUsersByWorkspace(ctx.workspaceId);
|
|
579
|
+
const canManageWorkspace = ctx.role === 'admin' || this.isSaasMode();
|
|
580
|
+
const users = (canManageWorkspace
|
|
581
|
+
? workspaceUsers
|
|
582
|
+
: workspaceUsers.filter((u) => u.username === ctx.username)).map((u) => ({ username: u.username, role: u.role }));
|
|
583
|
+
const serverRows = await store.getAllServersByOwner(ctx.workspaceId);
|
|
584
|
+
const servers = await Promise.all(serverRows.map(async (server) => {
|
|
585
|
+
const tools = (await store.getToolsForServer(server.id)).map((t) => `${server.id}__${t.name}`);
|
|
586
|
+
const resources = (await store.getResourcesForServer(server.id)).map((r) => `${server.id}__${r.name}`);
|
|
587
|
+
const rawType = server.sourceConfig?.type;
|
|
588
|
+
const type = typeof rawType === 'string' ? rawType : 'unknown';
|
|
589
|
+
return { id: server.id, name: server.name, type, tools, resources };
|
|
590
|
+
}));
|
|
591
|
+
res.json({ success: true, data: { workspaceId: ctx.workspaceId, users, servers } });
|
|
592
|
+
}
|
|
593
|
+
catch (error) {
|
|
594
|
+
res.status(500).json({ success: false, error: error instanceof Error ? error.message : 'Failed to load authorization context' });
|
|
595
|
+
}
|
|
596
|
+
};
|
|
597
|
+
this.getAuthorizationServers = async (req, res) => {
|
|
598
|
+
const ctx = await this.deps.resolveAuthContext(req, res);
|
|
599
|
+
if (!ctx) {
|
|
600
|
+
res.status(401).json({ success: false, error: 'Unauthorized' });
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
try {
|
|
604
|
+
const store = this.deps.ensureDataStore();
|
|
605
|
+
const servers = await store.getAllServersByOwner(ctx.workspaceId);
|
|
606
|
+
const data = await Promise.all(servers.map(async (server) => {
|
|
607
|
+
const cfg = await store.getServerAuthConfig(server.id);
|
|
608
|
+
return {
|
|
609
|
+
id: server.id,
|
|
610
|
+
name: server.name,
|
|
611
|
+
ownerUsername: server.ownerUsername,
|
|
612
|
+
requireMcpToken: cfg ? cfg.requireMcpToken : true
|
|
613
|
+
};
|
|
614
|
+
}));
|
|
615
|
+
res.json({ success: true, data: { servers: data } });
|
|
616
|
+
}
|
|
617
|
+
catch (error) {
|
|
618
|
+
res.status(500).json({ success: false, error: error instanceof Error ? error.message : 'Failed to load authorization settings' });
|
|
619
|
+
}
|
|
620
|
+
};
|
|
621
|
+
this.getAuthorizationTokenPolicy = async (req, res) => {
|
|
622
|
+
const actor = this.isSaasMode()
|
|
623
|
+
? await this.deps.resolveAuthContext(req, res)
|
|
624
|
+
: await this.deps.requireAdminApi(req, res);
|
|
625
|
+
if (this.isSaasMode() && !actor) {
|
|
626
|
+
res.status(401).json({ success: false, error: 'Unauthorized' });
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
if (!actor)
|
|
630
|
+
return;
|
|
631
|
+
try {
|
|
632
|
+
const store = this.deps.ensureDataStore();
|
|
633
|
+
const users = (await store.getAllUsersByWorkspace(actor.workspaceId)).map((u) => u.username);
|
|
634
|
+
const servers = (await store.getAllServersByOwner(actor.workspaceId)).map((s) => s.id);
|
|
635
|
+
const toolsByServer = await Promise.all(servers.map((serverId) => store.getToolsForServer(serverId)));
|
|
636
|
+
const tools = servers.flatMap((serverId, index) => toolsByServer[index].map((t) => `${serverId}__${t.name}`));
|
|
637
|
+
const globalPolicy = await store.getMcpTokenPolicy('global', '*');
|
|
638
|
+
const userPolicies = await store.listMcpTokenPolicies('user');
|
|
639
|
+
const serverPolicies = await store.listMcpTokenPolicies('server');
|
|
640
|
+
const toolPolicies = await store.listMcpTokenPolicies('tool');
|
|
641
|
+
const userRules = {};
|
|
642
|
+
const serverRules = {};
|
|
643
|
+
const toolRules = {};
|
|
644
|
+
for (const username of users)
|
|
645
|
+
userRules[username] = null;
|
|
646
|
+
for (const id of servers)
|
|
647
|
+
serverRules[id] = null;
|
|
648
|
+
for (const id of tools)
|
|
649
|
+
toolRules[id] = null;
|
|
650
|
+
userPolicies
|
|
651
|
+
.filter((p) => users.includes(p.scopeId))
|
|
652
|
+
.forEach((p) => { userRules[p.scopeId] = p.requireMcpToken; });
|
|
653
|
+
serverPolicies
|
|
654
|
+
.filter((p) => p.scopeId.startsWith(`${actor.workspaceId}__`))
|
|
655
|
+
.forEach((p) => { serverRules[p.scopeId] = p.requireMcpToken; });
|
|
656
|
+
toolPolicies
|
|
657
|
+
.filter((p) => p.scopeId.startsWith(`${actor.workspaceId}__`))
|
|
658
|
+
.forEach((p) => { toolRules[p.scopeId] = p.requireMcpToken; });
|
|
659
|
+
res.json({
|
|
660
|
+
success: true,
|
|
661
|
+
data: {
|
|
662
|
+
globalRequireMcpToken: globalPolicy ? globalPolicy.requireMcpToken : true,
|
|
663
|
+
userRules,
|
|
664
|
+
serverRules,
|
|
665
|
+
toolRules
|
|
666
|
+
}
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
catch (error) {
|
|
670
|
+
res.status(500).json({ success: false, error: error instanceof Error ? error.message : 'Failed to load token policy' });
|
|
671
|
+
}
|
|
672
|
+
};
|
|
673
|
+
this.updateAuthorizationTokenPolicy = async (req, res) => {
|
|
674
|
+
const actor = this.isSaasMode()
|
|
675
|
+
? await this.deps.resolveAuthContext(req, res)
|
|
676
|
+
: await this.deps.requireAdminApi(req, res);
|
|
677
|
+
if (this.isSaasMode() && !actor) {
|
|
678
|
+
res.status(401).json({ success: false, error: 'Unauthorized' });
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
if (!actor)
|
|
682
|
+
return;
|
|
683
|
+
try {
|
|
684
|
+
const store = this.deps.ensureDataStore();
|
|
685
|
+
const users = (await store.getAllUsersByWorkspace(actor.workspaceId)).map((u) => u.username);
|
|
686
|
+
const usersSet = new Set(users);
|
|
687
|
+
const globalRequireMcpToken = req.body?.globalRequireMcpToken !== false;
|
|
688
|
+
const userRules = this.normalizePolicyMap(req.body?.userRules, 'user', actor.workspaceId, usersSet);
|
|
689
|
+
const serverRules = this.normalizePolicyMap(req.body?.serverRules, 'server', actor.workspaceId, usersSet);
|
|
690
|
+
const toolRules = this.normalizePolicyMap(req.body?.toolRules, 'tool', actor.workspaceId, usersSet);
|
|
691
|
+
await store.setMcpTokenPolicy('global', '*', globalRequireMcpToken);
|
|
692
|
+
await this.applyPolicyMap(store, 'user', userRules);
|
|
693
|
+
await this.applyPolicyMap(store, 'server', serverRules);
|
|
694
|
+
await this.applyPolicyMap(store, 'tool', toolRules);
|
|
695
|
+
res.json({
|
|
696
|
+
success: true,
|
|
697
|
+
data: {
|
|
698
|
+
globalRequireMcpToken,
|
|
699
|
+
userRules,
|
|
700
|
+
serverRules,
|
|
701
|
+
toolRules
|
|
702
|
+
}
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
catch (error) {
|
|
706
|
+
res.status(500).json({ success: false, error: error instanceof Error ? error.message : 'Failed to update token policy' });
|
|
707
|
+
}
|
|
708
|
+
};
|
|
709
|
+
this.updateAuthorizationServer = async (req, res) => {
|
|
710
|
+
const ctx = await this.deps.resolveAuthContext(req, res);
|
|
711
|
+
if (!ctx) {
|
|
712
|
+
res.status(401).json({ success: false, error: 'Unauthorized' });
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
const serverId = String(req.params.id || '').trim();
|
|
716
|
+
const requireMcpToken = req.body?.requireMcpToken !== false;
|
|
717
|
+
if (!serverId) {
|
|
718
|
+
res.status(400).json({ success: false, error: 'Server id is required' });
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
try {
|
|
722
|
+
const store = this.deps.ensureDataStore();
|
|
723
|
+
const server = await store.getServerForOwner(serverId, ctx.workspaceId);
|
|
724
|
+
if (!server) {
|
|
725
|
+
res.status(404).json({ success: false, error: 'Server not found for current user' });
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
await store.setServerAuthConfig(serverId, requireMcpToken);
|
|
729
|
+
res.json({ success: true, data: { serverId, requireMcpToken } });
|
|
730
|
+
}
|
|
731
|
+
catch (error) {
|
|
732
|
+
res.status(500).json({ success: false, error: error instanceof Error ? error.message : 'Failed to update authorization settings' });
|
|
733
|
+
}
|
|
734
|
+
};
|
|
735
|
+
this.createAuthorizationMcpToken = async (req, res) => {
|
|
736
|
+
const ctx = await this.deps.resolveAuthContext(req, res);
|
|
737
|
+
if (!ctx) {
|
|
738
|
+
res.status(401).json({ success: false, error: 'Unauthorized' });
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
const store = this.deps.ensureDataStore();
|
|
742
|
+
const tokenName = String(req.body?.tokenName || '').trim();
|
|
743
|
+
if (!tokenName) {
|
|
744
|
+
res.status(400).json({ success: false, error: 'Token name is required' });
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
const subjectUsername = this.deps.normalizeUsername(req.body?.subjectUsername) || ctx.username;
|
|
748
|
+
if (subjectUsername !== ctx.username && (this.isSaasMode() || ctx.role !== 'admin')) {
|
|
749
|
+
res.status(403).json({ success: false, error: 'Only admin can generate token for other users' });
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
const subjectUser = await store.getUserInWorkspace(subjectUsername, ctx.workspaceId);
|
|
753
|
+
if (!subjectUser) {
|
|
754
|
+
res.status(404).json({ success: false, error: 'Selected user is not in this workspace' });
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
const neverExpires = req.body?.neverExpires === true;
|
|
758
|
+
const rawTtlHours = Number(req.body?.ttlHours);
|
|
759
|
+
const ttlHours = neverExpires ? null : (Number.isFinite(rawTtlHours) && rawTtlHours > 0 ? Math.min(rawTtlHours, 24 * 3650) : 24 * 30);
|
|
760
|
+
const serverRules = this.normalizeTriStateRules(req.body?.serverRules, ctx.workspaceId, 'server');
|
|
761
|
+
const toolRules = this.normalizeTriStateRules(req.body?.toolRules, ctx.workspaceId, 'tool');
|
|
762
|
+
const resourceRules = this.normalizeTriStateRules(req.body?.resourceRules, ctx.workspaceId, 'resource');
|
|
763
|
+
// Backward-compatible fields derived from tri-state maps.
|
|
764
|
+
const serverAllows = Object.entries(serverRules).filter(([, v]) => v === true).map(([k]) => k);
|
|
765
|
+
const serverDenies = Object.entries(serverRules).filter(([, v]) => v === false).map(([k]) => k);
|
|
766
|
+
const toolAllows = Object.entries(toolRules).filter(([, v]) => v === true).map(([k]) => k);
|
|
767
|
+
const toolDenies = Object.entries(toolRules).filter(([, v]) => v === false).map(([k]) => k);
|
|
768
|
+
const resourceAllows = Object.entries(resourceRules).filter(([, v]) => v === true).map(([k]) => k);
|
|
769
|
+
const resourceDenies = Object.entries(resourceRules).filter(([, v]) => v === false).map(([k]) => k);
|
|
770
|
+
const allowAllServers = serverAllows.length === 0 && serverDenies.length === 0;
|
|
771
|
+
const allowAllTools = toolAllows.length === 0 && toolDenies.length === 0;
|
|
772
|
+
const allowAllResources = resourceAllows.length === 0 && resourceDenies.length === 0;
|
|
773
|
+
const serverIds = serverAllows;
|
|
774
|
+
const allowedTools = toolAllows;
|
|
775
|
+
const allowedResources = resourceAllows;
|
|
776
|
+
const tokenPack = this.deps.createMcpToken(subjectUser.username, subjectUser.workspaceId, subjectUser.role, ttlHours ? ttlHours * 3600 : undefined);
|
|
777
|
+
await store.createMcpToken({
|
|
778
|
+
id: tokenPack.tokenId,
|
|
779
|
+
tokenName,
|
|
780
|
+
workspaceId: subjectUser.workspaceId,
|
|
781
|
+
subjectUsername: subjectUser.username,
|
|
782
|
+
createdBy: ctx.username,
|
|
783
|
+
tokenHash: tokenPack.tokenHash,
|
|
784
|
+
tokenValue: tokenPack.token,
|
|
785
|
+
allowAllServers,
|
|
786
|
+
allowAllTools,
|
|
787
|
+
allowAllResources,
|
|
788
|
+
serverIds,
|
|
789
|
+
allowedTools,
|
|
790
|
+
allowedResources,
|
|
791
|
+
serverRules,
|
|
792
|
+
toolRules,
|
|
793
|
+
resourceRules,
|
|
794
|
+
neverExpires,
|
|
795
|
+
expiresAt: tokenPack.expiresAt
|
|
796
|
+
});
|
|
797
|
+
res.json({
|
|
798
|
+
success: true,
|
|
799
|
+
data: {
|
|
800
|
+
id: tokenPack.tokenId,
|
|
801
|
+
tokenName,
|
|
802
|
+
token: tokenPack.token,
|
|
803
|
+
expiresAt: tokenPack.expiresAt,
|
|
804
|
+
neverExpires,
|
|
805
|
+
subjectUsername: subjectUser.username,
|
|
806
|
+
workspaceId: subjectUser.workspaceId
|
|
807
|
+
}
|
|
808
|
+
});
|
|
809
|
+
};
|
|
810
|
+
this.getAuthorizationTokens = async (req, res) => {
|
|
811
|
+
const ctx = await this.deps.resolveAuthContext(req, res);
|
|
812
|
+
if (!ctx) {
|
|
813
|
+
res.status(401).json({ success: false, error: 'Unauthorized' });
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
const canManageWorkspace = ctx.role === 'admin' || this.isSaasMode();
|
|
817
|
+
const tokens = (await this.deps.ensureDataStore()
|
|
818
|
+
.getMcpTokensByWorkspace(ctx.workspaceId))
|
|
819
|
+
.filter((t) => canManageWorkspace || t.subjectUsername === ctx.username)
|
|
820
|
+
.map((t) => ({
|
|
821
|
+
id: t.id,
|
|
822
|
+
tokenName: t.tokenName,
|
|
823
|
+
subjectUsername: t.subjectUsername,
|
|
824
|
+
createdBy: t.createdBy,
|
|
825
|
+
createdAt: t.createdAt,
|
|
826
|
+
expiresAt: t.expiresAt,
|
|
827
|
+
neverExpires: t.neverExpires,
|
|
828
|
+
revokedAt: t.revokedAt,
|
|
829
|
+
allowAllServers: t.allowAllServers,
|
|
830
|
+
allowAllTools: t.allowAllTools,
|
|
831
|
+
allowAllResources: t.allowAllResources,
|
|
832
|
+
serverRules: t.serverRules || {},
|
|
833
|
+
toolRules: t.toolRules || {},
|
|
834
|
+
resourceRules: t.resourceRules || {}
|
|
835
|
+
}));
|
|
836
|
+
res.json({ success: true, data: { tokens } });
|
|
837
|
+
};
|
|
838
|
+
this.getAuthorizationTokenById = async (req, res) => {
|
|
839
|
+
const ctx = await this.deps.resolveAuthContext(req, res);
|
|
840
|
+
if (!ctx) {
|
|
841
|
+
res.status(401).json({ success: false, error: 'Unauthorized' });
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
const canManageWorkspace = ctx.role === 'admin' || this.isSaasMode();
|
|
845
|
+
const token = await this.deps.ensureDataStore().getMcpTokenById(String(req.params.id || ''));
|
|
846
|
+
if (!token || token.workspaceId !== ctx.workspaceId || (!canManageWorkspace && token.subjectUsername !== ctx.username)) {
|
|
847
|
+
res.status(404).json({ success: false, error: 'Token not found' });
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
res.json({
|
|
851
|
+
success: true,
|
|
852
|
+
data: {
|
|
853
|
+
id: token.id,
|
|
854
|
+
tokenName: token.tokenName,
|
|
855
|
+
token: token.tokenValue,
|
|
856
|
+
subjectUsername: token.subjectUsername,
|
|
857
|
+
createdBy: token.createdBy,
|
|
858
|
+
createdAt: token.createdAt,
|
|
859
|
+
expiresAt: token.expiresAt,
|
|
860
|
+
neverExpires: token.neverExpires,
|
|
861
|
+
revokedAt: token.revokedAt,
|
|
862
|
+
allowAllServers: token.allowAllServers,
|
|
863
|
+
allowAllTools: token.allowAllTools,
|
|
864
|
+
allowAllResources: token.allowAllResources,
|
|
865
|
+
serverRules: token.serverRules || {},
|
|
866
|
+
toolRules: token.toolRules || {},
|
|
867
|
+
resourceRules: token.resourceRules || {}
|
|
868
|
+
}
|
|
869
|
+
});
|
|
870
|
+
};
|
|
871
|
+
this.deleteAuthorizationToken = async (req, res) => {
|
|
872
|
+
const ctx = await this.deps.resolveAuthContext(req, res);
|
|
873
|
+
if (!ctx) {
|
|
874
|
+
res.status(401).json({ success: false, error: 'Unauthorized' });
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
const canManageWorkspace = ctx.role === 'admin' || this.isSaasMode();
|
|
878
|
+
const token = await this.deps.ensureDataStore().getMcpTokenById(String(req.params.id || ''));
|
|
879
|
+
if (!token || token.workspaceId !== ctx.workspaceId || (!canManageWorkspace && token.subjectUsername !== ctx.username)) {
|
|
880
|
+
res.status(404).json({ success: false, error: 'Token not found' });
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
await this.deps.ensureDataStore().revokeMcpToken(token.id);
|
|
884
|
+
res.json({ success: true });
|
|
885
|
+
};
|
|
886
|
+
this.createUser = async (req, res) => {
|
|
887
|
+
if (this.isSaasMode()) {
|
|
888
|
+
res.status(404).json({ success: false, error: 'User management is not available in SAAS mode' });
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
const actor = await this.deps.requireAdminApi(req, res);
|
|
892
|
+
if (!actor)
|
|
893
|
+
return;
|
|
894
|
+
const username = this.deps.normalizeUsername(req.body?.username);
|
|
895
|
+
const password = typeof req.body?.password === 'string' ? req.body.password : '';
|
|
896
|
+
const roleInput = typeof req.body?.role === 'string' ? req.body.role.trim().toLowerCase() : 'user';
|
|
897
|
+
const role = roleInput === 'admin' ? 'admin' : 'user';
|
|
898
|
+
if (!username) {
|
|
899
|
+
res.status(400).json({ success: false, error: 'Username is required' });
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
if (!/^[a-zA-Z0-9._-]{3,64}$/.test(username)) {
|
|
903
|
+
res.status(400).json({ success: false, error: 'Username must be 3-64 chars and contain only letters, numbers, dot, underscore, or dash' });
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
if (!password || password.length < 6) {
|
|
907
|
+
res.status(400).json({ success: false, error: 'Password must be at least 6 characters' });
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
try {
|
|
911
|
+
const store = this.deps.ensureDataStore();
|
|
912
|
+
if (await store.getUserInWorkspace(username, actor.workspaceId)) {
|
|
913
|
+
res.status(409).json({ success: false, error: `User "${username}" already exists` });
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
await store.createUser(username, this.deps.hashPassword(password), role, actor.workspaceId);
|
|
917
|
+
res.status(201).json({
|
|
918
|
+
success: true,
|
|
919
|
+
data: {
|
|
920
|
+
user: { username, role, workspaceId: actor.workspaceId },
|
|
921
|
+
createdBy: actor.username
|
|
922
|
+
}
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
catch (error) {
|
|
926
|
+
res.status(500).json({ success: false, error: error instanceof Error ? error.message : 'Failed to create user' });
|
|
927
|
+
}
|
|
928
|
+
};
|
|
929
|
+
this.updateUserRole = async (req, res) => {
|
|
930
|
+
if (this.isSaasMode()) {
|
|
931
|
+
res.status(404).json({ success: false, error: 'User management is not available in SAAS mode' });
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
const actor = await this.deps.requireAdminApi(req, res);
|
|
935
|
+
if (!actor)
|
|
936
|
+
return;
|
|
937
|
+
const targetUsername = this.deps.normalizeUsername(req.params.username);
|
|
938
|
+
const roleInput = typeof req.body?.role === 'string' ? req.body.role.trim().toLowerCase() : '';
|
|
939
|
+
const role = roleInput === 'admin' ? 'admin' : 'user';
|
|
940
|
+
if (!targetUsername) {
|
|
941
|
+
res.status(400).json({ success: false, error: 'Username is required' });
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
const store = this.deps.ensureDataStore();
|
|
945
|
+
const target = await store.getUserInWorkspace(targetUsername, actor.workspaceId);
|
|
946
|
+
if (!target) {
|
|
947
|
+
res.status(404).json({ success: false, error: 'User not found in current workspace' });
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
950
|
+
await store.updateUserRole(targetUsername, actor.workspaceId, role);
|
|
951
|
+
res.json({ success: true, data: { user: { username: targetUsername, workspaceId: actor.workspaceId, role } } });
|
|
952
|
+
};
|
|
953
|
+
this.login = async (req, res) => {
|
|
954
|
+
if (this.deps.authMode === 'NONE') {
|
|
955
|
+
res.status(400).json({ success: false, error: 'Login is disabled when AUTH_MODE=NONE' });
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
if (this.deps.authMode === 'SUPABASE_GOOGLE') {
|
|
959
|
+
res.status(400).json({ success: false, error: 'Use Google Sign-In for SUPABASE_GOOGLE mode' });
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
const username = this.deps.normalizeUsername(req.body?.username);
|
|
963
|
+
const password = typeof req.body?.password === 'string' ? req.body.password : '';
|
|
964
|
+
if (!username || !password) {
|
|
965
|
+
res.status(400).json({ success: false, error: 'Username and password are required' });
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
let authenticatedUsername = '';
|
|
969
|
+
let authenticatedWorkspaceId = '';
|
|
970
|
+
let authenticatedRole = 'user';
|
|
971
|
+
try {
|
|
972
|
+
const storeUser = await this.deps.ensureDataStore().getUser(username);
|
|
973
|
+
if (storeUser && this.deps.verifyPassword(password, storeUser.passwordHash)) {
|
|
974
|
+
authenticatedUsername = storeUser.username;
|
|
975
|
+
authenticatedWorkspaceId = storeUser.workspaceId || storeUser.username;
|
|
976
|
+
authenticatedRole = storeUser.role;
|
|
977
|
+
}
|
|
978
|
+
else {
|
|
979
|
+
const legacyAdmin = this.deps.authAdminUsers.find((item) => item.username === username && item.password === password);
|
|
980
|
+
if (legacyAdmin) {
|
|
981
|
+
authenticatedUsername = legacyAdmin.username;
|
|
982
|
+
authenticatedWorkspaceId = legacyAdmin.username;
|
|
983
|
+
authenticatedRole = 'admin';
|
|
984
|
+
await this.deps.ensureDataStore().upsertUser(legacyAdmin.username, this.deps.hashPassword(legacyAdmin.password), 'admin', legacyAdmin.username);
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
catch (error) {
|
|
989
|
+
logger_1.logger.error('Login datastore error:', error);
|
|
990
|
+
}
|
|
991
|
+
if (!authenticatedUsername) {
|
|
992
|
+
res.status(401).json({ success: false, error: 'Invalid username or password' });
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
if (!authenticatedWorkspaceId)
|
|
996
|
+
authenticatedWorkspaceId = authenticatedUsername;
|
|
997
|
+
await this.issueSession(res, authenticatedUsername, authenticatedWorkspaceId, authenticatedRole, {
|
|
998
|
+
displayName: authenticatedUsername
|
|
999
|
+
});
|
|
1000
|
+
res.json({
|
|
1001
|
+
success: true,
|
|
1002
|
+
data: {
|
|
1003
|
+
username: authenticatedUsername,
|
|
1004
|
+
workspaceId: authenticatedWorkspaceId,
|
|
1005
|
+
role: authenticatedRole,
|
|
1006
|
+
isAdmin: authenticatedRole === 'admin'
|
|
1007
|
+
}
|
|
1008
|
+
});
|
|
1009
|
+
};
|
|
1010
|
+
this.oauthProviderStart = (req, res) => {
|
|
1011
|
+
const authProperty = this.deps.getAuthProperty();
|
|
1012
|
+
if (this.deps.authMode !== 'SUPABASE_GOOGLE') {
|
|
1013
|
+
res.status(400).json({ success: false, error: 'Supabase login is only available when AUTH_MODE=SUPABASE_GOOGLE' });
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
if (!authProperty.providerUrl) {
|
|
1017
|
+
res.status(500).json({ success: false, error: 'SUPABASE_URL is not configured' });
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
const next = typeof req.query.next === 'string' && req.query.next.startsWith('/') ? req.query.next : '/';
|
|
1021
|
+
const appBaseUrl = this.resolveAppBaseUrl(req);
|
|
1022
|
+
const redirectTo = `${appBaseUrl.replace(/\/+$/, '')}/login?oauth=callback`;
|
|
1023
|
+
this.deps.setCookie(res, this.oauthNextCookieName, next, 10 * 60);
|
|
1024
|
+
const authorizeUrl = `${authProperty.providerUrl.replace(/\/+$/, '')}/auth/v1/authorize?provider=google&redirect_to=${encodeURIComponent(redirectTo)}`;
|
|
1025
|
+
res.redirect(authorizeUrl);
|
|
1026
|
+
};
|
|
1027
|
+
this.oauthProviderSession = async (req, res) => {
|
|
1028
|
+
const authProperty = this.deps.getAuthProperty();
|
|
1029
|
+
if (this.deps.authMode !== 'SUPABASE_GOOGLE') {
|
|
1030
|
+
res.status(400).json({ success: false, error: 'Supabase session endpoint is only available when AUTH_MODE=SUPABASE_GOOGLE' });
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
if (!authProperty.providerUrl || !authProperty.publicKey) {
|
|
1034
|
+
res.status(500).json({ success: false, error: 'SUPABASE_URL or SUPABASE_ANON_KEY is not configured' });
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
const cookies = this.deps.parseCookies(req);
|
|
1038
|
+
const cookieNextRaw = String(cookies[this.oauthNextCookieName] || '').trim();
|
|
1039
|
+
const cookieNext = cookieNextRaw.startsWith('/') ? cookieNextRaw : '/oauth/authorize/complete';
|
|
1040
|
+
const accessToken = typeof req.body?.accessToken === 'string' ? req.body.accessToken.trim() : '';
|
|
1041
|
+
if (!accessToken) {
|
|
1042
|
+
res.status(400).json({ success: false, error: 'accessToken is required' });
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
try {
|
|
1046
|
+
const userRes = await fetch(`${authProperty.providerUrl.replace(/\/+$/, '')}/auth/v1/user`, {
|
|
1047
|
+
headers: {
|
|
1048
|
+
apikey: authProperty.publicKey,
|
|
1049
|
+
Authorization: `Bearer ${accessToken}`
|
|
1050
|
+
}
|
|
1051
|
+
});
|
|
1052
|
+
const userPayload = await userRes.json().catch(() => ({}));
|
|
1053
|
+
if (!userRes.ok || !userPayload?.id) {
|
|
1054
|
+
res.status(401).json({ success: false, error: userPayload?.msg || userPayload?.error_description || 'Invalid Supabase access token' });
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
const email = typeof userPayload?.email === 'string' ? userPayload.email : '';
|
|
1058
|
+
const avatarUrl = typeof userPayload?.user_metadata?.avatar_url === 'string'
|
|
1059
|
+
? userPayload.user_metadata.avatar_url
|
|
1060
|
+
: '';
|
|
1061
|
+
const displayNameRaw = userPayload?.user_metadata?.full_name
|
|
1062
|
+
|| userPayload?.user_metadata?.name
|
|
1063
|
+
|| userPayload?.user_metadata?.preferred_username
|
|
1064
|
+
|| '';
|
|
1065
|
+
const displayName = String(displayNameRaw || '').trim();
|
|
1066
|
+
const createdDate = typeof userPayload?.created_at === 'string'
|
|
1067
|
+
? userPayload.created_at
|
|
1068
|
+
: (typeof userPayload?.createdAt === 'string'
|
|
1069
|
+
? userPayload.createdAt
|
|
1070
|
+
: (typeof userPayload?.identities?.[0]?.created_at === 'string'
|
|
1071
|
+
? userPayload.identities[0].created_at
|
|
1072
|
+
: ''));
|
|
1073
|
+
const lastSignInDate = typeof userPayload?.last_sign_in_at === 'string'
|
|
1074
|
+
? userPayload.last_sign_in_at
|
|
1075
|
+
: (typeof userPayload?.lastSignInAt === 'string'
|
|
1076
|
+
? userPayload.lastSignInAt
|
|
1077
|
+
: (typeof userPayload?.identities?.[0]?.last_sign_in_at === 'string'
|
|
1078
|
+
? userPayload.identities[0].last_sign_in_at
|
|
1079
|
+
: new Date().toISOString()));
|
|
1080
|
+
const username = this.normalizeSupabaseUsername(email, userPayload?.id);
|
|
1081
|
+
const workspaceId = username;
|
|
1082
|
+
const role = this.resolveSupabaseRole(username, email);
|
|
1083
|
+
await this.deps.ensureDataStore().upsertUser(username, this.deps.hashPassword(`supabase:${userPayload.id}`), role, workspaceId);
|
|
1084
|
+
await this.issueSession(res, username, workspaceId, role, {
|
|
1085
|
+
displayName: displayName || username,
|
|
1086
|
+
email,
|
|
1087
|
+
avatarUrl,
|
|
1088
|
+
createdDate,
|
|
1089
|
+
lastSignInDate
|
|
1090
|
+
});
|
|
1091
|
+
this.deps.clearCookie(res, this.oauthNextCookieName);
|
|
1092
|
+
res.json({
|
|
1093
|
+
success: true,
|
|
1094
|
+
data: {
|
|
1095
|
+
username,
|
|
1096
|
+
workspaceId,
|
|
1097
|
+
role,
|
|
1098
|
+
isAdmin: role === 'admin',
|
|
1099
|
+
next: cookieNext
|
|
1100
|
+
}
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1103
|
+
catch (error) {
|
|
1104
|
+
res.status(500).json({ success: false, error: error instanceof Error ? error.message : 'Failed to validate Supabase session' });
|
|
1105
|
+
}
|
|
1106
|
+
};
|
|
1107
|
+
this.refresh = async (req, res) => {
|
|
1108
|
+
if (this.deps.authMode === 'NONE') {
|
|
1109
|
+
res.status(400).json({ success: false, error: 'Refresh endpoint is not available when AUTH_MODE=NONE' });
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
const cookies = this.deps.parseCookies(req);
|
|
1113
|
+
const refreshToken = cookies[this.deps.refreshCookieName];
|
|
1114
|
+
const currentAccessToken = cookies[this.deps.accessCookieName];
|
|
1115
|
+
const currentPayload = currentAccessToken
|
|
1116
|
+
? ((0, token_utils_1.verifyAccessToken)(currentAccessToken, this.deps.authCookieSecret)
|
|
1117
|
+
|| (0, token_utils_1.verifyAccessTokenAllowExpired)(currentAccessToken, this.deps.authCookieSecret))
|
|
1118
|
+
: null;
|
|
1119
|
+
if (!refreshToken) {
|
|
1120
|
+
res.status(401).json({ success: false, error: 'Missing refresh token' });
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
const refreshTokenHash = (0, token_utils_1.hashRefreshToken)(refreshToken, this.deps.authCookieSecret);
|
|
1124
|
+
const record = await this.deps.ensureDataStore().getRefreshToken(refreshTokenHash);
|
|
1125
|
+
if (!record || record.revokedAt || new Date(record.expiresAt).getTime() <= Date.now()) {
|
|
1126
|
+
if (record && !record.revokedAt)
|
|
1127
|
+
await this.deps.ensureDataStore().revokeRefreshToken(refreshTokenHash);
|
|
1128
|
+
this.deps.clearCookie(res, this.deps.accessCookieName);
|
|
1129
|
+
this.deps.clearCookie(res, this.deps.refreshCookieName);
|
|
1130
|
+
res.status(401).json({ success: false, error: 'Refresh token is invalid or expired' });
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
await this.deps.ensureDataStore().revokeRefreshToken(refreshTokenHash);
|
|
1134
|
+
const newRefreshToken = (0, token_utils_1.createRefreshToken)();
|
|
1135
|
+
const newRefreshHash = (0, token_utils_1.hashRefreshToken)(newRefreshToken, this.deps.authCookieSecret);
|
|
1136
|
+
const refreshExpiresAt = new Date(Date.now() + this.deps.authRefreshTtlSec * 1000).toISOString();
|
|
1137
|
+
await this.deps.ensureDataStore().saveRefreshToken(newRefreshHash, record.username, refreshExpiresAt);
|
|
1138
|
+
const refreshedUser = await this.deps.ensureDataStore().getUser(record.username);
|
|
1139
|
+
const refreshedWorkspaceId = refreshedUser?.workspaceId || record.username;
|
|
1140
|
+
const refreshedRole = (refreshedUser?.role || this.deps.getUserRole(record.username, refreshedWorkspaceId));
|
|
1141
|
+
const newAccessToken = (0, token_utils_1.createAccessToken)(record.username, this.deps.authCookieSecret, this.deps.authAccessTtlSec, refreshedWorkspaceId, refreshedRole, currentPayload?.displayName || record.username, currentPayload?.email, currentPayload?.avatarUrl, currentPayload?.createdDate, currentPayload?.lastSignInDate);
|
|
1142
|
+
this.deps.setCookie(res, this.deps.accessCookieName, newAccessToken, this.deps.authAccessTtlSec);
|
|
1143
|
+
this.deps.setCookie(res, this.deps.refreshCookieName, newRefreshToken, this.deps.authRefreshTtlSec);
|
|
1144
|
+
res.json({ success: true, data: { username: record.username } });
|
|
1145
|
+
};
|
|
1146
|
+
this.logout = async (req, res) => {
|
|
1147
|
+
const cookies = this.deps.parseCookies(req);
|
|
1148
|
+
const refreshToken = cookies[this.deps.refreshCookieName];
|
|
1149
|
+
if (refreshToken) {
|
|
1150
|
+
const refreshTokenHash = (0, token_utils_1.hashRefreshToken)(refreshToken, this.deps.authCookieSecret);
|
|
1151
|
+
await this.deps.ensureDataStore().revokeRefreshToken(refreshTokenHash);
|
|
1152
|
+
}
|
|
1153
|
+
this.deps.clearCookie(res, this.deps.accessCookieName);
|
|
1154
|
+
this.deps.clearCookie(res, this.deps.refreshCookieName);
|
|
1155
|
+
res.json({ success: true });
|
|
1156
|
+
};
|
|
1157
|
+
this.getLoginPage = async (req, res) => {
|
|
1158
|
+
if (this.deps.authMode === 'NONE') {
|
|
1159
|
+
res.redirect('/');
|
|
1160
|
+
return;
|
|
1161
|
+
}
|
|
1162
|
+
const next = typeof req.query.next === 'string' && req.query.next.startsWith('/')
|
|
1163
|
+
? req.query.next
|
|
1164
|
+
: '/';
|
|
1165
|
+
const ctx = await this.deps.resolveAuthContext(req, res);
|
|
1166
|
+
if (ctx) {
|
|
1167
|
+
res.redirect(next);
|
|
1168
|
+
return;
|
|
1169
|
+
}
|
|
1170
|
+
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
|
|
1171
|
+
res.setHeader('Pragma', 'no-cache');
|
|
1172
|
+
res.setHeader('Expires', '0');
|
|
1173
|
+
res.setHeader('Surrogate-Control', 'no-store');
|
|
1174
|
+
res.sendFile(path_1.default.join(this.deps.publicDir, 'login.html'));
|
|
1175
|
+
};
|
|
1176
|
+
this.getUsersPage = (_req, res) => {
|
|
1177
|
+
if (this.isSaasMode()) {
|
|
1178
|
+
res.status(404).send('Not Found');
|
|
1179
|
+
return;
|
|
1180
|
+
}
|
|
1181
|
+
res.sendFile(path_1.default.join(this.deps.publicDir, 'users.html'));
|
|
1182
|
+
};
|
|
1183
|
+
this.getAuthorizationPage = (_req, res) => {
|
|
1184
|
+
res.sendFile(path_1.default.join(this.deps.publicDir, 'authorization.html'));
|
|
1185
|
+
};
|
|
1186
|
+
}
|
|
1187
|
+
maskTokenForLog(value) {
|
|
1188
|
+
const token = String(value || '');
|
|
1189
|
+
if (!token)
|
|
1190
|
+
return '';
|
|
1191
|
+
if (token.length <= 16)
|
|
1192
|
+
return `${token.slice(0, 4)}...(${token.length})`;
|
|
1193
|
+
return `${token.slice(0, 8)}...${token.slice(-6)}(${token.length})`;
|
|
1194
|
+
}
|
|
1195
|
+
isSaasMode() {
|
|
1196
|
+
return String(this.deps.deployMode || '').trim().toUpperCase() === 'SAAS';
|
|
1197
|
+
}
|
|
1198
|
+
resolveAppBaseUrl(req) {
|
|
1199
|
+
const configured = this.deps.getAuthProperty().appBaseUrl.trim();
|
|
1200
|
+
const hostHeader = (req.get('x-forwarded-host') || req.get('host') || '').trim();
|
|
1201
|
+
const protoHeader = (req.get('x-forwarded-proto') || req.protocol || 'http').trim();
|
|
1202
|
+
const host = hostHeader.split(',')[0].trim().toLowerCase();
|
|
1203
|
+
const proto = protoHeader.split(',')[0].trim().toLowerCase() || 'http';
|
|
1204
|
+
const requestOrigin = host ? `${proto}://${host}` : '';
|
|
1205
|
+
if (!configured)
|
|
1206
|
+
return requestOrigin;
|
|
1207
|
+
// Use a single canonical issuer/resource in production to avoid token binding drift
|
|
1208
|
+
// between www/non-www hosts.
|
|
1209
|
+
try {
|
|
1210
|
+
const configuredHost = new URL(configured).host.toLowerCase();
|
|
1211
|
+
const isLocalConfigured = configuredHost.startsWith('localhost:') || configuredHost.startsWith('127.0.0.1:');
|
|
1212
|
+
if (!isLocalConfigured)
|
|
1213
|
+
return configured;
|
|
1214
|
+
}
|
|
1215
|
+
catch {
|
|
1216
|
+
return requestOrigin || configured;
|
|
1217
|
+
}
|
|
1218
|
+
// Local development convenience: if APP_BASE_URL is localhost but request host differs,
|
|
1219
|
+
// reflect request origin.
|
|
1220
|
+
if (requestOrigin)
|
|
1221
|
+
return requestOrigin;
|
|
1222
|
+
return configured;
|
|
1223
|
+
}
|
|
1224
|
+
normalizeTriStateRules(raw, workspaceId, prefix) {
|
|
1225
|
+
const input = raw && typeof raw === 'object' ? raw : {};
|
|
1226
|
+
const out = {};
|
|
1227
|
+
for (const [key, value] of Object.entries(input)) {
|
|
1228
|
+
const id = String(key || '').trim();
|
|
1229
|
+
if (!id || !id.startsWith(prefix === 'server' ? `${workspaceId}__` : `${workspaceId}__`))
|
|
1230
|
+
continue;
|
|
1231
|
+
if (value === true || value === 'allow' || value === 'true' || value === 1 || value === '1')
|
|
1232
|
+
out[id] = true;
|
|
1233
|
+
else if (value === false || value === 'deny' || value === 'false' || value === -1 || value === '-1')
|
|
1234
|
+
out[id] = false;
|
|
1235
|
+
else
|
|
1236
|
+
out[id] = null;
|
|
1237
|
+
}
|
|
1238
|
+
return out;
|
|
1239
|
+
}
|
|
1240
|
+
normalizePolicyMap(raw, kind, workspaceId, usersInWorkspace) {
|
|
1241
|
+
const input = raw && typeof raw === 'object' ? raw : {};
|
|
1242
|
+
const out = {};
|
|
1243
|
+
for (const [key, value] of Object.entries(input)) {
|
|
1244
|
+
const id = String(key || '').trim();
|
|
1245
|
+
if (!id)
|
|
1246
|
+
continue;
|
|
1247
|
+
if (kind === 'user' && !usersInWorkspace.has(id))
|
|
1248
|
+
continue;
|
|
1249
|
+
if ((kind === 'server' || kind === 'tool') && !id.startsWith(`${workspaceId}__`))
|
|
1250
|
+
continue;
|
|
1251
|
+
if (value === true || value === 'allow' || value === 'true' || value === 1 || value === '1')
|
|
1252
|
+
out[id] = true;
|
|
1253
|
+
else if (value === false || value === 'deny' || value === 'false' || value === 0 || value === '0')
|
|
1254
|
+
out[id] = false;
|
|
1255
|
+
else
|
|
1256
|
+
out[id] = null;
|
|
1257
|
+
}
|
|
1258
|
+
return out;
|
|
1259
|
+
}
|
|
1260
|
+
applyPolicyMap(store, scopeType, map) {
|
|
1261
|
+
const writes = [];
|
|
1262
|
+
for (const [id, decision] of Object.entries(map)) {
|
|
1263
|
+
writes.push(store.setMcpTokenPolicy(scopeType, id, decision));
|
|
1264
|
+
}
|
|
1265
|
+
return Promise.all(writes).then(() => undefined);
|
|
1266
|
+
}
|
|
1267
|
+
registerRoutes(app) {
|
|
1268
|
+
app.get('/.well-known/oauth-authorization-server', this.getOAuthAuthorizationServerMetadata);
|
|
1269
|
+
app.get('/.well-known/oauth-authorization-server/mcp', this.getOAuthAuthorizationServerMetadata);
|
|
1270
|
+
app.get('/mcp/.well-known/oauth-authorization-server', this.getOAuthAuthorizationServerMetadata);
|
|
1271
|
+
app.get('/.well-known/oauth-protected-resource', this.getOAuthProtectedResourceMetadata);
|
|
1272
|
+
app.get('/.well-known/oauth-protected-resource/mcp', this.getOAuthProtectedResourceMetadata);
|
|
1273
|
+
app.get('/mcp/.well-known/oauth-protected-resource', this.getOAuthProtectedResourceMetadata);
|
|
1274
|
+
app.get('/.well-known/openid-configuration', this.getOpenIdConfigurationMetadata);
|
|
1275
|
+
app.get('/.well-known/openid-configuration/mcp', this.getOpenIdConfigurationMetadata);
|
|
1276
|
+
app.get('/mcp/.well-known/openid-configuration', this.getOpenIdConfigurationMetadata);
|
|
1277
|
+
app.get('/oauth/jwks', this.getJwks);
|
|
1278
|
+
app.post('/oauth/register', this.oauthRegister);
|
|
1279
|
+
app.get('/oauth/authorize', this.oauthAuthorize);
|
|
1280
|
+
app.get('/oauth/authorize/complete', this.oauthAuthorizeComplete);
|
|
1281
|
+
app.post('/oauth/token', this.oauthToken);
|
|
1282
|
+
app.post('/oauth/introspect', this.oauthIntrospect);
|
|
1283
|
+
app.get('/oauth/userinfo', this.oauthUserinfo);
|
|
1284
|
+
app.post('/oauth/userinfo', this.oauthUserinfo);
|
|
1285
|
+
app.get('/api/auth/me', this.getMe);
|
|
1286
|
+
app.get('/api/auth/roles', this.getRoles);
|
|
1287
|
+
app.get('/api/auth/users', this.getUsers);
|
|
1288
|
+
app.get('/api/authorization/config', this.getAuthorizationConfig);
|
|
1289
|
+
app.get('/api/authorization/context', this.getAuthorizationContext);
|
|
1290
|
+
app.get('/api/authorization/token-policy', this.getAuthorizationTokenPolicy);
|
|
1291
|
+
app.post('/api/authorization/token-policy', this.updateAuthorizationTokenPolicy);
|
|
1292
|
+
app.get('/api/authorization/servers', this.getAuthorizationServers);
|
|
1293
|
+
app.post('/api/authorization/servers/:id', this.updateAuthorizationServer);
|
|
1294
|
+
app.post('/api/authorization/mcp-token', this.createAuthorizationMcpToken);
|
|
1295
|
+
app.get('/api/authorization/tokens', this.getAuthorizationTokens);
|
|
1296
|
+
app.get('/api/authorization/tokens/:id', this.getAuthorizationTokenById);
|
|
1297
|
+
app.delete('/api/authorization/tokens/:id', this.deleteAuthorizationToken);
|
|
1298
|
+
app.post('/api/auth/users', this.createUser);
|
|
1299
|
+
app.patch('/api/auth/users/:username/role', this.updateUserRole);
|
|
1300
|
+
app.post('/api/auth/login', this.login);
|
|
1301
|
+
app.get('/api/auth/oauth/start', this.oauthProviderStart);
|
|
1302
|
+
app.post('/api/auth/oauth/session', this.oauthProviderSession);
|
|
1303
|
+
// Backward-compatible aliases (deprecated).
|
|
1304
|
+
app.get('/api/auth/supabase/start', this.oauthProviderStart);
|
|
1305
|
+
app.post('/api/auth/supabase/session', this.oauthProviderSession);
|
|
1306
|
+
app.post('/api/auth/refresh', this.refresh);
|
|
1307
|
+
app.post('/api/auth/logout', this.logout);
|
|
1308
|
+
app.get('/login', this.getLoginPage);
|
|
1309
|
+
app.get('/users', this.getUsersPage);
|
|
1310
|
+
app.get('/authorization', this.getAuthorizationPage);
|
|
1311
|
+
}
|
|
1312
|
+
normalizeOAuthCodeChallengeMethod(value) {
|
|
1313
|
+
const normalized = String(value || '').trim().toUpperCase();
|
|
1314
|
+
return normalized === 'PLAIN' ? 'plain' : 'S256';
|
|
1315
|
+
}
|
|
1316
|
+
toBase64Url(input) {
|
|
1317
|
+
return Buffer.from(input, 'utf8').toString('base64url');
|
|
1318
|
+
}
|
|
1319
|
+
fromBase64Url(input) {
|
|
1320
|
+
return Buffer.from(input, 'base64url').toString('utf8');
|
|
1321
|
+
}
|
|
1322
|
+
signOAuthState(payloadEncoded) {
|
|
1323
|
+
return crypto_1.default.createHmac('sha256', this.deps.authCookieSecret).update(payloadEncoded).digest('base64url');
|
|
1324
|
+
}
|
|
1325
|
+
encodeOAuthPendingRequest(payload) {
|
|
1326
|
+
const encoded = this.toBase64Url(JSON.stringify(payload));
|
|
1327
|
+
const sig = this.signOAuthState(encoded);
|
|
1328
|
+
return `${encoded}.${sig}`;
|
|
1329
|
+
}
|
|
1330
|
+
decodeOAuthPendingRequest(raw) {
|
|
1331
|
+
const value = String(raw || '').trim();
|
|
1332
|
+
const dot = value.lastIndexOf('.');
|
|
1333
|
+
if (dot <= 0 || dot >= value.length - 1)
|
|
1334
|
+
return null;
|
|
1335
|
+
const encoded = value.slice(0, dot);
|
|
1336
|
+
const sig = value.slice(dot + 1);
|
|
1337
|
+
const expected = this.signOAuthState(encoded);
|
|
1338
|
+
if (sig !== expected)
|
|
1339
|
+
return null;
|
|
1340
|
+
try {
|
|
1341
|
+
const parsed = JSON.parse(this.fromBase64Url(encoded));
|
|
1342
|
+
if (!parsed || typeof parsed !== 'object')
|
|
1343
|
+
return null;
|
|
1344
|
+
return {
|
|
1345
|
+
clientId: String(parsed.clientId || ''),
|
|
1346
|
+
redirectUri: String(parsed.redirectUri || ''),
|
|
1347
|
+
state: String(parsed.state || ''),
|
|
1348
|
+
scope: String(parsed.scope || ''),
|
|
1349
|
+
resource: String(parsed.resource || ''),
|
|
1350
|
+
codeChallenge: String(parsed.codeChallenge || ''),
|
|
1351
|
+
codeChallengeMethod: this.normalizeOAuthCodeChallengeMethod(String(parsed.codeChallengeMethod || 'S256')),
|
|
1352
|
+
createdAt: Number(parsed.createdAt || 0)
|
|
1353
|
+
};
|
|
1354
|
+
}
|
|
1355
|
+
catch {
|
|
1356
|
+
return null;
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
encodeOAuthCode(record) {
|
|
1360
|
+
const encoded = this.toBase64Url(JSON.stringify(record));
|
|
1361
|
+
const sig = this.signOAuthState(encoded);
|
|
1362
|
+
return `${encoded}.${sig}`;
|
|
1363
|
+
}
|
|
1364
|
+
decodeOAuthCode(code) {
|
|
1365
|
+
const value = String(code || '').trim();
|
|
1366
|
+
const dot = value.lastIndexOf('.');
|
|
1367
|
+
if (dot <= 0 || dot >= value.length - 1)
|
|
1368
|
+
return null;
|
|
1369
|
+
const encoded = value.slice(0, dot);
|
|
1370
|
+
const sig = value.slice(dot + 1);
|
|
1371
|
+
const expected = this.signOAuthState(encoded);
|
|
1372
|
+
if (sig !== expected)
|
|
1373
|
+
return null;
|
|
1374
|
+
try {
|
|
1375
|
+
const parsed = JSON.parse(this.fromBase64Url(encoded));
|
|
1376
|
+
if (!parsed || typeof parsed !== 'object')
|
|
1377
|
+
return null;
|
|
1378
|
+
return parsed;
|
|
1379
|
+
}
|
|
1380
|
+
catch {
|
|
1381
|
+
return null;
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
createIdToken(params) {
|
|
1385
|
+
const now = Math.floor(Date.now() / 1000);
|
|
1386
|
+
const header = Buffer.from(JSON.stringify({ alg: 'RS256', typ: 'JWT', kid: (0, jwks_provider_1.getKid)() })).toString('base64url');
|
|
1387
|
+
const payloadObj = {
|
|
1388
|
+
iss: params.issuer,
|
|
1389
|
+
sub: params.sub,
|
|
1390
|
+
aud: params.clientId,
|
|
1391
|
+
iat: now,
|
|
1392
|
+
exp: now + params.ttlSec
|
|
1393
|
+
};
|
|
1394
|
+
if (params.nonce)
|
|
1395
|
+
payloadObj.nonce = params.nonce;
|
|
1396
|
+
const payloadEncoded = Buffer.from(JSON.stringify(payloadObj)).toString('base64url');
|
|
1397
|
+
const signingInput = `${header}.${payloadEncoded}`;
|
|
1398
|
+
const signature = crypto_1.default.sign('sha256', Buffer.from(signingInput), (0, jwks_provider_1.getRsaPrivateKey)()).toString('base64url');
|
|
1399
|
+
return `${signingInput}.${signature}`;
|
|
1400
|
+
}
|
|
1401
|
+
createAuthorizationCode(record) {
|
|
1402
|
+
const payload = {
|
|
1403
|
+
...record,
|
|
1404
|
+
expiresAt: Date.now() + this.oauthCodeTtlMs
|
|
1405
|
+
};
|
|
1406
|
+
return this.encodeOAuthCode(payload);
|
|
1407
|
+
}
|
|
1408
|
+
appendQueryParam(urlStr, key, value) {
|
|
1409
|
+
try {
|
|
1410
|
+
const url = new URL(urlStr);
|
|
1411
|
+
url.searchParams.set(key, value);
|
|
1412
|
+
return url.toString();
|
|
1413
|
+
}
|
|
1414
|
+
catch {
|
|
1415
|
+
const sep = urlStr.includes('?') ? '&' : '?';
|
|
1416
|
+
return `${urlStr}${sep}${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
normalizeRedirectUri(urlStr) {
|
|
1420
|
+
try {
|
|
1421
|
+
const u = new URL(urlStr);
|
|
1422
|
+
const path = u.pathname !== '/' ? u.pathname.replace(/\/+$/, '') : '/';
|
|
1423
|
+
const port = u.port ? `:${u.port}` : '';
|
|
1424
|
+
return `${u.protocol}//${u.hostname}${port}${path}${u.search}`;
|
|
1425
|
+
}
|
|
1426
|
+
catch {
|
|
1427
|
+
return String(urlStr || '').trim();
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
redirectOAuthError(res, redirectUri, state, error, errorDescription) {
|
|
1431
|
+
let location = this.appendQueryParam(redirectUri, 'error', error);
|
|
1432
|
+
location = this.appendQueryParam(location, 'error_description', errorDescription);
|
|
1433
|
+
if (state)
|
|
1434
|
+
location = this.appendQueryParam(location, 'state', state);
|
|
1435
|
+
res.redirect(location);
|
|
1436
|
+
}
|
|
1437
|
+
async issueSession(res, username, workspaceId, role, profile) {
|
|
1438
|
+
const accessToken = (0, token_utils_1.createAccessToken)(username, this.deps.authCookieSecret, this.deps.authAccessTtlSec, workspaceId, role, profile?.displayName || username, profile?.email, profile?.avatarUrl, profile?.createdDate, profile?.lastSignInDate);
|
|
1439
|
+
const refreshToken = (0, token_utils_1.createRefreshToken)();
|
|
1440
|
+
const refreshTokenHash = (0, token_utils_1.hashRefreshToken)(refreshToken, this.deps.authCookieSecret);
|
|
1441
|
+
const refreshExpiresAt = new Date(Date.now() + this.deps.authRefreshTtlSec * 1000).toISOString();
|
|
1442
|
+
await this.deps.ensureDataStore().saveRefreshToken(refreshTokenHash, username, refreshExpiresAt);
|
|
1443
|
+
this.deps.setCookie(res, this.deps.accessCookieName, accessToken, this.deps.authAccessTtlSec);
|
|
1444
|
+
this.deps.setCookie(res, this.deps.refreshCookieName, refreshToken, this.deps.authRefreshTtlSec);
|
|
1445
|
+
}
|
|
1446
|
+
normalizeSupabaseUsername(email, userId) {
|
|
1447
|
+
const raw = String(email || '').trim().toLowerCase();
|
|
1448
|
+
if (raw) {
|
|
1449
|
+
const localPart = (raw.split('@')[0] || '').trim().toLowerCase();
|
|
1450
|
+
const normalizedLocal = localPart.replace(/[^a-z0-9._-]/g, '_');
|
|
1451
|
+
const shortHash = crypto_1.default.createHash('sha1').update(raw).digest('hex').slice(0, 8);
|
|
1452
|
+
const maxBaseLen = 64 - (shortHash.length + 1);
|
|
1453
|
+
const fallbackBase = raw.replace(/[^a-z0-9._-]/g, '_');
|
|
1454
|
+
const baseSource = normalizedLocal.length >= 3 ? normalizedLocal : fallbackBase;
|
|
1455
|
+
const base = baseSource.slice(0, Math.max(3, maxBaseLen));
|
|
1456
|
+
const candidate = `${base}_${shortHash}`;
|
|
1457
|
+
if (candidate.length >= 3)
|
|
1458
|
+
return candidate.slice(0, 64);
|
|
1459
|
+
}
|
|
1460
|
+
const fallback = String(userId || '').trim().toLowerCase().replace(/[^a-z0-9._-]/g, '_').slice(0, 64);
|
|
1461
|
+
return fallback || 'user';
|
|
1462
|
+
}
|
|
1463
|
+
resolveSupabaseRole(username, email) {
|
|
1464
|
+
const authProperty = this.deps.getAuthProperty();
|
|
1465
|
+
const normalizedEmail = String(email || '').trim().toLowerCase();
|
|
1466
|
+
const isAdminEmail = authProperty.adminEmails.some((v) => v.toLowerCase() === normalizedEmail);
|
|
1467
|
+
const isLegacyAdmin = this.deps.authAdminUsers.some((u) => u.username === username || u.username === normalizedEmail);
|
|
1468
|
+
return (isAdminEmail || isLegacyAdmin) ? 'admin' : 'user';
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
exports.AuthApi = AuthApi;
|
|
1472
|
+
//# sourceMappingURL=authApi.js.map
|