@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,329 @@
|
|
|
1
|
+
import { BadGatewayException, Logger, ServiceUnavailableException } from '@nestjs/common';
|
|
2
|
+
|
|
3
|
+
import { ConfigService } from '../../../common/services/config.service';
|
|
4
|
+
import { ErrorCode } from '../../error-code';
|
|
5
|
+
import {
|
|
6
|
+
ILlmProvider,
|
|
7
|
+
LlmCapabilities,
|
|
8
|
+
LlmCompletionOptions,
|
|
9
|
+
LlmMessage,
|
|
10
|
+
LlmResponse,
|
|
11
|
+
LlmToolSchema,
|
|
12
|
+
} from '../interfaces/llm-provider.interface';
|
|
13
|
+
import { ResolvedAiConnection } from '../interfaces/resolved-ai-connection.interface';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Provider for OpenAI-compatible chat-completions endpoints.
|
|
17
|
+
*
|
|
18
|
+
* Works with any backend that speaks the OpenAI chat-completions shape — local
|
|
19
|
+
* runtimes or hosted endpoints alike. Implemented with the native `fetch` API so
|
|
20
|
+
* the framework core stays dependency-free and vendor-neutral.
|
|
21
|
+
*
|
|
22
|
+
* Capabilities are taken from the connection config (the admin knows what the
|
|
23
|
+
* endpoint supports). The orchestrator compensates: when `nativeTools` is false it
|
|
24
|
+
* emulates tool calling via the system prompt; when `jsonResponse` is false it
|
|
25
|
+
* relies on prompt-instructed JSON. Both are off by default (the safe, lowest
|
|
26
|
+
* common denominator) and can be enabled per connection.
|
|
27
|
+
*/
|
|
28
|
+
export class OpenAiCompatibleProvider implements ILlmProvider {
|
|
29
|
+
readonly capabilities: LlmCapabilities;
|
|
30
|
+
readonly name = 'openai-compatible';
|
|
31
|
+
|
|
32
|
+
private readonly logger = new Logger(OpenAiCompatibleProvider.name);
|
|
33
|
+
private readonly defaultTimeoutMs: number;
|
|
34
|
+
|
|
35
|
+
constructor(private readonly connection: ResolvedAiConnection) {
|
|
36
|
+
this.capabilities = {
|
|
37
|
+
jsonResponse: connection.supportsJsonResponse ?? false,
|
|
38
|
+
nativeTools: connection.supportsNativeTools ?? false,
|
|
39
|
+
systemPrompt: true,
|
|
40
|
+
};
|
|
41
|
+
// Default to 2 minutes for interactive prompts; overridable per connection.
|
|
42
|
+
this.defaultTimeoutMs = connection.timeoutMs ?? 120_000;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Send a chat completion request to the OpenAI-compatible `/chat/completions`
|
|
47
|
+
* endpoint and map the response to {@link LlmResponse}. Sends native `tools` and
|
|
48
|
+
* `response_format` only when the connection's capabilities allow it; native tool
|
|
49
|
+
* calls are mapped via {@link mapNativeToolCalls}. Aborts after the connection's
|
|
50
|
+
* timeout and maps HTTP/transport errors to a {@link ServiceUnavailableException}.
|
|
51
|
+
*/
|
|
52
|
+
async chat(messages: LlmMessage[], tools: LlmToolSchema[], options?: LlmCompletionOptions): Promise<LlmResponse> {
|
|
53
|
+
const url = `${this.connection.baseUrl.replace(/\/$/, '')}/chat/completions`;
|
|
54
|
+
if (!url.startsWith('http')) {
|
|
55
|
+
this.logger.warn(`AI connection "${this.connection.name}" has no valid baseUrl: ${this.connection.baseUrl}`);
|
|
56
|
+
throw new ServiceUnavailableException(ErrorCode.AI_CONNECTION_INVALID_URL);
|
|
57
|
+
}
|
|
58
|
+
this.assertBaseUrlAllowed(url);
|
|
59
|
+
|
|
60
|
+
const body: Record<string, any> = {
|
|
61
|
+
max_tokens: options?.maxTokens ?? this.connection.defaultMaxTokens ?? 2048,
|
|
62
|
+
messages: messages.map((m) => ({
|
|
63
|
+
// Map the emulated 'tool' role to 'user' for backends that only know
|
|
64
|
+
// system/user/assistant.
|
|
65
|
+
content: this.mapMessageContent(m),
|
|
66
|
+
role: m.role === 'tool' ? 'user' : m.role,
|
|
67
|
+
})),
|
|
68
|
+
model: options?.model ?? this.connection.model,
|
|
69
|
+
stream: false,
|
|
70
|
+
temperature: options?.temperature ?? this.connection.defaultTemperature ?? 0.1,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// Attach native parameters only when the backend supports them.
|
|
74
|
+
if (this.capabilities.nativeTools && tools.length) {
|
|
75
|
+
body.tools = tools.map((t) => ({
|
|
76
|
+
function: { description: t.description, name: t.name, parameters: t.parameters },
|
|
77
|
+
type: 'function',
|
|
78
|
+
}));
|
|
79
|
+
}
|
|
80
|
+
if (this.capabilities.jsonResponse) {
|
|
81
|
+
body.response_format = { type: 'json_object' };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const timeoutMs = options?.timeoutMs ?? this.defaultTimeoutMs;
|
|
85
|
+
let response: Response;
|
|
86
|
+
try {
|
|
87
|
+
response = await fetch(url, {
|
|
88
|
+
body: JSON.stringify(body),
|
|
89
|
+
headers: {
|
|
90
|
+
Authorization: `Bearer ${this.connection.apiKey}`,
|
|
91
|
+
'Content-Type': 'application/json',
|
|
92
|
+
},
|
|
93
|
+
method: 'POST',
|
|
94
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
95
|
+
});
|
|
96
|
+
} catch (err) {
|
|
97
|
+
const message = (err as Error)?.name === 'TimeoutError' ? `timeout after ${timeoutMs}ms` : (err as Error).message;
|
|
98
|
+
this.logger.warn(`AI request to "${this.connection.name}" failed: ${message}`);
|
|
99
|
+
throw new ServiceUnavailableException(ErrorCode.AI_PROVIDER_ERROR);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!response.ok) {
|
|
103
|
+
const text = await response.text().catch(() => '');
|
|
104
|
+
this.logger.warn(`AI completion failed (${response.status}) at ${url}: ${text.slice(0, 300)}`);
|
|
105
|
+
throw new BadGatewayException(ErrorCode.AI_PROVIDER_ERROR);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const result = (await response.json()) as {
|
|
109
|
+
choices?: { message?: { content?: string; tool_calls?: any[] } }[];
|
|
110
|
+
usage?: { completion_tokens?: number; prompt_tokens?: number; total_tokens?: number };
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const choice = result.choices?.[0]?.message;
|
|
114
|
+
const text = choice?.content ?? '';
|
|
115
|
+
const nativeToolCalls = this.capabilities.nativeTools ? this.mapNativeToolCalls(choice?.tool_calls) : undefined;
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
raw: result,
|
|
119
|
+
text,
|
|
120
|
+
toolCalls: nativeToolCalls,
|
|
121
|
+
usage: {
|
|
122
|
+
completionTokens: result.usage?.completion_tokens,
|
|
123
|
+
promptTokens: result.usage?.prompt_tokens,
|
|
124
|
+
totalTokens: result.usage?.total_tokens,
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Optional SSRF hardening: when `ai.allowedBaseUrlHosts` is configured (non-empty),
|
|
131
|
+
* only allow requests to those hosts (matched by `host` incl. port, or bare
|
|
132
|
+
* `hostname`). Unset → permissive (so local providers like Ollama on localhost work
|
|
133
|
+
* out of the box). `baseUrl` is admin-only, so this guards a compromised/misconfigured
|
|
134
|
+
* admin, not an end-user input.
|
|
135
|
+
*/
|
|
136
|
+
protected assertBaseUrlAllowed(url: string): void {
|
|
137
|
+
const allowedHosts = ConfigService.get<string[]>('ai.allowedBaseUrlHosts');
|
|
138
|
+
if (!Array.isArray(allowedHosts) || !allowedHosts.length) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
let parsed: URL;
|
|
142
|
+
try {
|
|
143
|
+
parsed = new URL(url);
|
|
144
|
+
} catch {
|
|
145
|
+
throw new ServiceUnavailableException(ErrorCode.AI_CONNECTION_INVALID_URL);
|
|
146
|
+
}
|
|
147
|
+
if (!allowedHosts.includes(parsed.host) && !allowedHosts.includes(parsed.hostname)) {
|
|
148
|
+
this.logger.warn(
|
|
149
|
+
`AI connection "${this.connection.name}" host "${parsed.host}" is not in ai.allowedBaseUrlHosts`,
|
|
150
|
+
);
|
|
151
|
+
throw new ServiceUnavailableException(ErrorCode.AI_CONNECTION_NOT_AVAILABLE);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Probe the backend to auto-detect capabilities for flags the connection left
|
|
157
|
+
* undefined. Explicit flags are authoritative and are NOT probed. Best effort:
|
|
158
|
+
* - JSON: send `response_format: json_object`; 2xx → true, 4xx → false.
|
|
159
|
+
* - Native tools: send a trivial tool with `tool_choice: 'required'`; 2xx WITH a
|
|
160
|
+
* `tool_calls` result → true, otherwise → false (a backend that silently ignores
|
|
161
|
+
* tools returns no tool_calls and is treated as unsupported).
|
|
162
|
+
*
|
|
163
|
+
* Throws on a transport error so callers can treat the connection as undetected
|
|
164
|
+
* (and retry later) rather than persisting a wrong value.
|
|
165
|
+
*/
|
|
166
|
+
async detectCapabilities(): Promise<{ jsonResponse?: boolean; nativeTools?: boolean }> {
|
|
167
|
+
const result: { jsonResponse?: boolean; nativeTools?: boolean } = {};
|
|
168
|
+
if (this.connection.supportsJsonResponse === undefined) {
|
|
169
|
+
const res = await this.probe({
|
|
170
|
+
max_tokens: 8,
|
|
171
|
+
messages: [{ content: 'Reply with the JSON object {"ok":true}.', role: 'user' }],
|
|
172
|
+
response_format: { type: 'json_object' },
|
|
173
|
+
});
|
|
174
|
+
result.jsonResponse = res.ok;
|
|
175
|
+
}
|
|
176
|
+
if (this.connection.supportsNativeTools === undefined) {
|
|
177
|
+
const res = await this.probe({
|
|
178
|
+
max_tokens: 8,
|
|
179
|
+
messages: [{ content: 'Call the ping tool.', role: 'user' }],
|
|
180
|
+
tool_choice: 'required',
|
|
181
|
+
tools: [
|
|
182
|
+
{
|
|
183
|
+
function: {
|
|
184
|
+
description: 'A no-op capability probe.',
|
|
185
|
+
name: 'ping',
|
|
186
|
+
parameters: { properties: {}, type: 'object' },
|
|
187
|
+
},
|
|
188
|
+
type: 'function',
|
|
189
|
+
},
|
|
190
|
+
],
|
|
191
|
+
});
|
|
192
|
+
result.nativeTools = res.ok && !!res.json?.choices?.[0]?.message?.tool_calls?.length;
|
|
193
|
+
}
|
|
194
|
+
return result;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Send a minimal probe request and report whether the endpoint accepted it.
|
|
199
|
+
* `ok` is true only for a 2xx response; a 4xx (feature unsupported) yields false.
|
|
200
|
+
*/
|
|
201
|
+
protected async probe(extra: Record<string, any>): Promise<{ json?: any; ok: boolean }> {
|
|
202
|
+
const url = `${this.connection.baseUrl.replace(/\/$/, '')}/chat/completions`;
|
|
203
|
+
if (!url.startsWith('http')) {
|
|
204
|
+
throw new ServiceUnavailableException(ErrorCode.AI_CONNECTION_INVALID_URL);
|
|
205
|
+
}
|
|
206
|
+
this.assertBaseUrlAllowed(url);
|
|
207
|
+
const response = await fetch(url, {
|
|
208
|
+
body: JSON.stringify({ model: this.connection.model, ...extra }),
|
|
209
|
+
headers: { Authorization: `Bearer ${this.connection.apiKey}`, 'Content-Type': 'application/json' },
|
|
210
|
+
method: 'POST',
|
|
211
|
+
signal: AbortSignal.timeout(this.connection.timeoutMs ?? 30_000),
|
|
212
|
+
});
|
|
213
|
+
if (!response.ok) {
|
|
214
|
+
return { ok: false };
|
|
215
|
+
}
|
|
216
|
+
const json = await response.json().catch(() => undefined);
|
|
217
|
+
return { json, ok: true };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Determine the model's total context window automatically: first an Ollama
|
|
222
|
+
* `/api/show` probe (local runtimes expose `<arch>.context_length`), then a
|
|
223
|
+
* known-model heuristic table. Best effort — returns undefined when unknown.
|
|
224
|
+
*/
|
|
225
|
+
async detectContextWindow(): Promise<number | undefined> {
|
|
226
|
+
const fromBackend = await this.probeContextWindow().catch(() => undefined);
|
|
227
|
+
return fromBackend ?? this.knownContextWindow(this.connection.model);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Heuristic context window (tokens) for well-known model families, matched by
|
|
232
|
+
* case-insensitive substrings of the model id. Override/extend for custom models.
|
|
233
|
+
*/
|
|
234
|
+
protected knownContextWindow(model: string | undefined): number | undefined {
|
|
235
|
+
const m = (model || '').toLowerCase();
|
|
236
|
+
const table: [number, string[]][] = [
|
|
237
|
+
[1_000_000, ['gemini-1.5', 'gemini-2']],
|
|
238
|
+
[200_000, ['claude']],
|
|
239
|
+
[
|
|
240
|
+
128_000,
|
|
241
|
+
['gpt-4o', 'gpt-4.1', 'gpt-4-turbo', 'o1', 'o3', 'gpt-oss', 'mistral-large', 'mistral-small3', 'command-r'],
|
|
242
|
+
],
|
|
243
|
+
[131_072, ['qwen2.5', 'qwen3', 'llama-3.1', 'llama3.1', 'llama-3.3', 'llama3.3']],
|
|
244
|
+
[65_536, ['mixtral']],
|
|
245
|
+
[32_768, ['qwen2', 'mistral', 'gemma2', 'gemma-2']],
|
|
246
|
+
[16_385, ['gpt-3.5']],
|
|
247
|
+
[8_192, ['llama3', 'llama-3', 'gemma']],
|
|
248
|
+
];
|
|
249
|
+
for (const [window, keys] of table) {
|
|
250
|
+
if (keys.some((k) => m.includes(k))) {
|
|
251
|
+
return window;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return undefined;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Probe a local Ollama backend for the model's context length via `/api/show`.
|
|
259
|
+
* Returns undefined for non-Ollama endpoints or on any error.
|
|
260
|
+
*/
|
|
261
|
+
protected async probeContextWindow(): Promise<number | undefined> {
|
|
262
|
+
const base = this.connection.baseUrl.replace(/\/v1\/?$/, '').replace(/\/$/, '');
|
|
263
|
+
if (!base.startsWith('http')) {
|
|
264
|
+
return undefined;
|
|
265
|
+
}
|
|
266
|
+
const response = await fetch(`${base}/api/show`, {
|
|
267
|
+
body: JSON.stringify({ name: this.connection.model }),
|
|
268
|
+
headers: { 'Content-Type': 'application/json' },
|
|
269
|
+
method: 'POST',
|
|
270
|
+
signal: AbortSignal.timeout(this.connection.timeoutMs ?? 15_000),
|
|
271
|
+
}).catch(() => undefined);
|
|
272
|
+
if (!response?.ok) {
|
|
273
|
+
return undefined;
|
|
274
|
+
}
|
|
275
|
+
const json = (await response.json().catch(() => undefined)) as { model_info?: Record<string, unknown> } | undefined;
|
|
276
|
+
const info = json?.model_info;
|
|
277
|
+
if (!info) {
|
|
278
|
+
return undefined;
|
|
279
|
+
}
|
|
280
|
+
const key = Object.keys(info).find((k) => k.endsWith('.context_length'));
|
|
281
|
+
const value = key ? info[key] : undefined;
|
|
282
|
+
return typeof value === 'number' && value > 0 ? value : undefined;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Map native `tool_calls` to the normalized {@link LlmResponse.toolCalls}.
|
|
287
|
+
*/
|
|
288
|
+
/**
|
|
289
|
+
* Map a (possibly multi-modal) LlmMessage to the OpenAI content shape:
|
|
290
|
+
* - Plain text → `content: string` (unchanged).
|
|
291
|
+
* - Text + attachments → `content: [{type:'text',text}, {type:'image_url',image_url:{...}}, ...]`
|
|
292
|
+
* Files/PDFs with `dataUrl` are passed as `image_url` too — backends that don't
|
|
293
|
+
* support vision will warn or ignore.
|
|
294
|
+
*/
|
|
295
|
+
protected mapMessageContent(m: { attachments?: any[]; content: string }): any {
|
|
296
|
+
if (!Array.isArray(m.attachments) || !m.attachments.length) {
|
|
297
|
+
return m.content;
|
|
298
|
+
}
|
|
299
|
+
const parts: any[] = [];
|
|
300
|
+
if (m.content) {
|
|
301
|
+
parts.push({ text: m.content, type: 'text' });
|
|
302
|
+
}
|
|
303
|
+
for (const a of m.attachments) {
|
|
304
|
+
const url = a?.url || a?.dataUrl;
|
|
305
|
+
if (!url) {
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
parts.push({ image_url: { url }, type: 'image_url' });
|
|
309
|
+
}
|
|
310
|
+
return parts.length ? parts : m.content;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
protected mapNativeToolCalls(toolCalls: any[] | undefined) {
|
|
314
|
+
if (!toolCalls?.length) {
|
|
315
|
+
return undefined;
|
|
316
|
+
}
|
|
317
|
+
return toolCalls
|
|
318
|
+
.filter((c) => c?.function?.name)
|
|
319
|
+
.map((c) => {
|
|
320
|
+
let parsedArgs: Record<string, any> = {};
|
|
321
|
+
try {
|
|
322
|
+
parsedArgs = c.function.arguments ? JSON.parse(c.function.arguments) : {};
|
|
323
|
+
} catch {
|
|
324
|
+
parsedArgs = {};
|
|
325
|
+
}
|
|
326
|
+
return { arguments: parsedArgs, id: c.id, name: c.function.name };
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
|
2
|
+
import { createCipheriv, createDecipheriv, createHash, randomBytes } from 'node:crypto';
|
|
3
|
+
|
|
4
|
+
import { isProductionLikeEnv } from '../../../common/helpers/cookies.helper';
|
|
5
|
+
import { ConfigService } from '../../../common/services/config.service';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* AES-256-GCM wrapper for AI connection secrets (API keys).
|
|
9
|
+
*
|
|
10
|
+
* Stored shape: `"<iv_b64>.<authTag_b64>.<ciphertext_b64>"` — a dot-separated
|
|
11
|
+
* triplet that carries everything {@link decrypt} needs. The 256-bit key is
|
|
12
|
+
* derived via SHA-256 from a configurable pass-phrase, so any length works.
|
|
13
|
+
*
|
|
14
|
+
* Secret resolution order:
|
|
15
|
+
* 1. `ai.encryptionSecret` (config / `NSC__AI__ENCRYPTION_SECRET`)
|
|
16
|
+
* 2. `SECRETS_ENCRYPTION_KEY` (env)
|
|
17
|
+
* 3. an insecure development default (logs a warning on first use)
|
|
18
|
+
*
|
|
19
|
+
* Rotating the secret invalidates every value currently in the database;
|
|
20
|
+
* operators must decrypt-then-re-encrypt on rotation (out of scope here).
|
|
21
|
+
*/
|
|
22
|
+
@Injectable()
|
|
23
|
+
export class AiCryptoService implements OnModuleInit {
|
|
24
|
+
private readonly logger = new Logger(AiCryptoService.name);
|
|
25
|
+
private warned = false;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Fail loud at boot in production/staging when AI is active but no real encryption
|
|
29
|
+
* secret is configured — otherwise stored API keys would be "encrypted" with a
|
|
30
|
+
* public, insecure development default. Mirrors the email/cookies production guards.
|
|
31
|
+
*/
|
|
32
|
+
onModuleInit(): void {
|
|
33
|
+
this.assertProductionSafe();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Throw when running in a production-like environment without a configured secret.
|
|
38
|
+
* No-op in local/dev/ci/e2e (there the insecure default + a warning are acceptable).
|
|
39
|
+
*/
|
|
40
|
+
assertProductionSafe(): void {
|
|
41
|
+
if (isProductionLikeEnv(ConfigService.get<string>('env')) && !this.resolveSecret()) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
'AI encryption secret is required in production/staging but none is set ' +
|
|
44
|
+
'(ai.encryptionSecret / NSC__AI__ENCRYPTION_SECRET / SECRETS_ENCRYPTION_KEY). ' +
|
|
45
|
+
'Without it, stored AI API keys are encrypted with a public, insecure development ' +
|
|
46
|
+
'default. Set a random 32+ char value.',
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Decrypt a stored cipher text. Values without the expected triplet shape are
|
|
53
|
+
* returned unchanged (tolerates legacy/plain values without crashing).
|
|
54
|
+
*/
|
|
55
|
+
decrypt(cipherText: string): string {
|
|
56
|
+
if (!cipherText) {
|
|
57
|
+
return '';
|
|
58
|
+
}
|
|
59
|
+
const parts = cipherText.split('.');
|
|
60
|
+
if (parts.length !== 3) {
|
|
61
|
+
return cipherText;
|
|
62
|
+
}
|
|
63
|
+
const [ivB64, tagB64, dataB64] = parts;
|
|
64
|
+
try {
|
|
65
|
+
const iv = Buffer.from(ivB64, 'base64');
|
|
66
|
+
const tag = Buffer.from(tagB64, 'base64');
|
|
67
|
+
const data = Buffer.from(dataB64, 'base64');
|
|
68
|
+
const decipher = createDecipheriv('aes-256-gcm', this.getKey(), iv);
|
|
69
|
+
decipher.setAuthTag(tag);
|
|
70
|
+
const plain = Buffer.concat([decipher.update(data), decipher.final()]);
|
|
71
|
+
return plain.toString('utf8');
|
|
72
|
+
} catch (err) {
|
|
73
|
+
this.logger.error(`Decrypt failed: ${(err as Error).message}`);
|
|
74
|
+
throw new Error('Secret decryption failed — key mismatch or corrupted value', { cause: err });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Encrypt a plaintext secret. Empty/undefined input is preserved as an empty
|
|
80
|
+
* string so "set and cleared" stays distinguishable from "never set".
|
|
81
|
+
*/
|
|
82
|
+
encrypt(plainText: string): string {
|
|
83
|
+
if (plainText === '' || plainText === undefined || plainText === null) {
|
|
84
|
+
return '';
|
|
85
|
+
}
|
|
86
|
+
const iv = randomBytes(12); // GCM standard: 96-bit IV
|
|
87
|
+
const cipher = createCipheriv('aes-256-gcm', this.getKey(), iv);
|
|
88
|
+
const encrypted = Buffer.concat([cipher.update(plainText, 'utf8'), cipher.final()]);
|
|
89
|
+
const tag = cipher.getAuthTag();
|
|
90
|
+
return [iv.toString('base64'), tag.toString('base64'), encrypted.toString('base64')].join('.');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Derive the 32-byte AES key from the configured pass-phrase. `protected` so a
|
|
95
|
+
* project can override key derivation (e.g. to source the key from a KMS).
|
|
96
|
+
*/
|
|
97
|
+
/** Resolve the configured pass-phrase from config/env (no dev fallback). */
|
|
98
|
+
protected resolveSecret(): string | undefined {
|
|
99
|
+
return (
|
|
100
|
+
ConfigService.get<string>('ai.encryptionSecret') ||
|
|
101
|
+
process.env.NSC__AI__ENCRYPTION_SECRET ||
|
|
102
|
+
process.env.SECRETS_ENCRYPTION_KEY
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
protected getKey(): Buffer {
|
|
107
|
+
const raw = this.resolveSecret();
|
|
108
|
+
if (!raw) {
|
|
109
|
+
if (!this.warned) {
|
|
110
|
+
this.logger.warn(
|
|
111
|
+
'No AI encryption secret set (ai.encryptionSecret / NSC__AI__ENCRYPTION_SECRET / SECRETS_ENCRYPTION_KEY) — ' +
|
|
112
|
+
'using an insecure development default. DO NOT run this in production; set a random 32+ char value.',
|
|
113
|
+
);
|
|
114
|
+
this.warned = true;
|
|
115
|
+
}
|
|
116
|
+
return createHash('sha256').update('lt-nest-server-ai-dev-only-insecure-default').digest();
|
|
117
|
+
}
|
|
118
|
+
return createHash('sha256').update(raw, 'utf8').digest();
|
|
119
|
+
}
|
|
120
|
+
}
|