@lenne.tech/nest-server 11.25.6 → 11.26.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/.claude/rules/configurable-features.md +2 -0
- package/CLAUDE.md +18 -0
- package/FRAMEWORK-API.md +48 -1
- package/dist/config.env.js +35 -1
- package/dist/config.env.js.map +1 -1
- package/dist/core/common/helpers/config.helper.js +2 -2
- package/dist/core/common/helpers/config.helper.js.map +1 -1
- package/dist/core/common/interceptors/check-security.interceptor.js +9 -2
- package/dist/core/common/interceptors/check-security.interceptor.js.map +1 -1
- package/dist/core/common/interfaces/server-options.interface.d.ts +89 -0
- package/dist/core/modules/ai/core-ai-mcp.controller.d.ts +21 -0
- package/dist/core/modules/ai/core-ai-mcp.controller.js +191 -0
- package/dist/core/modules/ai/core-ai-mcp.controller.js.map +1 -0
- package/dist/core/modules/ai/core-ai.controller.d.ts +90 -0
- package/dist/core/modules/ai/core-ai.controller.js +576 -0
- package/dist/core/modules/ai/core-ai.controller.js.map +1 -0
- package/dist/core/modules/ai/core-ai.module.d.ts +43 -0
- package/dist/core/modules/ai/core-ai.module.js +164 -0
- package/dist/core/modules/ai/core-ai.module.js.map +1 -0
- package/dist/core/modules/ai/core-ai.resolver.d.ts +84 -0
- package/dist/core/modules/ai/core-ai.resolver.js +530 -0
- package/dist/core/modules/ai/core-ai.resolver.js.map +1 -0
- package/dist/core/modules/ai/helpers/ai-mcp-oauth.helper.d.ts +7 -0
- package/dist/core/modules/ai/helpers/ai-mcp-oauth.helper.js +16 -0
- package/dist/core/modules/ai/helpers/ai-mcp-oauth.helper.js.map +1 -0
- package/dist/core/modules/ai/hooks/ai-hook.base.d.ts +10 -0
- package/dist/core/modules/ai/hooks/ai-hook.base.js +30 -0
- package/dist/core/modules/ai/hooks/ai-hook.base.js.map +1 -0
- package/dist/core/modules/ai/hooks/ai-hook.registry.d.ts +22 -0
- package/dist/core/modules/ai/hooks/ai-hook.registry.js +98 -0
- package/dist/core/modules/ai/hooks/ai-hook.registry.js.map +1 -0
- package/dist/core/modules/ai/index.d.ts +69 -0
- package/dist/core/modules/ai/index.js +86 -0
- package/dist/core/modules/ai/index.js.map +1 -0
- package/dist/core/modules/ai/inputs/core-ai-budget-limit-create.input.d.ts +5 -0
- package/dist/core/modules/ai/inputs/core-ai-budget-limit-create.input.js +43 -0
- package/dist/core/modules/ai/inputs/core-ai-budget-limit-create.input.js.map +1 -0
- package/dist/core/modules/ai/inputs/core-ai-budget-limit.input.d.ts +7 -0
- package/dist/core/modules/ai/inputs/core-ai-budget-limit.input.js +74 -0
- package/dist/core/modules/ai/inputs/core-ai-budget-limit.input.js.map +1 -0
- package/dist/core/modules/ai/inputs/core-ai-connection-create.input.d.ts +6 -0
- package/dist/core/modules/ai/inputs/core-ai-connection-create.input.js +49 -0
- package/dist/core/modules/ai/inputs/core-ai-connection-create.input.js.map +1 -0
- package/dist/core/modules/ai/inputs/core-ai-connection-preference.input.d.ts +6 -0
- package/dist/core/modules/ai/inputs/core-ai-connection-preference.input.js +60 -0
- package/dist/core/modules/ai/inputs/core-ai-connection-preference.input.js.map +1 -0
- package/dist/core/modules/ai/inputs/core-ai-connection.input.d.ts +23 -0
- package/dist/core/modules/ai/inputs/core-ai-connection.input.js +229 -0
- package/dist/core/modules/ai/inputs/core-ai-connection.input.js.map +1 -0
- package/dist/core/modules/ai/inputs/core-ai-conversation-create.input.d.ts +3 -0
- package/dist/core/modules/ai/inputs/core-ai-conversation-create.input.js +21 -0
- package/dist/core/modules/ai/inputs/core-ai-conversation-create.input.js.map +1 -0
- package/dist/core/modules/ai/inputs/core-ai-conversation.input.d.ts +4 -0
- package/dist/core/modules/ai/inputs/core-ai-conversation.input.js +42 -0
- package/dist/core/modules/ai/inputs/core-ai-conversation.input.js.map +1 -0
- package/dist/core/modules/ai/inputs/core-ai-prompt-create.input.d.ts +10 -0
- package/dist/core/modules/ai/inputs/core-ai-prompt-create.input.js +74 -0
- package/dist/core/modules/ai/inputs/core-ai-prompt-create.input.js.map +1 -0
- package/dist/core/modules/ai/inputs/core-ai-prompt-hint-create.input.d.ts +5 -0
- package/dist/core/modules/ai/inputs/core-ai-prompt-hint-create.input.js +41 -0
- package/dist/core/modules/ai/inputs/core-ai-prompt-hint-create.input.js.map +1 -0
- package/dist/core/modules/ai/inputs/core-ai-prompt-hint.input.d.ts +6 -0
- package/dist/core/modules/ai/inputs/core-ai-prompt-hint.input.js +63 -0
- package/dist/core/modules/ai/inputs/core-ai-prompt-hint.input.js.map +1 -0
- package/dist/core/modules/ai/inputs/core-ai-prompt-update.input.d.ts +10 -0
- package/dist/core/modules/ai/inputs/core-ai-prompt-update.input.js +70 -0
- package/dist/core/modules/ai/inputs/core-ai-prompt-update.input.js.map +1 -0
- package/dist/core/modules/ai/inputs/core-ai-prompt.input.d.ts +18 -0
- package/dist/core/modules/ai/inputs/core-ai-prompt.input.js +131 -0
- package/dist/core/modules/ai/inputs/core-ai-prompt.input.js.map +1 -0
- package/dist/core/modules/ai/inputs/core-ai-slot-create.input.d.ts +5 -0
- package/dist/core/modules/ai/inputs/core-ai-slot-create.input.js +41 -0
- package/dist/core/modules/ai/inputs/core-ai-slot-create.input.js.map +1 -0
- package/dist/core/modules/ai/inputs/core-ai-slot-update.input.d.ts +9 -0
- package/dist/core/modules/ai/inputs/core-ai-slot-update.input.js +91 -0
- package/dist/core/modules/ai/inputs/core-ai-slot-update.input.js.map +1 -0
- package/dist/core/modules/ai/interfaces/ai-hook.interface.d.ts +28 -0
- package/dist/core/modules/ai/interfaces/ai-hook.interface.js +3 -0
- package/dist/core/modules/ai/interfaces/ai-hook.interface.js.map +1 -0
- package/dist/core/modules/ai/interfaces/ai-placeholder.interface.d.ts +21 -0
- package/dist/core/modules/ai/interfaces/ai-placeholder.interface.js +3 -0
- package/dist/core/modules/ai/interfaces/ai-placeholder.interface.js.map +1 -0
- package/dist/core/modules/ai/interfaces/ai-tool.interface.d.ts +30 -0
- package/dist/core/modules/ai/interfaces/ai-tool.interface.js +3 -0
- package/dist/core/modules/ai/interfaces/ai-tool.interface.js.map +1 -0
- package/dist/core/modules/ai/interfaces/llm-provider.interface.d.ts +55 -0
- package/dist/core/modules/ai/interfaces/llm-provider.interface.js +3 -0
- package/dist/core/modules/ai/interfaces/llm-provider.interface.js.map +1 -0
- package/dist/core/modules/ai/interfaces/resolved-ai-connection.interface.d.ts +16 -0
- package/dist/core/modules/ai/interfaces/resolved-ai-connection.interface.js +3 -0
- package/dist/core/modules/ai/interfaces/resolved-ai-connection.interface.js.map +1 -0
- package/dist/core/modules/ai/models/core-ai-action.model.d.ts +6 -0
- package/dist/core/modules/ai/models/core-ai-action.model.js +44 -0
- package/dist/core/modules/ai/models/core-ai-action.model.js.map +1 -0
- package/dist/core/modules/ai/models/core-ai-available-connection.model.d.ts +8 -0
- package/dist/core/modules/ai/models/core-ai-available-connection.model.js +53 -0
- package/dist/core/modules/ai/models/core-ai-available-connection.model.js.map +1 -0
- package/dist/core/modules/ai/models/core-ai-budget-limit.model.d.ts +97 -0
- package/dist/core/modules/ai/models/core-ai-budget-limit.model.js +79 -0
- package/dist/core/modules/ai/models/core-ai-budget-limit.model.js.map +1 -0
- package/dist/core/modules/ai/models/core-ai-connection-preference.model.d.ts +91 -0
- package/dist/core/modules/ai/models/core-ai-connection-preference.model.js +67 -0
- package/dist/core/modules/ai/models/core-ai-connection-preference.model.js.map +1 -0
- package/dist/core/modules/ai/models/core-ai-connection.model.d.ts +200 -0
- package/dist/core/modules/ai/models/core-ai-connection.model.js +263 -0
- package/dist/core/modules/ai/models/core-ai-connection.model.js.map +1 -0
- package/dist/core/modules/ai/models/core-ai-conversation.model.d.ts +93 -0
- package/dist/core/modules/ai/models/core-ai-conversation.model.js +82 -0
- package/dist/core/modules/ai/models/core-ai-conversation.model.js.map +1 -0
- package/dist/core/modules/ai/models/core-ai-interaction.model.d.ts +133 -0
- package/dist/core/modules/ai/models/core-ai-interaction.model.js +137 -0
- package/dist/core/modules/ai/models/core-ai-interaction.model.js.map +1 -0
- package/dist/core/modules/ai/models/core-ai-message.model.d.ts +5 -0
- package/dist/core/modules/ai/models/core-ai-message.model.js +38 -0
- package/dist/core/modules/ai/models/core-ai-message.model.js.map +1 -0
- package/dist/core/modules/ai/models/core-ai-mode.model.d.ts +109 -0
- package/dist/core/modules/ai/models/core-ai-mode.model.js +100 -0
- package/dist/core/modules/ai/models/core-ai-mode.model.js.map +1 -0
- package/dist/core/modules/ai/models/core-ai-prompt-hint.model.d.ts +103 -0
- package/dist/core/modules/ai/models/core-ai-prompt-hint.model.js +89 -0
- package/dist/core/modules/ai/models/core-ai-prompt-hint.model.js.map +1 -0
- package/dist/core/modules/ai/models/core-ai-prompt.model.d.ts +122 -0
- package/dist/core/modules/ai/models/core-ai-prompt.model.js +117 -0
- package/dist/core/modules/ai/models/core-ai-prompt.model.js.map +1 -0
- package/dist/core/modules/ai/models/core-ai-response.model.d.ts +29 -0
- package/dist/core/modules/ai/models/core-ai-response.model.js +105 -0
- package/dist/core/modules/ai/models/core-ai-response.model.js.map +1 -0
- package/dist/core/modules/ai/models/core-ai-slot.model.d.ts +121 -0
- package/dist/core/modules/ai/models/core-ai-slot.model.js +119 -0
- package/dist/core/modules/ai/models/core-ai-slot.model.js.map +1 -0
- package/dist/core/modules/ai/models/core-ai-tool-grant.model.d.ts +103 -0
- package/dist/core/modules/ai/models/core-ai-tool-grant.model.js +88 -0
- package/dist/core/modules/ai/models/core-ai-tool-grant.model.js.map +1 -0
- package/dist/core/modules/ai/models/core-ai-tool-policy.model.d.ts +122 -0
- package/dist/core/modules/ai/models/core-ai-tool-policy.model.js +121 -0
- package/dist/core/modules/ai/models/core-ai-tool-policy.model.js.map +1 -0
- package/dist/core/modules/ai/models/core-ai-usage-info.model.d.ts +24 -0
- package/dist/core/modules/ai/models/core-ai-usage-info.model.js +127 -0
- package/dist/core/modules/ai/models/core-ai-usage-info.model.js.map +1 -0
- package/dist/core/modules/ai/models/core-ai-usage.model.d.ts +5 -0
- package/dist/core/modules/ai/models/core-ai-usage.model.js +38 -0
- package/dist/core/modules/ai/models/core-ai-usage.model.js.map +1 -0
- package/dist/core/modules/ai/providers/claude-cli.provider.d.ts +16 -0
- package/dist/core/modules/ai/providers/claude-cli.provider.js +151 -0
- package/dist/core/modules/ai/providers/claude-cli.provider.js.map +1 -0
- package/dist/core/modules/ai/providers/llm-provider.factory.d.ts +10 -0
- package/dist/core/modules/ai/providers/llm-provider.factory.js +45 -0
- package/dist/core/modules/ai/providers/llm-provider.factory.js.map +1 -0
- package/dist/core/modules/ai/providers/openai-compatible.provider.d.ts +32 -0
- package/dist/core/modules/ai/providers/openai-compatible.provider.js +236 -0
- package/dist/core/modules/ai/providers/openai-compatible.provider.js.map +1 -0
- package/dist/core/modules/ai/services/ai-crypto.service.d.ts +11 -0
- package/dist/core/modules/ai/services/ai-crypto.service.js +84 -0
- package/dist/core/modules/ai/services/ai-crypto.service.js.map +1 -0
- package/dist/core/modules/ai/services/core-ai-budget.service.d.ts +42 -0
- package/dist/core/modules/ai/services/core-ai-budget.service.js +211 -0
- package/dist/core/modules/ai/services/core-ai-budget.service.js.map +1 -0
- package/dist/core/modules/ai/services/core-ai-connection-preference.service.d.ts +18 -0
- package/dist/core/modules/ai/services/core-ai-connection-preference.service.js +53 -0
- package/dist/core/modules/ai/services/core-ai-connection-preference.service.js.map +1 -0
- package/dist/core/modules/ai/services/core-ai-connection-resolver.service.d.ts +55 -0
- package/dist/core/modules/ai/services/core-ai-connection-resolver.service.js +177 -0
- package/dist/core/modules/ai/services/core-ai-connection-resolver.service.js.map +1 -0
- package/dist/core/modules/ai/services/core-ai-connection.service.d.ts +43 -0
- package/dist/core/modules/ai/services/core-ai-connection.service.js +272 -0
- package/dist/core/modules/ai/services/core-ai-connection.service.js.map +1 -0
- package/dist/core/modules/ai/services/core-ai-conversation.service.d.ts +26 -0
- package/dist/core/modules/ai/services/core-ai-conversation.service.js +72 -0
- package/dist/core/modules/ai/services/core-ai-conversation.service.js.map +1 -0
- package/dist/core/modules/ai/services/core-ai-interaction.service.d.ts +13 -0
- package/dist/core/modules/ai/services/core-ai-interaction.service.js +52 -0
- package/dist/core/modules/ai/services/core-ai-interaction.service.js.map +1 -0
- package/dist/core/modules/ai/services/core-ai-mcp-client.service.d.ts +51 -0
- package/dist/core/modules/ai/services/core-ai-mcp-client.service.js +123 -0
- package/dist/core/modules/ai/services/core-ai-mcp-client.service.js.map +1 -0
- package/dist/core/modules/ai/services/core-ai-mcp-oauth.service.d.ts +68 -0
- package/dist/core/modules/ai/services/core-ai-mcp-oauth.service.js +279 -0
- package/dist/core/modules/ai/services/core-ai-mcp-oauth.service.js.map +1 -0
- package/dist/core/modules/ai/services/core-ai-mcp.service.d.ts +22 -0
- package/dist/core/modules/ai/services/core-ai-mcp.service.js +59 -0
- package/dist/core/modules/ai/services/core-ai-mcp.service.js.map +1 -0
- package/dist/core/modules/ai/services/core-ai-mode.service.d.ts +14 -0
- package/dist/core/modules/ai/services/core-ai-mode.service.js +56 -0
- package/dist/core/modules/ai/services/core-ai-mode.service.js.map +1 -0
- package/dist/core/modules/ai/services/core-ai-placeholder.registry.d.ts +15 -0
- package/dist/core/modules/ai/services/core-ai-placeholder.registry.js +102 -0
- package/dist/core/modules/ai/services/core-ai-placeholder.registry.js.map +1 -0
- package/dist/core/modules/ai/services/core-ai-prompt-builder.service.d.ts +42 -0
- package/dist/core/modules/ai/services/core-ai-prompt-builder.service.js +136 -0
- package/dist/core/modules/ai/services/core-ai-prompt-builder.service.js.map +1 -0
- package/dist/core/modules/ai/services/core-ai-prompt-hint.service.d.ts +29 -0
- package/dist/core/modules/ai/services/core-ai-prompt-hint.service.js +98 -0
- package/dist/core/modules/ai/services/core-ai-prompt-hint.service.js.map +1 -0
- package/dist/core/modules/ai/services/core-ai-prompt.service.d.ts +21 -0
- package/dist/core/modules/ai/services/core-ai-prompt.service.js +143 -0
- package/dist/core/modules/ai/services/core-ai-prompt.service.js.map +1 -0
- package/dist/core/modules/ai/services/core-ai-slot.service.d.ts +51 -0
- package/dist/core/modules/ai/services/core-ai-slot.service.js +372 -0
- package/dist/core/modules/ai/services/core-ai-slot.service.js.map +1 -0
- package/dist/core/modules/ai/services/core-ai-tool-grant.service.d.ts +22 -0
- package/dist/core/modules/ai/services/core-ai-tool-grant.service.js +97 -0
- package/dist/core/modules/ai/services/core-ai-tool-grant.service.js.map +1 -0
- package/dist/core/modules/ai/services/core-ai-tool-policy.service.d.ts +23 -0
- package/dist/core/modules/ai/services/core-ai-tool-policy.service.js +102 -0
- package/dist/core/modules/ai/services/core-ai-tool-policy.service.js.map +1 -0
- package/dist/core/modules/ai/services/core-ai.service.d.ts +144 -0
- package/dist/core/modules/ai/services/core-ai.service.js +991 -0
- package/dist/core/modules/ai/services/core-ai.service.js.map +1 -0
- package/dist/core/modules/ai/tools/ai-tool.base.d.ts +15 -0
- package/dist/core/modules/ai/tools/ai-tool.base.js +30 -0
- package/dist/core/modules/ai/tools/ai-tool.base.js.map +1 -0
- package/dist/core/modules/ai/tools/ai-tool.registry.d.ts +19 -0
- package/dist/core/modules/ai/tools/ai-tool.registry.js +71 -0
- package/dist/core/modules/ai/tools/ai-tool.registry.js.map +1 -0
- package/dist/core/modules/ai/tools/ask-user-question.tool.d.ts +38 -0
- package/dist/core/modules/ai/tools/ask-user-question.tool.js +65 -0
- package/dist/core/modules/ai/tools/ask-user-question.tool.js.map +1 -0
- package/dist/core/modules/ai/tools/search-tools.tool.d.ts +24 -0
- package/dist/core/modules/ai/tools/search-tools.tool.js +68 -0
- package/dist/core/modules/ai/tools/search-tools.tool.js.map +1 -0
- package/dist/core/modules/error-code/error-codes.d.ts +81 -0
- package/dist/core/modules/error-code/error-codes.js +72 -0
- package/dist/core/modules/error-code/error-codes.js.map +1 -1
- package/dist/core.module.js +8 -0
- package/dist/core.module.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/server/modules/ai/ai-tools.module.d.ts +5 -0
- package/dist/server/modules/ai/ai-tools.module.js +36 -0
- package/dist/server/modules/ai/ai-tools.module.js.map +1 -0
- package/dist/server/modules/ai/tools/delete-user.tool.d.ts +30 -0
- package/dist/server/modules/ai/tools/delete-user.tool.js +49 -0
- package/dist/server/modules/ai/tools/delete-user.tool.js.map +1 -0
- package/dist/server/modules/ai/tools/find-users.tool.d.ts +30 -0
- package/dist/server/modules/ai/tools/find-users.tool.js +52 -0
- package/dist/server/modules/ai/tools/find-users.tool.js.map +1 -0
- package/dist/server/modules/ai/tools/get-user.tool.d.ts +26 -0
- package/dist/server/modules/ai/tools/get-user.tool.js +48 -0
- package/dist/server/modules/ai/tools/get-user.tool.js.map +1 -0
- package/dist/server/modules/ai/tools/update-user-job-title.tool.d.ts +32 -0
- package/dist/server/modules/ai/tools/update-user-job-title.tool.js +54 -0
- package/dist/server/modules/ai/tools/update-user-job-title.tool.js.map +1 -0
- package/dist/server/modules/error-code/error-codes.d.ts +9 -0
- package/dist/server/server.module.js +2 -0
- package/dist/server/server.module.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/docs/REQUEST-LIFECYCLE.md +10 -0
- package/migration-guides/11.25.x-to-11.26.0.md +278 -0
- package/package.json +2 -1
- package/src/config.env.ts +48 -1
- package/src/core/common/helpers/config.helper.ts +5 -2
- package/src/core/common/interceptors/check-security.interceptor.ts +13 -3
- package/src/core/common/interfaces/server-options.interface.ts +312 -0
- package/src/core/modules/ai/INTEGRATION-CHECKLIST.md +220 -0
- package/src/core/modules/ai/README.md +526 -0
- package/src/core/modules/ai/core-ai-mcp.controller.ts +223 -0
- package/src/core/modules/ai/core-ai.controller.ts +547 -0
- package/src/core/modules/ai/core-ai.module.ts +269 -0
- package/src/core/modules/ai/core-ai.resolver.ts +526 -0
- package/src/core/modules/ai/helpers/ai-mcp-oauth.helper.ts +35 -0
- package/src/core/modules/ai/hooks/ai-hook.base.ts +23 -0
- package/src/core/modules/ai/hooks/ai-hook.registry.ts +119 -0
- package/src/core/modules/ai/index.ts +70 -0
- package/src/core/modules/ai/inputs/core-ai-budget-limit-create.input.ts +27 -0
- package/src/core/modules/ai/inputs/core-ai-budget-limit.input.ts +52 -0
- package/src/core/modules/ai/inputs/core-ai-connection-create.input.ts +32 -0
- package/src/core/modules/ai/inputs/core-ai-connection-preference.input.ts +42 -0
- package/src/core/modules/ai/inputs/core-ai-connection.input.ts +235 -0
- package/src/core/modules/ai/inputs/core-ai-conversation-create.input.ts +13 -0
- package/src/core/modules/ai/inputs/core-ai-conversation.input.ts +33 -0
- package/src/core/modules/ai/inputs/core-ai-prompt-create.input.ts +50 -0
- package/src/core/modules/ai/inputs/core-ai-prompt-hint-create.input.ts +26 -0
- package/src/core/modules/ai/inputs/core-ai-prompt-hint.input.ts +43 -0
- package/src/core/modules/ai/inputs/core-ai-prompt-update.input.ts +42 -0
- package/src/core/modules/ai/inputs/core-ai-prompt.input.ts +144 -0
- package/src/core/modules/ai/inputs/core-ai-slot-create.input.ts +26 -0
- package/src/core/modules/ai/inputs/core-ai-slot-update.input.ts +65 -0
- package/src/core/modules/ai/interfaces/ai-hook.interface.ts +74 -0
- package/src/core/modules/ai/interfaces/ai-placeholder.interface.ts +49 -0
- package/src/core/modules/ai/interfaces/ai-tool.interface.ts +112 -0
- package/src/core/modules/ai/interfaces/llm-provider.interface.ts +187 -0
- package/src/core/modules/ai/interfaces/resolved-ai-connection.interface.ts +55 -0
- package/src/core/modules/ai/models/core-ai-action.model.ts +37 -0
- package/src/core/modules/ai/models/core-ai-available-connection.model.ts +42 -0
- package/src/core/modules/ai/models/core-ai-budget-limit.model.ts +82 -0
- package/src/core/modules/ai/models/core-ai-connection-preference.model.ts +72 -0
- package/src/core/modules/ai/models/core-ai-connection.model.ts +330 -0
- package/src/core/modules/ai/models/core-ai-conversation.model.ts +87 -0
- package/src/core/modules/ai/models/core-ai-interaction.model.ts +145 -0
- package/src/core/modules/ai/models/core-ai-message.model.ts +29 -0
- package/src/core/modules/ai/models/core-ai-mode.model.ts +93 -0
- package/src/core/modules/ai/models/core-ai-prompt-hint.model.ts +98 -0
- package/src/core/modules/ai/models/core-ai-prompt.model.ts +120 -0
- package/src/core/modules/ai/models/core-ai-response.model.ts +119 -0
- package/src/core/modules/ai/models/core-ai-slot.model.ts +146 -0
- package/src/core/modules/ai/models/core-ai-tool-grant.model.ts +87 -0
- package/src/core/modules/ai/models/core-ai-tool-policy.model.ts +115 -0
- package/src/core/modules/ai/models/core-ai-usage-info.model.ts +107 -0
- package/src/core/modules/ai/models/core-ai-usage.model.ts +29 -0
- package/src/core/modules/ai/providers/claude-cli.provider.ts +240 -0
- package/src/core/modules/ai/providers/llm-provider.factory.ts +63 -0
- package/src/core/modules/ai/providers/openai-compatible.provider.ts +329 -0
- package/src/core/modules/ai/services/ai-crypto.service.ts +120 -0
- package/src/core/modules/ai/services/core-ai-budget.service.ts +297 -0
- package/src/core/modules/ai/services/core-ai-connection-preference.service.ts +74 -0
- package/src/core/modules/ai/services/core-ai-connection-resolver.service.ts +308 -0
- package/src/core/modules/ai/services/core-ai-connection.service.ts +387 -0
- package/src/core/modules/ai/services/core-ai-conversation.service.ts +105 -0
- package/src/core/modules/ai/services/core-ai-interaction.service.ts +56 -0
- package/src/core/modules/ai/services/core-ai-mcp-client.service.ts +169 -0
- package/src/core/modules/ai/services/core-ai-mcp-oauth.service.ts +445 -0
- package/src/core/modules/ai/services/core-ai-mcp.service.ts +94 -0
- package/src/core/modules/ai/services/core-ai-mode.service.ts +43 -0
- package/src/core/modules/ai/services/core-ai-placeholder.registry.ts +123 -0
- package/src/core/modules/ai/services/core-ai-prompt-builder.service.ts +228 -0
- package/src/core/modules/ai/services/core-ai-prompt-hint.service.ts +133 -0
- package/src/core/modules/ai/services/core-ai-prompt.service.ts +164 -0
- package/src/core/modules/ai/services/core-ai-slot.service.ts +526 -0
- package/src/core/modules/ai/services/core-ai-tool-grant.service.ts +107 -0
- package/src/core/modules/ai/services/core-ai-tool-policy.service.ts +124 -0
- package/src/core/modules/ai/services/core-ai.service.ts +1396 -0
- package/src/core/modules/ai/tools/ai-tool.base.ts +46 -0
- package/src/core/modules/ai/tools/ai-tool.registry.ts +124 -0
- package/src/core/modules/ai/tools/ask-user-question.tool.ts +76 -0
- package/src/core/modules/ai/tools/search-tools.tool.ts +70 -0
- package/src/core/modules/error-code/error-codes.ts +86 -0
- package/src/core.module.ts +12 -0
- package/src/index.ts +6 -0
- package/src/server/modules/ai/ai-tools.module.ts +33 -0
- package/src/server/modules/ai/tools/delete-user.tool.ts +44 -0
- package/src/server/modules/ai/tools/find-users.tool.ts +49 -0
- package/src/server/modules/ai/tools/get-user.tool.ts +43 -0
- package/src/server/modules/ai/tools/update-user-job-title.tool.ts +62 -0
- package/src/server/server.module.ts +5 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { Injectable, Logger } from '@nestjs/common';
|
|
2
|
+
|
|
3
|
+
import { RoleEnum } from '../../../common/enums/role.enum';
|
|
4
|
+
import { AiToolContext, AiToolResult, IAiTool } from '../interfaces/ai-tool.interface';
|
|
5
|
+
import { AiToolRegistry } from '../tools/ai-tool.registry';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The MCP-client side of `@modelcontextprotocol/sdk` that we need to talk to. Kept
|
|
9
|
+
* structurally compatible so projects can pass either the real SDK `Client` or a
|
|
10
|
+
* test double / lightweight wrapper.
|
|
11
|
+
*/
|
|
12
|
+
export interface McpLikeClient {
|
|
13
|
+
callTool(req: {
|
|
14
|
+
arguments?: Record<string, any>;
|
|
15
|
+
name: string;
|
|
16
|
+
}): Promise<{ content?: { text?: string; type?: string }[]; isError?: boolean }>;
|
|
17
|
+
close?(): Promise<void> | void;
|
|
18
|
+
listTools(): Promise<{ tools: { description?: string; inputSchema?: any; name: string }[] }>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Configuration of an external MCP server we connect to as a client.
|
|
23
|
+
*/
|
|
24
|
+
export interface AiMcpClientConfig {
|
|
25
|
+
/** Connected MCP client instance. Use `client` to pass a ready-made client (recommended). */
|
|
26
|
+
client?: McpLikeClient;
|
|
27
|
+
/** Roles required to see the imported tools. @default [S_USER] */
|
|
28
|
+
defaultRoles?: (RoleEnum | string)[];
|
|
29
|
+
/** Logical name; becomes the namespace prefix `<name>_<tool>` for imported tools. */
|
|
30
|
+
name: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Wires an external MCP server into our {@link AiToolRegistry}: lists the server's
|
|
35
|
+
* tools and registers each one as a wrapper tool that calls back to the MCP
|
|
36
|
+
* client's `callTool`. Imported tools are namespaced as `<clientName>_<toolName>`
|
|
37
|
+
* (avoids collisions across clients and with locally registered tools).
|
|
38
|
+
*
|
|
39
|
+
* The wrapper passes the original AiToolContext to the registry filter so role
|
|
40
|
+
* gating still applies — but the underlying CALL goes to the external server,
|
|
41
|
+
* whose own security model also applies. By default we mark imported tools as
|
|
42
|
+
* `mutating: true` (conservative) — they require confirmation. A project can
|
|
43
|
+
* override `defaultRoles` / mutating flag via the config.
|
|
44
|
+
*
|
|
45
|
+
* Override this class for project-specific connection logic (stdio spawn /
|
|
46
|
+
* StreamableHTTP / SSE / OAuth) via `CoreModule.forRoot(env, { ai: { mcpClientService } })`.
|
|
47
|
+
*/
|
|
48
|
+
@Injectable()
|
|
49
|
+
export class CoreAiMcpClientService {
|
|
50
|
+
protected readonly logger = new Logger(CoreAiMcpClientService.name);
|
|
51
|
+
protected readonly registered = new Map<string, { client: McpLikeClient; toolNames: string[] }>();
|
|
52
|
+
|
|
53
|
+
constructor(protected readonly registry: AiToolRegistry) {}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Discover the external server's tools and register each one as a namespaced
|
|
57
|
+
* wrapper in the global tool registry. Idempotent on `name` — re-registers if
|
|
58
|
+
* called again with the same name (drops the previous tools first).
|
|
59
|
+
*/
|
|
60
|
+
async registerExternalClient(config: AiMcpClientConfig): Promise<string[]> {
|
|
61
|
+
if (!config?.name || !config?.client) {
|
|
62
|
+
throw new Error('McpClientService.registerExternalClient requires { name, client }');
|
|
63
|
+
}
|
|
64
|
+
// Drop previous registration under this name.
|
|
65
|
+
await this.unregisterExternalClient(config.name);
|
|
66
|
+
|
|
67
|
+
let discovered: { tools: { description?: string; inputSchema?: any; name: string }[] };
|
|
68
|
+
try {
|
|
69
|
+
discovered = await config.client.listTools();
|
|
70
|
+
} catch (err) {
|
|
71
|
+
this.logger.warn(`MCP listTools for "${config.name}" failed: ${(err as Error).message}`);
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const registered: string[] = [];
|
|
76
|
+
for (const t of discovered?.tools || []) {
|
|
77
|
+
if (!t?.name) {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const ns = `${config.name}_${t.name}`;
|
|
81
|
+
const wrapper: IAiTool = this.buildWrapperTool(ns, t, config);
|
|
82
|
+
this.registry.register(wrapper);
|
|
83
|
+
registered.push(ns);
|
|
84
|
+
}
|
|
85
|
+
this.registered.set(config.name, { client: config.client, toolNames: registered });
|
|
86
|
+
if (registered.length) {
|
|
87
|
+
this.logger.log(
|
|
88
|
+
`Registered ${registered.length} tool(s) from MCP client "${config.name}": ${registered.join(', ')}`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
return registered;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Tear down a previously registered MCP client and remove its tools. */
|
|
95
|
+
async unregisterExternalClient(name: string): Promise<void> {
|
|
96
|
+
const entry = this.registered.get(name);
|
|
97
|
+
if (!entry) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
for (const toolName of entry.toolNames) {
|
|
101
|
+
try {
|
|
102
|
+
this.registry.unregister(toolName);
|
|
103
|
+
} catch {
|
|
104
|
+
// best-effort
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
await entry.client.close?.();
|
|
109
|
+
} catch {
|
|
110
|
+
// best-effort
|
|
111
|
+
}
|
|
112
|
+
this.registered.delete(name);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Build the IAiTool wrapper for one imported MCP tool. */
|
|
116
|
+
protected buildWrapperTool(
|
|
117
|
+
namespacedName: string,
|
|
118
|
+
descriptor: { description?: string; inputSchema?: any; name: string },
|
|
119
|
+
config: AiMcpClientConfig,
|
|
120
|
+
): IAiTool {
|
|
121
|
+
const client = config.client!;
|
|
122
|
+
const tool: IAiTool = {
|
|
123
|
+
description: descriptor.description || `(External MCP tool from "${config.name}")`,
|
|
124
|
+
// Conservative default for external tools — they require confirmation until
|
|
125
|
+
// the project explicitly opts out via a hook or scoped policy.
|
|
126
|
+
mutating: true,
|
|
127
|
+
name: namespacedName,
|
|
128
|
+
parameters: descriptor.inputSchema || { properties: {}, type: 'object' },
|
|
129
|
+
roles: config.defaultRoles || [RoleEnum.S_USER],
|
|
130
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
131
|
+
execute: async (args: Record<string, any>, _context: AiToolContext): Promise<AiToolResult> => {
|
|
132
|
+
try {
|
|
133
|
+
const res = await client.callTool({ arguments: args ?? {}, name: descriptor.name });
|
|
134
|
+
if (res?.isError) {
|
|
135
|
+
return {
|
|
136
|
+
message: this.extractText(res) || `MCP tool "${descriptor.name}" reported an error.`,
|
|
137
|
+
success: false,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
const text = this.extractText(res);
|
|
141
|
+
let data: unknown = text;
|
|
142
|
+
if (text) {
|
|
143
|
+
try {
|
|
144
|
+
data = JSON.parse(text);
|
|
145
|
+
} catch {
|
|
146
|
+
// keep raw text
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return { data, success: true };
|
|
150
|
+
} catch (err) {
|
|
151
|
+
return { message: (err as Error).message, success: false };
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
return tool;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Concatenate text parts of an MCP tool result. */
|
|
159
|
+
protected extractText(res: { content?: { text?: string; type?: string }[] } | undefined): string {
|
|
160
|
+
if (!res?.content?.length) {
|
|
161
|
+
return '';
|
|
162
|
+
}
|
|
163
|
+
return res.content
|
|
164
|
+
.filter((p) => p?.text)
|
|
165
|
+
.map((p) => p.text!)
|
|
166
|
+
.join('\n')
|
|
167
|
+
.trim();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
|
2
|
+
import { InjectConnection } from '@nestjs/mongoose';
|
|
3
|
+
import { createHash, createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
|
|
4
|
+
import { Connection } from 'mongoose';
|
|
5
|
+
|
|
6
|
+
import { isProductionLikeEnv } from '../../../common/helpers/cookies.helper';
|
|
7
|
+
import { ConfigService } from '../../../common/services/config.service';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Stored OAuth client (dynamically registered).
|
|
11
|
+
*
|
|
12
|
+
* For confidential clients (i.e. clients that registered with a
|
|
13
|
+
* `token_endpoint_auth_method` other than `'none'`), `client_secret` is
|
|
14
|
+
* persisted and returned here so the SDK's `clientAuth` middleware can verify
|
|
15
|
+
* it. Dropping the secret from the returned shape would silently downgrade
|
|
16
|
+
* confidential clients to public clients (the SDK's secret-verification branch
|
|
17
|
+
* is gated by `if (client.client_secret)`).
|
|
18
|
+
*/
|
|
19
|
+
export interface AiMcpOAuthClient {
|
|
20
|
+
client_id: string;
|
|
21
|
+
client_name?: string;
|
|
22
|
+
client_secret?: string;
|
|
23
|
+
client_secret_expires_at?: number;
|
|
24
|
+
redirect_uris: string[];
|
|
25
|
+
token_endpoint_auth_method?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Decoded MCP access-token payload.
|
|
30
|
+
*/
|
|
31
|
+
export interface AiMcpAccessTokenPayload {
|
|
32
|
+
cid: string;
|
|
33
|
+
exp: number;
|
|
34
|
+
sub: string;
|
|
35
|
+
type: 'mcp_access';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Security core for the MCP OAuth 2.1 flow.
|
|
40
|
+
*
|
|
41
|
+
* Implements the security-critical primitives — HMAC-signed access tokens
|
|
42
|
+
* (constant-time verification), PKCE (S256) verification — and MongoDB-backed
|
|
43
|
+
* stores for dynamically registered clients, authorization codes (TTL) and
|
|
44
|
+
* refresh tokens. These power the SDK `OAuthServerProvider` returned by
|
|
45
|
+
* {@link buildOAuthProvider}, which `mcpAuthRouter` mounts (see
|
|
46
|
+
* `mountAiMcpOAuth`). The interactive consent/authorize flow is a thin layer on
|
|
47
|
+
* top of these primitives and is documented in the integration checklist.
|
|
48
|
+
*
|
|
49
|
+
* Token format: `base64url(JSON payload).base64url(HMAC-SHA256)`.
|
|
50
|
+
*/
|
|
51
|
+
@Injectable()
|
|
52
|
+
export class CoreAiMcpOAuthService implements OnModuleInit {
|
|
53
|
+
protected readonly logger = new Logger(CoreAiMcpOAuthService.name);
|
|
54
|
+
|
|
55
|
+
/** Native MongoDB collections (no Mongoose schema needed for OAuth artifacts). */
|
|
56
|
+
protected readonly clientsCollection = 'aiMcpOAuthClients';
|
|
57
|
+
protected readonly codesCollection = 'aiMcpOAuthCodes';
|
|
58
|
+
protected readonly refreshCollection = 'aiMcpOAuthRefreshTokens';
|
|
59
|
+
|
|
60
|
+
private indexesEnsured = false;
|
|
61
|
+
private warnedSecret = false;
|
|
62
|
+
|
|
63
|
+
constructor(@InjectConnection() protected readonly connection: Connection) {}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Fail loud at boot in production/staging when the OAuth 2.1 layer is enabled
|
|
67
|
+
* (`ai.mcp.oauth`) but no signing secret is configured — otherwise tokens would be
|
|
68
|
+
* signed with a public, insecure development default.
|
|
69
|
+
*/
|
|
70
|
+
onModuleInit(): void {
|
|
71
|
+
this.assertProductionSafe();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Throw in production/staging when OAuth is enabled but no secret is configured. */
|
|
75
|
+
assertProductionSafe(): void {
|
|
76
|
+
if (this.oauthEnabled() && isProductionLikeEnv(ConfigService.get<string>('env')) && !this.resolveSecret()) {
|
|
77
|
+
throw new Error(
|
|
78
|
+
'AI MCP OAuth signing secret is required in production/staging when ai.mcp.oauth is enabled, ' +
|
|
79
|
+
'but none is set (ai.mcp.oauthSecret / ai.encryptionSecret / NSC__AI__ENCRYPTION_SECRET / ' +
|
|
80
|
+
'SECRETS_ENCRYPTION_KEY). Set a random 32+ char value.',
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Whether the OAuth 2.1 layer is enabled via `ai.mcp.oauth`. */
|
|
86
|
+
protected oauthEnabled(): boolean {
|
|
87
|
+
const mcp = ConfigService.get<{ oauth?: boolean }>('ai.mcp');
|
|
88
|
+
return typeof mcp === 'object' && mcp?.oauth === true;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Resolve the HMAC signing secret from config/env (no dev fallback). */
|
|
92
|
+
protected resolveSecret(): string | undefined {
|
|
93
|
+
return (
|
|
94
|
+
ConfigService.get<string>('ai.mcp.oauthSecret') ||
|
|
95
|
+
ConfigService.get<string>('ai.encryptionSecret') ||
|
|
96
|
+
process.env.NSC__AI__ENCRYPTION_SECRET ||
|
|
97
|
+
process.env.SECRETS_ENCRYPTION_KEY
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ===================================================================================================================
|
|
102
|
+
// Security primitives (unit-tested)
|
|
103
|
+
// ===================================================================================================================
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Sign an access token for a user + client with a TTL (seconds).
|
|
107
|
+
*/
|
|
108
|
+
signAccessToken(userId: string, clientId: string, ttlSeconds = 3600): string {
|
|
109
|
+
const payload: AiMcpAccessTokenPayload = {
|
|
110
|
+
cid: clientId,
|
|
111
|
+
exp: Math.floor(Date.now() / 1000) + ttlSeconds,
|
|
112
|
+
sub: userId,
|
|
113
|
+
type: 'mcp_access',
|
|
114
|
+
};
|
|
115
|
+
const payloadStr = Buffer.from(JSON.stringify(payload)).toString('base64url');
|
|
116
|
+
return `${payloadStr}.${this.hmac(payloadStr)}`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Verify an access token's signature and expiry; returns the payload or null.
|
|
121
|
+
*/
|
|
122
|
+
verifyAccessToken(token: string): AiMcpAccessTokenPayload | null {
|
|
123
|
+
if (!token || !token.includes('.')) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
const [payloadStr, signature] = token.split('.');
|
|
127
|
+
if (!payloadStr || !signature) {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
const expected = this.hmac(payloadStr);
|
|
131
|
+
if (!this.safeEqual(signature, expected)) {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
const payload = JSON.parse(Buffer.from(payloadStr, 'base64url').toString('utf8')) as AiMcpAccessTokenPayload;
|
|
136
|
+
if (payload.type !== 'mcp_access' || payload.exp <= Math.floor(Date.now() / 1000)) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
return payload;
|
|
140
|
+
} catch {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Verify a PKCE code verifier against a stored S256 challenge.
|
|
147
|
+
*/
|
|
148
|
+
verifyPkce(verifier: string, challenge: string, method = 'S256'): boolean {
|
|
149
|
+
if (!verifier || !challenge) {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
if (method !== 'S256') {
|
|
153
|
+
// Only S256 is accepted (plain is insecure).
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
const computed = createHash('sha256').update(verifier).digest('base64url');
|
|
157
|
+
return this.safeEqual(computed, challenge);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Generate a cryptographically secure random id/token.
|
|
162
|
+
*/
|
|
163
|
+
generateId(bytes = 32): string {
|
|
164
|
+
return randomBytes(bytes).toString('base64url');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ===================================================================================================================
|
|
168
|
+
// Stores (MongoDB)
|
|
169
|
+
// ===================================================================================================================
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Register a dynamic OAuth client.
|
|
173
|
+
*/
|
|
174
|
+
async registerClient(client: AiMcpOAuthClient): Promise<AiMcpOAuthClient> {
|
|
175
|
+
await this.ensureIndexes();
|
|
176
|
+
await this.db()
|
|
177
|
+
.collection(this.clientsCollection)
|
|
178
|
+
.insertOne({ ...client, createdAt: new Date() });
|
|
179
|
+
return client;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Look up a registered client.
|
|
184
|
+
*
|
|
185
|
+
* Returns `client_secret` and `client_secret_expires_at` when present so the
|
|
186
|
+
* SDK's `clientAuth` middleware can verify the secret on token-endpoint
|
|
187
|
+
* requests (`if (client.client_secret) { …verify… }`). Public clients
|
|
188
|
+
* (registered with `token_endpoint_auth_method: 'none'`) have no secret and
|
|
189
|
+
* the verification branch is skipped — PKCE protects the auth-code flow.
|
|
190
|
+
*/
|
|
191
|
+
async getClient(clientId: string): Promise<AiMcpOAuthClient | null> {
|
|
192
|
+
const doc = await this.db().collection(this.clientsCollection).findOne({ client_id: clientId });
|
|
193
|
+
if (!doc) {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
client_id: doc.client_id,
|
|
198
|
+
client_name: doc.client_name,
|
|
199
|
+
client_secret: doc.client_secret,
|
|
200
|
+
client_secret_expires_at: doc.client_secret_expires_at,
|
|
201
|
+
redirect_uris: doc.redirect_uris,
|
|
202
|
+
token_endpoint_auth_method: doc.token_endpoint_auth_method,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Persist an authorization code with its PKCE challenge (TTL 10 minutes).
|
|
208
|
+
*/
|
|
209
|
+
async saveAuthorizationCode(
|
|
210
|
+
code: string,
|
|
211
|
+
data: { clientId: string; codeChallenge: string; userId: string },
|
|
212
|
+
): Promise<void> {
|
|
213
|
+
await this.ensureIndexes();
|
|
214
|
+
await this.db().collection(this.codesCollection).insertOne({
|
|
215
|
+
clientId: data.clientId,
|
|
216
|
+
code,
|
|
217
|
+
codeChallenge: data.codeChallenge,
|
|
218
|
+
createdAt: new Date(),
|
|
219
|
+
userId: data.userId,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Read an authorization code WITHOUT deleting it (for PKCE challenge lookup).
|
|
225
|
+
*/
|
|
226
|
+
async getAuthorizationCode(
|
|
227
|
+
code: string,
|
|
228
|
+
): Promise<{ clientId: string; codeChallenge: string; userId: string } | null> {
|
|
229
|
+
const doc = await this.db().collection(this.codesCollection).findOne({ code });
|
|
230
|
+
return doc ? { clientId: doc.clientId, codeChallenge: doc.codeChallenge, userId: doc.userId } : null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Consume (read + delete) an authorization code. One-time use.
|
|
235
|
+
*/
|
|
236
|
+
async consumeAuthorizationCode(
|
|
237
|
+
code: string,
|
|
238
|
+
): Promise<{ clientId: string; codeChallenge: string; userId: string } | null> {
|
|
239
|
+
const doc = await this.db().collection(this.codesCollection).findOneAndDelete({ code });
|
|
240
|
+
const value = (doc as any)?.value ?? doc; // driver version compatibility
|
|
241
|
+
if (!value) {
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
return { clientId: value.clientId, codeChallenge: value.codeChallenge, userId: value.userId };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Issue a refresh token for a user + client.
|
|
249
|
+
*/
|
|
250
|
+
async issueRefreshToken(userId: string, clientId: string): Promise<string> {
|
|
251
|
+
await this.ensureIndexes();
|
|
252
|
+
const token = this.generateId();
|
|
253
|
+
await this.db().collection(this.refreshCollection).insertOne({ clientId, createdAt: new Date(), token, userId });
|
|
254
|
+
return token;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Rotate a refresh token: validate, delete, issue a new one. Returns the user/client.
|
|
259
|
+
*
|
|
260
|
+
* The `clientId` parameter is REQUIRED — refresh tokens are bound to the
|
|
261
|
+
* issuing client per OAuth 2.1 §4.13.2 / §7.4. Looking up the token alone
|
|
262
|
+
* would let a different client present a stolen refresh token and obtain an
|
|
263
|
+
* access token for the original user's subject (impersonation). The
|
|
264
|
+
* `findOneAndDelete({ token, clientId })` query enforces the binding atomically.
|
|
265
|
+
*/
|
|
266
|
+
async rotateRefreshToken(
|
|
267
|
+
token: string,
|
|
268
|
+
clientId: string,
|
|
269
|
+
): Promise<{ clientId: string; newToken: string; userId: string } | null> {
|
|
270
|
+
if (!token || !clientId) {
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
const doc = await this.db().collection(this.refreshCollection).findOneAndDelete({ clientId, token });
|
|
274
|
+
const value = (doc as any)?.value ?? doc;
|
|
275
|
+
if (!value) {
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
const newToken = await this.issueRefreshToken(value.userId, value.clientId);
|
|
279
|
+
return { clientId: value.clientId, newToken, userId: value.userId };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Load the minimal user shape ({ id, roles }) for a verified OAuth subject from
|
|
284
|
+
* the `users` collection (read-only). Enough for tool role-filtering + ownership.
|
|
285
|
+
*/
|
|
286
|
+
async loadUser(userId: string): Promise<{ id: string; roles: string[] } | null> {
|
|
287
|
+
try {
|
|
288
|
+
const { ObjectId } = await import('mongodb');
|
|
289
|
+
const query = ObjectId.isValid(userId) ? { _id: new ObjectId(userId) } : { _id: userId as any };
|
|
290
|
+
const doc = await this.db()
|
|
291
|
+
.collection('users')
|
|
292
|
+
.findOne(query, { projection: { roles: 1 } });
|
|
293
|
+
if (!doc) {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
return { id: userId, roles: Array.isArray(doc.roles) ? doc.roles : [] };
|
|
297
|
+
} catch {
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Build an SDK-compatible `OAuthServerProvider` from the primitives + stores.
|
|
304
|
+
*
|
|
305
|
+
* The machine-to-machine methods (clients store, PKCE challenge lookup, code +
|
|
306
|
+
* refresh exchange, access-token verification) are fully implemented. The
|
|
307
|
+
* interactive {@link authorizeConsent} (browser login/consent) is overridable —
|
|
308
|
+
* a consumer wires it to its own login + consent UI (see INTEGRATION-CHECKLIST).
|
|
309
|
+
*/
|
|
310
|
+
buildOAuthProvider(accessTtlSeconds = 3600): Record<string, any> {
|
|
311
|
+
return {
|
|
312
|
+
authorize: (client: any, params: any, res: any) => this.authorizeConsent(client, params, res),
|
|
313
|
+
challengeForAuthorizationCode: async (_client: any, authorizationCode: string) => {
|
|
314
|
+
const stored = await this.getAuthorizationCode(authorizationCode);
|
|
315
|
+
return stored?.codeChallenge ?? '';
|
|
316
|
+
},
|
|
317
|
+
clientsStore: {
|
|
318
|
+
getClient: (clientId: string) => this.getClient(clientId),
|
|
319
|
+
registerClient: (client: AiMcpOAuthClient) => this.registerClient(client),
|
|
320
|
+
},
|
|
321
|
+
exchangeAuthorizationCode: async (client: any, authorizationCode: string) => {
|
|
322
|
+
const stored = await this.consumeAuthorizationCode(authorizationCode);
|
|
323
|
+
if (!stored || stored.clientId !== client.client_id) {
|
|
324
|
+
throw new Error('invalid_grant');
|
|
325
|
+
}
|
|
326
|
+
return {
|
|
327
|
+
access_token: this.signAccessToken(stored.userId, stored.clientId, accessTtlSeconds),
|
|
328
|
+
expires_in: accessTtlSeconds,
|
|
329
|
+
refresh_token: await this.issueRefreshToken(stored.userId, stored.clientId),
|
|
330
|
+
token_type: 'Bearer',
|
|
331
|
+
};
|
|
332
|
+
},
|
|
333
|
+
exchangeRefreshToken: async (client: any, refreshToken: string) => {
|
|
334
|
+
// Bind the rotating refresh token to the calling client (OAuth 2.1
|
|
335
|
+
// §4.13.2 / §7.4). Without this check, a different client could present
|
|
336
|
+
// a stolen refresh token and obtain an access token for the original
|
|
337
|
+
// user's subject. `rotateRefreshToken` enforces the binding atomically
|
|
338
|
+
// via `findOneAndDelete({ token, clientId })`.
|
|
339
|
+
const callerClientId = client?.client_id;
|
|
340
|
+
if (!callerClientId) {
|
|
341
|
+
throw new Error('invalid_client');
|
|
342
|
+
}
|
|
343
|
+
const rotated = await this.rotateRefreshToken(refreshToken, callerClientId);
|
|
344
|
+
if (!rotated) {
|
|
345
|
+
throw new Error('invalid_grant');
|
|
346
|
+
}
|
|
347
|
+
return {
|
|
348
|
+
access_token: this.signAccessToken(rotated.userId, rotated.clientId, accessTtlSeconds),
|
|
349
|
+
expires_in: accessTtlSeconds,
|
|
350
|
+
refresh_token: rotated.newToken,
|
|
351
|
+
token_type: 'Bearer',
|
|
352
|
+
};
|
|
353
|
+
},
|
|
354
|
+
verifyAccessToken: async (token: string) => {
|
|
355
|
+
const payload = this.verifyAccessToken(token);
|
|
356
|
+
if (!payload) {
|
|
357
|
+
throw new Error('invalid_token');
|
|
358
|
+
}
|
|
359
|
+
return { clientId: payload.cid, extra: { userId: payload.sub }, scopes: [], token };
|
|
360
|
+
},
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Interactive authorize/consent step. Override to integrate your login + consent
|
|
366
|
+
* UI: authenticate the user, then call {@link saveAuthorizationCode} and redirect
|
|
367
|
+
* to `redirect_uri?code=...&state=...`. Default: not implemented.
|
|
368
|
+
*/
|
|
369
|
+
protected async authorizeConsent(_client: any, _params: any, _res: any): Promise<void> {
|
|
370
|
+
throw new Error(
|
|
371
|
+
'authorizeConsent is not implemented — override CoreAiMcpOAuthService.authorizeConsent to wire your login/consent UI.',
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// ===================================================================================================================
|
|
376
|
+
// Helpers
|
|
377
|
+
// ===================================================================================================================
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Compute the HMAC-SHA256 of a payload string (base64url).
|
|
381
|
+
*/
|
|
382
|
+
protected hmac(payloadStr: string): string {
|
|
383
|
+
return createHmac('sha256', this.getSecret()).update(payloadStr).digest('base64url');
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Constant-time string comparison (avoids timing attacks on token/PKCE checks).
|
|
388
|
+
*/
|
|
389
|
+
protected safeEqual(a: string, b: string): boolean {
|
|
390
|
+
const bufA = Buffer.from(a);
|
|
391
|
+
const bufB = Buffer.from(b);
|
|
392
|
+
if (bufA.length !== bufB.length) {
|
|
393
|
+
return false;
|
|
394
|
+
}
|
|
395
|
+
return timingSafeEqual(bufA, bufB);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Resolve the HMAC secret (same sources as AiCryptoService; dedicated override
|
|
400
|
+
* via `ai.mcp.oauthSecret`).
|
|
401
|
+
*/
|
|
402
|
+
protected getSecret(): string {
|
|
403
|
+
const raw = this.resolveSecret();
|
|
404
|
+
if (!raw) {
|
|
405
|
+
if (!this.warnedSecret) {
|
|
406
|
+
this.logger.warn(
|
|
407
|
+
'No AI MCP OAuth secret set — using an insecure development default. Set ai.mcp.oauthSecret ' +
|
|
408
|
+
'(or ai.encryptionSecret / NSC__AI__ENCRYPTION_SECRET) to a random 32+ char value in production.',
|
|
409
|
+
);
|
|
410
|
+
this.warnedSecret = true;
|
|
411
|
+
}
|
|
412
|
+
return 'lt-nest-server-ai-mcp-oauth-dev-only-insecure-default';
|
|
413
|
+
}
|
|
414
|
+
return raw;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Native MongoDB database handle.
|
|
419
|
+
*/
|
|
420
|
+
protected db() {
|
|
421
|
+
if (!this.connection.db) {
|
|
422
|
+
throw new Error('MongoDB connection not available for MCP OAuth');
|
|
423
|
+
}
|
|
424
|
+
return this.connection.db;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Ensure TTL + unique indexes on the OAuth collections (once).
|
|
429
|
+
*/
|
|
430
|
+
protected async ensureIndexes(): Promise<void> {
|
|
431
|
+
if (this.indexesEnsured) {
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
this.indexesEnsured = true;
|
|
435
|
+
try {
|
|
436
|
+
await this.db().collection(this.clientsCollection).createIndex({ client_id: 1 }, { unique: true });
|
|
437
|
+
// Authorization codes expire after 10 minutes.
|
|
438
|
+
await this.db().collection(this.codesCollection).createIndex({ createdAt: 1 }, { expireAfterSeconds: 600 });
|
|
439
|
+
await this.db().collection(this.codesCollection).createIndex({ code: 1 }, { unique: true });
|
|
440
|
+
await this.db().collection(this.refreshCollection).createIndex({ token: 1 }, { unique: true });
|
|
441
|
+
} catch (err) {
|
|
442
|
+
this.logger.warn(`Could not ensure MCP OAuth indexes: ${(err as Error).message}`);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|