@lenne.tech/nest-server 12.1.1 → 12.3.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/architecture.md +1 -0
- package/.claude/rules/better-auth.md +31 -4
- package/.claude/rules/configurable-features.md +8 -6
- package/.claude/rules/package-management.md +29 -2
- package/.claude/rules/persistence.md +27 -1
- package/.claude/rules/testing.md +80 -15
- package/.claude/rules/versioning.md +13 -2
- package/CLAUDE.md +7 -1
- package/FRAMEWORK-API.md +20 -4
- package/README.md +20 -2
- package/dist/config.env.js +2 -2
- package/dist/config.env.js.map +1 -1
- package/dist/core/common/helpers/graphql-ws-connection.helper.d.ts +4 -0
- package/dist/core/common/helpers/graphql-ws-connection.helper.js +17 -3
- package/dist/core/common/helpers/graphql-ws-connection.helper.js.map +1 -1
- package/dist/core/common/interfaces/server-options.interface.d.ts +49 -0
- package/dist/core/common/interfaces/service-options.interface.d.ts +3 -0
- package/dist/core/common/persistence/core-persistence.module.js +12 -2
- package/dist/core/common/persistence/core-persistence.module.js.map +1 -1
- package/dist/core/common/persistence/data-source.runtime.js +4 -2
- package/dist/core/common/persistence/data-source.runtime.js.map +1 -1
- package/dist/core/common/persistence/data-store.interface.d.ts +24 -9
- package/dist/core/common/persistence/id-format.d.ts +2 -0
- package/dist/core/common/persistence/id-format.js +14 -0
- package/dist/core/common/persistence/id-format.js.map +1 -1
- package/dist/core/common/persistence/model-descriptor.interface.d.ts +6 -0
- package/dist/core/common/persistence/model-metadata.registry.d.ts +2 -1
- package/dist/core/common/persistence/model-metadata.registry.js +20 -2
- package/dist/core/common/persistence/model-metadata.registry.js.map +1 -1
- package/dist/core/common/persistence/mongo/sync-schema.plugin.d.ts +18 -0
- package/dist/core/common/persistence/mongo/sync-schema.plugin.js +368 -0
- package/dist/core/common/persistence/mongo/sync-schema.plugin.js.map +1 -0
- package/dist/core/common/persistence/mongoose-data-store.d.ts +19 -9
- package/dist/core/common/persistence/mongoose-data-store.js +107 -31
- package/dist/core/common/persistence/mongoose-data-store.js.map +1 -1
- package/dist/core/common/persistence/persisted-model.decorator.d.ts +3 -0
- package/dist/core/common/persistence/persisted-model.decorator.js +9 -0
- package/dist/core/common/persistence/persisted-model.decorator.js.map +1 -1
- package/dist/core/common/persistence/policies/sync.policy.d.ts +17 -0
- package/dist/core/common/persistence/policies/sync.policy.js +77 -0
- package/dist/core/common/persistence/policies/sync.policy.js.map +1 -0
- package/dist/core/common/persistence/sql/mysql/mysql-data-store.d.ts +12 -8
- package/dist/core/common/persistence/sql/mysql/mysql-data-store.js +82 -29
- package/dist/core/common/persistence/sql/mysql/mysql-data-store.js.map +1 -1
- package/dist/core/common/persistence/sql/mysql/mysql-schema.compiler.d.ts +10 -0
- package/dist/core/common/persistence/sql/mysql/mysql-schema.compiler.js +61 -4
- package/dist/core/common/persistence/sql/mysql/mysql-schema.compiler.js.map +1 -1
- package/dist/core/common/persistence/sql/mysql/mysql-schema.migrator.js +101 -4
- package/dist/core/common/persistence/sql/mysql/mysql-schema.migrator.js.map +1 -1
- package/dist/core/common/persistence/sql/postgres/postgres-schema.compiler.d.ts +10 -0
- package/dist/core/common/persistence/sql/postgres/postgres-schema.compiler.js +41 -2
- package/dist/core/common/persistence/sql/postgres/postgres-schema.compiler.js.map +1 -1
- package/dist/core/common/persistence/sql/postgres/postgres-schema.migrator.js +98 -10
- package/dist/core/common/persistence/sql/postgres/postgres-schema.migrator.js.map +1 -1
- package/dist/core/common/persistence/sql/sql-data-store.d.ts +38 -14
- package/dist/core/common/persistence/sql/sql-data-store.js +212 -38
- package/dist/core/common/persistence/sql/sql-data-store.js.map +1 -1
- package/dist/core/common/persistence/sql/sql-errors.d.ts +2 -1
- package/dist/core/common/persistence/sql/sql-errors.js +3 -1
- package/dist/core/common/persistence/sql/sql-errors.js.map +1 -1
- package/dist/core/common/persistence/sync-access.registry.d.ts +17 -0
- package/dist/core/common/persistence/sync-access.registry.js +8 -0
- package/dist/core/common/persistence/sync-access.registry.js.map +1 -0
- package/dist/core/common/persistence/sync-model.d.ts +29 -0
- package/dist/core/common/persistence/sync-model.js +138 -0
- package/dist/core/common/persistence/sync-model.js.map +1 -0
- package/dist/core/common/services/crud.service.d.ts +13 -1
- package/dist/core/common/services/crud.service.js +174 -16
- package/dist/core/common/services/crud.service.js.map +1 -1
- package/dist/core/modules/ai/core-ai-mcp.controller.d.ts +3 -0
- package/dist/core/modules/ai/core-ai-mcp.controller.js +29 -0
- package/dist/core/modules/ai/core-ai-mcp.controller.js.map +1 -1
- package/dist/core/modules/ai/core-ai.constants.d.ts +2 -0
- package/dist/core/modules/ai/core-ai.constants.js +2 -0
- package/dist/core/modules/ai/core-ai.constants.js.map +1 -1
- package/dist/core/modules/ai/core-ai.controller.d.ts +6 -0
- package/dist/core/modules/ai/core-ai.controller.js +31 -1
- package/dist/core/modules/ai/core-ai.controller.js.map +1 -1
- package/dist/core/modules/ai/core-ai.module.d.ts +2 -0
- package/dist/core/modules/ai/core-ai.module.js +8 -1
- package/dist/core/modules/ai/core-ai.module.js.map +1 -1
- package/dist/core/modules/ai/core-ai.resolver.d.ts +6 -0
- package/dist/core/modules/ai/core-ai.resolver.js +32 -0
- package/dist/core/modules/ai/core-ai.resolver.js.map +1 -1
- package/dist/core/modules/ai/helpers/ai-mcp-oauth-consent.helper.d.ts +65 -1
- package/dist/core/modules/ai/helpers/ai-mcp-oauth-consent.helper.js +6 -3
- package/dist/core/modules/ai/helpers/ai-mcp-oauth-consent.helper.js.map +1 -1
- package/dist/core/modules/ai/helpers/ai-mcp-oauth.helper.js +48 -1
- package/dist/core/modules/ai/helpers/ai-mcp-oauth.helper.js.map +1 -1
- package/dist/core/modules/ai/helpers/ai-tool-authorization.helper.d.ts +9 -0
- package/dist/core/modules/ai/helpers/ai-tool-authorization.helper.js +54 -0
- package/dist/core/modules/ai/helpers/ai-tool-authorization.helper.js.map +1 -0
- package/dist/core/modules/ai/index.d.ts +7 -0
- package/dist/core/modules/ai/index.js +6 -0
- package/dist/core/modules/ai/index.js.map +1 -1
- package/dist/core/modules/ai/inputs/core-ai-settings.input.d.ts +3 -0
- package/dist/core/modules/ai/inputs/core-ai-settings.input.js +31 -0
- package/dist/core/modules/ai/inputs/core-ai-settings.input.js.map +1 -0
- package/dist/core/modules/ai/interfaces/ai-tool.interface.d.ts +2 -0
- package/dist/core/modules/ai/models/core-ai-settings.model.d.ts +4 -0
- package/dist/core/modules/ai/models/core-ai-settings.model.js +30 -0
- package/dist/core/modules/ai/models/core-ai-settings.model.js.map +1 -0
- package/dist/core/modules/ai/models/core-ai-user-setting.model.d.ts +8 -0
- package/dist/core/modules/ai/models/core-ai-user-setting.model.js +45 -0
- package/dist/core/modules/ai/models/core-ai-user-setting.model.js.map +1 -0
- package/dist/core/modules/ai/services/core-ai-conversation.service.d.ts +4 -1
- package/dist/core/modules/ai/services/core-ai-conversation.service.js +16 -4
- package/dist/core/modules/ai/services/core-ai-conversation.service.js.map +1 -1
- package/dist/core/modules/ai/services/core-ai-mcp-oauth.service.js +6 -6
- package/dist/core/modules/ai/services/core-ai-mcp-oauth.service.js.map +1 -1
- package/dist/core/modules/ai/services/core-ai-mcp.service.d.ts +27 -2
- package/dist/core/modules/ai/services/core-ai-mcp.service.js +118 -11
- package/dist/core/modules/ai/services/core-ai-mcp.service.js.map +1 -1
- package/dist/core/modules/ai/services/core-ai-prompt-builder.service.js +9 -6
- package/dist/core/modules/ai/services/core-ai-prompt-builder.service.js.map +1 -1
- package/dist/core/modules/ai/services/core-ai-user-setting.service.d.ts +17 -0
- package/dist/core/modules/ai/services/core-ai-user-setting.service.js +72 -0
- package/dist/core/modules/ai/services/core-ai-user-setting.service.js.map +1 -0
- package/dist/core/modules/ai/services/core-ai.service.d.ts +8 -3
- package/dist/core/modules/ai/services/core-ai.service.js +68 -29
- package/dist/core/modules/ai/services/core-ai.service.js.map +1 -1
- package/dist/core/modules/better-auth/better-auth.config.js +3 -1
- package/dist/core/modules/better-auth/better-auth.config.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js +2 -2
- package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-path-guard.middleware.d.ts +8 -0
- package/dist/core/modules/better-auth/core-better-auth-path-guard.middleware.js +64 -0
- package/dist/core/modules/better-auth/core-better-auth-path-guard.middleware.js.map +1 -0
- package/dist/core/modules/better-auth/core-better-auth-rate-limit.middleware.js +2 -1
- package/dist/core/modules/better-auth/core-better-auth-rate-limit.middleware.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.d.ts +1 -1
- package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.js +5 -3
- package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.module.js +2 -0
- package/dist/core/modules/better-auth/core-better-auth.module.js.map +1 -1
- package/dist/core/modules/better-auth/index.d.ts +1 -0
- package/dist/core/modules/better-auth/index.js +1 -0
- package/dist/core/modules/better-auth/index.js.map +1 -1
- package/dist/core/modules/error-code/error-codes.d.ts +72 -0
- package/dist/core/modules/error-code/error-codes.js +64 -0
- package/dist/core/modules/error-code/error-codes.js.map +1 -1
- package/dist/core/modules/migrate/sql-migration.helper.d.ts +15 -1
- package/dist/core/modules/migrate/sql-migration.helper.js +90 -5
- package/dist/core/modules/migrate/sql-migration.helper.js.map +1 -1
- package/dist/core/modules/sync/core-sync-base.controller.d.ts +7 -0
- package/dist/core/modules/sync/core-sync-base.controller.js +44 -0
- package/dist/core/modules/sync/core-sync-base.controller.js.map +1 -0
- package/dist/core/modules/sync/core-sync-cursor.d.ts +19 -0
- package/dist/core/modules/sync/core-sync-cursor.js +77 -0
- package/dist/core/modules/sync/core-sync-cursor.js.map +1 -0
- package/dist/core/modules/sync/core-sync-hints.controller.d.ts +4 -0
- package/dist/core/modules/sync/core-sync-hints.controller.js +55 -0
- package/dist/core/modules/sync/core-sync-hints.controller.js.map +1 -0
- package/dist/core/modules/sync/core-sync-hints.service.d.ts +28 -0
- package/dist/core/modules/sync/core-sync-hints.service.js +155 -0
- package/dist/core/modules/sync/core-sync-hints.service.js.map +1 -0
- package/dist/core/modules/sync/core-sync-infrastructure.models.d.ts +2 -0
- package/dist/core/modules/sync/core-sync-infrastructure.models.js +17 -0
- package/dist/core/modules/sync/core-sync-infrastructure.models.js.map +1 -0
- package/dist/core/modules/sync/core-sync-mutation.store.d.ts +43 -0
- package/dist/core/modules/sync/core-sync-mutation.store.js +88 -0
- package/dist/core/modules/sync/core-sync-mutation.store.js.map +1 -0
- package/dist/core/modules/sync/core-sync-state.store.d.ts +18 -0
- package/dist/core/modules/sync/core-sync-state.store.js +64 -0
- package/dist/core/modules/sync/core-sync-state.store.js.map +1 -0
- package/dist/core/modules/sync/core-sync.constants.d.ts +28 -0
- package/dist/core/modules/sync/core-sync.constants.js +23 -0
- package/dist/core/modules/sync/core-sync.constants.js.map +1 -0
- package/dist/core/modules/sync/core-sync.controller.d.ts +8 -0
- package/dist/core/modules/sync/core-sync.controller.js +79 -0
- package/dist/core/modules/sync/core-sync.controller.js.map +1 -0
- package/dist/core/modules/sync/core-sync.decorators.d.ts +12 -0
- package/dist/core/modules/sync/core-sync.decorators.js +28 -0
- package/dist/core/modules/sync/core-sync.decorators.js.map +1 -0
- package/dist/core/modules/sync/core-sync.module.d.ts +16 -0
- package/dist/core/modules/sync/core-sync.module.js +50 -0
- package/dist/core/modules/sync/core-sync.module.js.map +1 -0
- package/dist/core/modules/sync/core-sync.resolver.d.ts +9 -0
- package/dist/core/modules/sync/core-sync.resolver.js +46 -0
- package/dist/core/modules/sync/core-sync.resolver.js.map +1 -0
- package/dist/core/modules/sync/core-sync.service.d.ts +143 -0
- package/dist/core/modules/sync/core-sync.service.js +1237 -0
- package/dist/core/modules/sync/core-sync.service.js.map +1 -0
- package/dist/core/modules/sync/interfaces/core-sync.interfaces.d.ts +56 -0
- package/dist/core/modules/sync/interfaces/core-sync.interfaces.js +2 -0
- package/dist/core/modules/sync/interfaces/core-sync.interfaces.js.map +1 -0
- package/dist/core/modules/sync/models/core-sync-hint.model.d.ts +4 -0
- package/dist/core/modules/sync/models/core-sync-hint.model.js +30 -0
- package/dist/core/modules/sync/models/core-sync-hint.model.js.map +1 -0
- package/dist/core/modules/tenant/core-tenant.guard.js +14 -0
- package/dist/core/modules/tenant/core-tenant.guard.js.map +1 -1
- package/dist/core/modules/tenant/core-tenant.service.d.ts +21 -1
- package/dist/core/modules/tenant/core-tenant.service.js +153 -1
- package/dist/core/modules/tenant/core-tenant.service.js.map +1 -1
- package/dist/core.module.js +14 -0
- package/dist/core.module.js.map +1 -1
- package/dist/index.d.ts +16 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -1
- package/dist/server/modules/error-code/error-codes.d.ts +8 -0
- package/dist/server/modules/sync-note/inputs/sync-note-create.input.d.ts +4 -0
- package/dist/server/modules/sync-note/inputs/sync-note-create.input.js +27 -0
- package/dist/server/modules/sync-note/inputs/sync-note-create.input.js.map +1 -0
- package/dist/server/modules/sync-note/inputs/sync-note.input.d.ts +6 -0
- package/dist/server/modules/sync-note/inputs/sync-note.input.js +37 -0
- package/dist/server/modules/sync-note/inputs/sync-note.input.js.map +1 -0
- package/dist/server/modules/sync-note/sync-note.model.d.ts +10 -0
- package/dist/server/modules/sync-note/sync-note.model.js +73 -0
- package/dist/server/modules/sync-note/sync-note.model.js.map +1 -0
- package/dist/server/modules/sync-note/sync-note.module.d.ts +2 -0
- package/dist/server/modules/sync-note/sync-note.module.js +21 -0
- package/dist/server/modules/sync-note/sync-note.module.js.map +1 -0
- package/dist/server/modules/sync-note/sync-note.service.d.ts +10 -0
- package/dist/server/modules/sync-note/sync-note.service.js +35 -0
- package/dist/server/modules/sync-note/sync-note.service.js.map +1 -0
- package/dist/test/test.helper.d.ts +1 -0
- package/dist/test/test.helper.js +9 -2
- package/dist/test/test.helper.js.map +1 -1
- package/docs/REQUEST-LIFECYCLE.md +4 -2
- package/docs/error-codes.md +14 -0
- package/docs/offline-sync-protocol.md +111 -68
- package/docs/security-overrides.md +14 -0
- package/migration-guides/12.0.x-to-12.1.x.md +0 -1
- package/migration-guides/12.1.x-to-12.2.x.md +154 -0
- package/migration-guides/12.2.x-to-12.3.x.md +440 -0
- package/package.json +23 -21
- package/src/config.env.ts +6 -2
- package/src/core/common/helpers/graphql-ws-connection.helper.ts +32 -4
- package/src/core/common/interfaces/server-options.interface.ts +229 -8
- package/src/core/common/interfaces/service-options.interface.ts +15 -0
- package/src/core/common/persistence/core-persistence.module.ts +21 -2
- package/src/core/common/persistence/data-source.runtime.ts +5 -2
- package/src/core/common/persistence/data-store.interface.ts +65 -12
- package/src/core/common/persistence/id-format.ts +26 -0
- package/src/core/common/persistence/model-descriptor.interface.ts +12 -0
- package/src/core/common/persistence/model-metadata.registry.ts +31 -2
- package/src/core/common/persistence/mongo/sync-schema.plugin.ts +508 -0
- package/src/core/common/persistence/mongoose-data-store.ts +171 -35
- package/src/core/common/persistence/persisted-model.decorator.ts +26 -0
- package/src/core/common/persistence/policies/sync.policy.ts +121 -0
- package/src/core/common/persistence/sql/mysql/mysql-data-store.ts +108 -30
- package/src/core/common/persistence/sql/mysql/mysql-schema.compiler.ts +107 -4
- package/src/core/common/persistence/sql/mysql/mysql-schema.migrator.ts +129 -3
- package/src/core/common/persistence/sql/postgres/postgres-schema.compiler.ts +74 -2
- package/src/core/common/persistence/sql/postgres/postgres-schema.migrator.ts +139 -7
- package/src/core/common/persistence/sql/sql-data-store.ts +325 -44
- package/src/core/common/persistence/sql/sql-errors.ts +5 -1
- package/src/core/common/persistence/sync-access.registry.ts +50 -0
- package/src/core/common/persistence/sync-model.ts +270 -0
- package/src/core/common/services/crud.service.ts +257 -19
- package/src/core/modules/ai/INTEGRATION-CHECKLIST.md +36 -15
- package/src/core/modules/ai/README.md +163 -25
- package/src/core/modules/ai/core-ai-mcp.controller.ts +48 -1
- package/src/core/modules/ai/core-ai.constants.ts +6 -0
- package/src/core/modules/ai/core-ai.controller.ts +36 -1
- package/src/core/modules/ai/core-ai.module.ts +12 -0
- package/src/core/modules/ai/core-ai.resolver.ts +36 -0
- package/src/core/modules/ai/helpers/ai-mcp-oauth-consent.helper.ts +35 -4
- package/src/core/modules/ai/helpers/ai-mcp-oauth.helper.ts +68 -1
- package/src/core/modules/ai/helpers/ai-tool-authorization.helper.ts +99 -0
- package/src/core/modules/ai/index.ts +18 -0
- package/src/core/modules/ai/inputs/core-ai-settings.input.ts +24 -0
- package/src/core/modules/ai/interfaces/ai-hook.interface.ts +1 -1
- package/src/core/modules/ai/interfaces/ai-tool.interface.ts +38 -26
- package/src/core/modules/ai/models/core-ai-mode.model.ts +1 -1
- package/src/core/modules/ai/models/core-ai-settings.model.ts +23 -0
- package/src/core/modules/ai/models/core-ai-tool-policy.model.ts +1 -1
- package/src/core/modules/ai/models/core-ai-user-setting.model.ts +46 -0
- package/src/core/modules/ai/services/core-ai-conversation.service.ts +31 -6
- package/src/core/modules/ai/services/core-ai-mcp-oauth.service.ts +12 -9
- package/src/core/modules/ai/services/core-ai-mcp.service.ts +206 -13
- package/src/core/modules/ai/services/core-ai-prompt-builder.service.ts +13 -6
- package/src/core/modules/ai/services/core-ai-user-setting.service.ts +85 -0
- package/src/core/modules/ai/services/core-ai.service.ts +134 -35
- package/src/core/modules/api-token/README.md +4 -2
- package/src/core/modules/better-auth/CUSTOMIZATION.md +1 -1
- package/src/core/modules/better-auth/INTEGRATION-CHECKLIST.md +7 -0
- package/src/core/modules/better-auth/README.md +32 -1
- package/src/core/modules/better-auth/better-auth.config.ts +12 -1
- package/src/core/modules/better-auth/core-better-auth-email-verification.service.ts +5 -2
- package/src/core/modules/better-auth/core-better-auth-path-guard.middleware.ts +85 -0
- package/src/core/modules/better-auth/core-better-auth-rate-limit.middleware.ts +5 -2
- package/src/core/modules/better-auth/core-better-auth-rate-limiter.service.ts +14 -7
- package/src/core/modules/better-auth/core-better-auth.module.ts +12 -2
- package/src/core/modules/better-auth/index.ts +1 -0
- package/src/core/modules/error-code/error-codes.ts +77 -0
- package/src/core/modules/migrate/sql-migration.helper.ts +141 -7
- package/src/core/modules/sync/INTEGRATION-CHECKLIST.md +95 -0
- package/src/core/modules/sync/README.md +198 -0
- package/src/core/modules/sync/SECURITY.md +65 -0
- package/src/core/modules/sync/core-sync-base.controller.ts +43 -0
- package/src/core/modules/sync/core-sync-cursor.ts +117 -0
- package/src/core/modules/sync/core-sync-hints.controller.ts +52 -0
- package/src/core/modules/sync/core-sync-hints.service.ts +178 -0
- package/src/core/modules/sync/core-sync-infrastructure.models.ts +40 -0
- package/src/core/modules/sync/core-sync-mutation.store.ts +146 -0
- package/src/core/modules/sync/core-sync-state.store.ts +89 -0
- package/src/core/modules/sync/core-sync.constants.ts +51 -0
- package/src/core/modules/sync/core-sync.controller.ts +68 -0
- package/src/core/modules/sync/core-sync.decorators.ts +84 -0
- package/src/core/modules/sync/core-sync.module.ts +75 -0
- package/src/core/modules/sync/core-sync.resolver.ts +35 -0
- package/src/core/modules/sync/core-sync.service.ts +1773 -0
- package/src/core/modules/sync/interfaces/core-sync.interfaces.ts +85 -0
- package/src/core/modules/sync/models/core-sync-hint.model.ts +18 -0
- package/src/core/modules/tenant/INTEGRATION-CHECKLIST.md +12 -1
- package/src/core/modules/tenant/README.md +49 -3
- package/src/core/modules/tenant/core-tenant.guard.ts +26 -2
- package/src/core/modules/tenant/core-tenant.service.ts +287 -2
- package/src/core.module.ts +25 -0
- package/src/index.ts +21 -0
- package/src/server/modules/sync-note/inputs/sync-note-create.input.ts +14 -0
- package/src/server/modules/sync-note/inputs/sync-note.input.ts +23 -0
- package/src/server/modules/sync-note/sync-note.model.ts +77 -0
- package/src/server/modules/sync-note/sync-note.module.ts +16 -0
- package/src/server/modules/sync-note/sync-note.service.ts +25 -0
- package/src/test/test.helper.ts +17 -3
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from '../../../common/services/rate-limit-store.js';
|
|
11
11
|
import { RequestContext } from '../../../common/services/request-context.service.js';
|
|
12
12
|
import { ErrorCode } from '../../error-code/index.js';
|
|
13
|
+
import { authorizeToolCall, toolRequiresConfirmation } from '../helpers/ai-tool-authorization.helper.js';
|
|
13
14
|
import { AiToolAuthorization, AiToolContext, AiToolResult, IAiTool } from '../interfaces/ai-tool.interface.js';
|
|
14
15
|
import { LlmMessage, LlmResponse, LlmToolCall } from '../interfaces/llm-provider.interface.js';
|
|
15
16
|
import { ResolvedAiConnection } from '../interfaces/resolved-ai-connection.interface.js';
|
|
@@ -107,6 +108,9 @@ export interface AiRunContext {
|
|
|
107
108
|
export class CoreAiService {
|
|
108
109
|
protected readonly logger = new Logger(CoreAiService.name);
|
|
109
110
|
|
|
111
|
+
/** Connections already warned about a crowded context window; created on first use, at most 1000 entries. */
|
|
112
|
+
protected crowdedContextWarned?: Set<string>;
|
|
113
|
+
|
|
110
114
|
/** Rate-limit counters keyed by user id (Redis-backed when configured). */
|
|
111
115
|
protected rateLimitStore?: RateLimitStore;
|
|
112
116
|
|
|
@@ -142,7 +146,10 @@ export class CoreAiService {
|
|
|
142
146
|
hooks?: { onAction?: (action: CoreAiAction) => void },
|
|
143
147
|
): Promise<CoreAiResponse> {
|
|
144
148
|
const mode = input.mode || ConfigService.get<string>('ai.defaultMode') || 'auto';
|
|
145
|
-
const resolvedInput = await this.
|
|
149
|
+
const resolvedInput = await this.scopeConversation(
|
|
150
|
+
await this.resolvePromptPlaceholders(input, serviceOptions),
|
|
151
|
+
serviceOptions,
|
|
152
|
+
);
|
|
146
153
|
const run = await this.prepareRun(resolvedInput, serviceOptions);
|
|
147
154
|
// No usable connection → AI handling is effectively disabled.
|
|
148
155
|
if (!run) {
|
|
@@ -157,6 +164,28 @@ export class CoreAiService {
|
|
|
157
164
|
return response;
|
|
158
165
|
}
|
|
159
166
|
|
|
167
|
+
/**
|
|
168
|
+
* Keep `conversationId` only when the caller owns that conversation; otherwise the run has no conversation at all:
|
|
169
|
+
* no history, no persisted turns, no conversation-scoped tool grants, and no `conversationId` in the response.
|
|
170
|
+
*
|
|
171
|
+
* Every conversation use of the run reads the id from the input, so it is decided once, before the run starts. A
|
|
172
|
+
* foreign id must not reach any of them: its turns would land in another user's history and feed that user's next
|
|
173
|
+
* runs, where tools act with the owner's rights, and a "remember my decision" grant stored on it would let the
|
|
174
|
+
* owner's assistant run a mutating tool unconfirmed. Admins included — they may read other users' conversations,
|
|
175
|
+
* never continue them. An unknown id is answered exactly like a foreign one, so the response tells nobody which
|
|
176
|
+
* conversation ids exist.
|
|
177
|
+
*/
|
|
178
|
+
protected async scopeConversation(
|
|
179
|
+
input: CoreAiPromptInput,
|
|
180
|
+
serviceOptions: ServiceOptions,
|
|
181
|
+
): Promise<CoreAiPromptInput> {
|
|
182
|
+
if (!input?.conversationId) {
|
|
183
|
+
return input;
|
|
184
|
+
}
|
|
185
|
+
const owned = await this.conversationService?.isOwnedBy(input.conversationId, serviceOptions?.currentUser?.id);
|
|
186
|
+
return owned ? input : { ...input, conversationId: undefined };
|
|
187
|
+
}
|
|
188
|
+
|
|
160
189
|
/**
|
|
161
190
|
* Resolve `{{placeholder}}` tokens in the user's prompt text via the
|
|
162
191
|
* runtime placeholder registry. Returns the input untouched when no
|
|
@@ -403,6 +432,10 @@ export class CoreAiService {
|
|
|
403
432
|
},
|
|
404
433
|
);
|
|
405
434
|
const toolSchemas = this.promptBuilder.buildToolSchemas(tools);
|
|
435
|
+
// A native connection carries the tool definitions in the payload, outside the messages: they occupy the context
|
|
436
|
+
// window all the same, so they come off the input budget (12.3.0).
|
|
437
|
+
const toolTokens = provider.capabilities.nativeTools ? this.estimateTokens(JSON.stringify(toolSchemas)) : 0;
|
|
438
|
+
this.warnOnCrowdedContext(connection, this.estimateTokens(systemPrompt) + toolTokens);
|
|
406
439
|
// The JSON output contract belongs to the EMULATED protocol only: `output_contract`
|
|
407
440
|
// and `tool_protocol_emulated` both carry `capability: 'emulated'`, so a NATIVE-tools
|
|
408
441
|
// run is asked for PROSE and is handed no JSON shape to fill. Forcing
|
|
@@ -475,8 +508,8 @@ export class CoreAiService {
|
|
|
475
508
|
iterations++;
|
|
476
509
|
// Keep the session within the model's context window before every call:
|
|
477
510
|
// LLM-driven compaction first (summarize), then hard trim as a fallback.
|
|
478
|
-
await this.compactMessages(messages, connection);
|
|
479
|
-
this.fitMessagesToContext(messages, connection);
|
|
511
|
+
await this.compactMessages(messages, connection, toolTokens);
|
|
512
|
+
this.fitMessagesToContext(messages, connection, toolTokens);
|
|
480
513
|
const completion = await provider.chat(messages, toolSchemas, {
|
|
481
514
|
...jsonMode,
|
|
482
515
|
maxTokens: connection.defaultMaxTokens,
|
|
@@ -642,8 +675,17 @@ export class CoreAiService {
|
|
|
642
675
|
// Persist the turn pair for multi-turn conversations (not while awaiting confirmation
|
|
643
676
|
// or a pending clarifying question — the user hasn't given a real answer yet).
|
|
644
677
|
if (input.conversationId && this.conversationService && !requiresConfirmation && !pendingQuestion) {
|
|
645
|
-
|
|
646
|
-
await this.conversationService.appendMessage(
|
|
678
|
+
const owner = currentUser?.id;
|
|
679
|
+
await this.conversationService.appendMessage(
|
|
680
|
+
input.conversationId,
|
|
681
|
+
{ content: input.prompt, role: 'user' },
|
|
682
|
+
owner,
|
|
683
|
+
);
|
|
684
|
+
await this.conversationService.appendMessage(
|
|
685
|
+
input.conversationId,
|
|
686
|
+
{ content: finalText, role: 'assistant' },
|
|
687
|
+
owner,
|
|
688
|
+
);
|
|
647
689
|
}
|
|
648
690
|
|
|
649
691
|
await this.audit({
|
|
@@ -676,6 +718,7 @@ export class CoreAiService {
|
|
|
676
718
|
|
|
677
719
|
// 1. Planning call — the model returns the full ordered plan, executes nothing.
|
|
678
720
|
const planSystemPrompt = await this.promptBuilder.buildPlanSystemPrompt(tools, currentUser, { language });
|
|
721
|
+
this.warnOnCrowdedContext(connection, this.estimateTokens(planSystemPrompt));
|
|
679
722
|
const messages: LlmMessage[] = [{ content: planSystemPrompt, role: 'system' }];
|
|
680
723
|
for (const turn of history) {
|
|
681
724
|
messages.push({ content: turn.content, role: turn.role === 'assistant' ? 'assistant' : 'user' });
|
|
@@ -782,7 +825,7 @@ export class CoreAiService {
|
|
|
782
825
|
response.text = finalText;
|
|
783
826
|
response.usage = Object.assign(new CoreAiUsage(), usage);
|
|
784
827
|
|
|
785
|
-
await this.persistTurn(input, response);
|
|
828
|
+
await this.persistTurn(input, response, run);
|
|
786
829
|
await this.audit(this.auditRecord(input, run, response, usage));
|
|
787
830
|
return response;
|
|
788
831
|
}
|
|
@@ -895,9 +938,10 @@ export class CoreAiService {
|
|
|
895
938
|
}
|
|
896
939
|
|
|
897
940
|
/**
|
|
898
|
-
*
|
|
899
|
-
*
|
|
900
|
-
*
|
|
941
|
+
* Authorization for one tool call: the registry role filter (tool absent from the user's set → denied) and the
|
|
942
|
+
* tool's optional `authorize()` data-level check, fail-closed ({@link authorizeToolCall}). Runs before every
|
|
943
|
+
* execution (`executeToolCall()`) and, in plan mode, once more for the whole plan before any step runs. Never
|
|
944
|
+
* mutates anything.
|
|
901
945
|
*/
|
|
902
946
|
protected async authorizeCall(
|
|
903
947
|
call: LlmToolCall,
|
|
@@ -908,11 +952,7 @@ export class CoreAiService {
|
|
|
908
952
|
if (!tool) {
|
|
909
953
|
return { allowed: false, reason: `Unknown or not permitted tool: ${call.name}` };
|
|
910
954
|
}
|
|
911
|
-
|
|
912
|
-
return { allowed: true };
|
|
913
|
-
}
|
|
914
|
-
const result = await tool.authorize(call.arguments ?? {}, context);
|
|
915
|
-
return typeof result === 'boolean' ? { allowed: result } : result;
|
|
955
|
+
return authorizeToolCall(tool, call.arguments ?? {}, context);
|
|
916
956
|
}
|
|
917
957
|
|
|
918
958
|
/**
|
|
@@ -921,18 +961,7 @@ export class CoreAiService {
|
|
|
921
961
|
* client override unless enforced).
|
|
922
962
|
*/
|
|
923
963
|
protected confirmationRequiredFor(tool: IAiTool, input: CoreAiPromptInput): boolean {
|
|
924
|
-
|
|
925
|
-
return true;
|
|
926
|
-
}
|
|
927
|
-
if (!tool.mutating) {
|
|
928
|
-
return false;
|
|
929
|
-
}
|
|
930
|
-
const cfg = ConfigService.get<{ enabled?: boolean; default?: boolean; enforced?: boolean }>(
|
|
931
|
-
'ai.confirmation.mutating',
|
|
932
|
-
);
|
|
933
|
-
const enforced = cfg?.enforced === true;
|
|
934
|
-
const adminDefault = cfg?.default === true;
|
|
935
|
-
return enforced ? true : (input.requireConfirmation ?? adminDefault);
|
|
964
|
+
return toolRequiresConfirmation(tool, input.requireConfirmation);
|
|
936
965
|
}
|
|
937
966
|
|
|
938
967
|
/**
|
|
@@ -1051,13 +1080,22 @@ export class CoreAiService {
|
|
|
1051
1080
|
}
|
|
1052
1081
|
|
|
1053
1082
|
/**
|
|
1054
|
-
* Append the user + assistant turns to the conversation (skipped while awaiting
|
|
1083
|
+
* Append the user + assistant turns to the conversation of the run's user (skipped while awaiting
|
|
1055
1084
|
* confirmation or when denied).
|
|
1056
1085
|
*/
|
|
1057
|
-
protected async persistTurn(input: CoreAiPromptInput, response: CoreAiResponse): Promise<void> {
|
|
1086
|
+
protected async persistTurn(input: CoreAiPromptInput, response: CoreAiResponse, run: AiRunContext): Promise<void> {
|
|
1058
1087
|
if (input.conversationId && this.conversationService && !response.requiresConfirmation && !response.denied) {
|
|
1059
|
-
|
|
1060
|
-
await this.conversationService.appendMessage(
|
|
1088
|
+
const owner = run?.currentUser?.id;
|
|
1089
|
+
await this.conversationService.appendMessage(
|
|
1090
|
+
input.conversationId,
|
|
1091
|
+
{ content: input.prompt, role: 'user' },
|
|
1092
|
+
owner,
|
|
1093
|
+
);
|
|
1094
|
+
await this.conversationService.appendMessage(
|
|
1095
|
+
input.conversationId,
|
|
1096
|
+
{ content: response.text, role: 'assistant' },
|
|
1097
|
+
owner,
|
|
1098
|
+
);
|
|
1061
1099
|
}
|
|
1062
1100
|
}
|
|
1063
1101
|
|
|
@@ -1195,8 +1233,24 @@ export class CoreAiService {
|
|
|
1195
1233
|
}
|
|
1196
1234
|
}
|
|
1197
1235
|
|
|
1236
|
+
// Authorize the arguments that will actually run — after the hooks rewrote them. No learning signal: a refusal
|
|
1237
|
+
// is a decision about this user and these arguments, not a mistake the model should be taught to avoid.
|
|
1238
|
+
const authorization = await this.authorizeCall(call, availableTools, context);
|
|
1239
|
+
if (!authorization.allowed) {
|
|
1240
|
+
action.success = false;
|
|
1241
|
+
action.result = {
|
|
1242
|
+
error: {
|
|
1243
|
+
code: 'TOOL_NOT_AVAILABLE',
|
|
1244
|
+
hint: 'The user may not run this tool with these arguments. Do not retry it; explain this to the user.',
|
|
1245
|
+
message: authorization.reason || `Not permitted: ${call.name}`,
|
|
1246
|
+
},
|
|
1247
|
+
};
|
|
1248
|
+
return action;
|
|
1249
|
+
}
|
|
1250
|
+
const toolContext: AiToolContext = tool.authorize ? { ...context, authorization } : context;
|
|
1251
|
+
|
|
1198
1252
|
try {
|
|
1199
|
-
const raw = await tool.execute(call.arguments ?? {},
|
|
1253
|
+
const raw = await tool.execute(call.arguments ?? {}, toolContext);
|
|
1200
1254
|
let success = true;
|
|
1201
1255
|
let payload: unknown = raw;
|
|
1202
1256
|
let message: string | undefined;
|
|
@@ -1468,6 +1522,47 @@ export class CoreAiService {
|
|
|
1468
1522
|
return Math.max(512, window - reserve - margin);
|
|
1469
1523
|
}
|
|
1470
1524
|
|
|
1525
|
+
/**
|
|
1526
|
+
* The share of {@link contextBudget} the messages may take: what remains after `reservedTokens` — the native tool
|
|
1527
|
+
* definitions, which travel beside the messages but occupy the same window.
|
|
1528
|
+
*/
|
|
1529
|
+
protected messageBudget(connection: ResolvedAiConnection, reservedTokens = 0): number {
|
|
1530
|
+
return Math.max(512, this.contextBudget(connection) - Math.max(0, reservedTokens));
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
/** Share of the input budget the system prompt and tool definitions may take before {@link warnOnCrowdedContext} warns. */
|
|
1534
|
+
protected crowdedContextShare(): number {
|
|
1535
|
+
return 0.5;
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
/**
|
|
1539
|
+
* Warns once per connection and process when the system prompt and the native tool definitions take more than
|
|
1540
|
+
* {@link crowdedContextShare} of the input budget (12.3.0). What remains is all the conversation has: older turns
|
|
1541
|
+
* are dropped early, and a long user message is cut to fit — which, logged on debug only, read as an assistant that
|
|
1542
|
+
* answers generically. Names the remedy: a context window for the connection, or a smaller catalog.
|
|
1543
|
+
*/
|
|
1544
|
+
protected warnOnCrowdedContext(connection: ResolvedAiConnection, fixedTokens: number): void {
|
|
1545
|
+
const budget = this.contextBudget(connection);
|
|
1546
|
+
const key = connection.id ?? connection.name ?? '';
|
|
1547
|
+
const warned = (this.crowdedContextWarned ??= new Set<string>());
|
|
1548
|
+
if (fixedTokens <= budget * this.crowdedContextShare() || warned.has(key)) {
|
|
1549
|
+
return;
|
|
1550
|
+
}
|
|
1551
|
+
if (warned.size >= 1000) {
|
|
1552
|
+
warned.clear();
|
|
1553
|
+
}
|
|
1554
|
+
warned.add(key);
|
|
1555
|
+
const window = connection.contextWindow
|
|
1556
|
+
? `its context window of ${connection.contextWindow}`
|
|
1557
|
+
: `the ai.contextWindow fallback of ${ConfigService.get<number>('ai.contextWindow') ?? 8192} (the connection has no context window of its own)`;
|
|
1558
|
+
this.logger.warn(
|
|
1559
|
+
`AI connection "${connection.name ?? key}": the system prompt and tool definitions take ~${fixedTokens} of ` +
|
|
1560
|
+
`${budget} input tokens (${window}). Little room is left for the conversation: older turns are dropped ` +
|
|
1561
|
+
`early and a long message is cut. Set the connection's contextWindow, or shrink the tool catalog ` +
|
|
1562
|
+
`(ai.deferToolSchemas, fewer tools per role).`,
|
|
1563
|
+
);
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1471
1566
|
/**
|
|
1472
1567
|
* Keep the assembled session messages within the model's context window. Operates
|
|
1473
1568
|
* per user/session: the system prompt (index 0) and the most recent message (the
|
|
@@ -1483,14 +1578,18 @@ export class CoreAiService {
|
|
|
1483
1578
|
*
|
|
1484
1579
|
* Enabled by `ai.compaction !== false`. When OFF, only the hard trim is used.
|
|
1485
1580
|
*/
|
|
1486
|
-
protected async compactMessages(
|
|
1581
|
+
protected async compactMessages(
|
|
1582
|
+
messages: LlmMessage[],
|
|
1583
|
+
connection: ResolvedAiConnection,
|
|
1584
|
+
reservedTokens = 0,
|
|
1585
|
+
): Promise<number> {
|
|
1487
1586
|
if (ConfigService.get<boolean>('ai.compaction') === false) {
|
|
1488
1587
|
return 0;
|
|
1489
1588
|
}
|
|
1490
1589
|
if (messages.length <= 3) {
|
|
1491
1590
|
return 0;
|
|
1492
1591
|
}
|
|
1493
|
-
const budget = this.
|
|
1592
|
+
const budget = this.messageBudget(connection, reservedTokens);
|
|
1494
1593
|
const total = (): number => messages.reduce((sum, m) => sum + this.estimateTokens(m.content) + 4, 0);
|
|
1495
1594
|
if (total() <= budget) {
|
|
1496
1595
|
return 0;
|
|
@@ -1539,11 +1638,11 @@ export class CoreAiService {
|
|
|
1539
1638
|
}
|
|
1540
1639
|
}
|
|
1541
1640
|
|
|
1542
|
-
protected fitMessagesToContext(messages: LlmMessage[], connection: ResolvedAiConnection): void {
|
|
1641
|
+
protected fitMessagesToContext(messages: LlmMessage[], connection: ResolvedAiConnection, reservedTokens = 0): void {
|
|
1543
1642
|
if (messages.length === 0) {
|
|
1544
1643
|
return;
|
|
1545
1644
|
}
|
|
1546
|
-
const budget = this.
|
|
1645
|
+
const budget = this.messageBudget(connection, reservedTokens);
|
|
1547
1646
|
const total = (): number => messages.reduce((sum, m) => sum + this.estimateTokens(m.content) + 4, 0);
|
|
1548
1647
|
let dropped = 0;
|
|
1549
1648
|
// Drop oldest non-system, non-last messages until within budget.
|
|
@@ -199,8 +199,10 @@ it has no signed assertions.
|
|
|
199
199
|
`betterAuth.emailAndPassword.revokeSessionsOnPasswordReset` is on. With it off — and on a password
|
|
200
200
|
CHANGE — user tokens survive, like personal access tokens elsewhere; after a suspected compromise
|
|
201
201
|
call `revokeAllForUser(userId)`. Tenant tokens are never touched by a user's reset.
|
|
202
|
-
- **Deleting a tenant.** Call `
|
|
203
|
-
|
|
202
|
+
- **Deleting a tenant.** Call `CoreTenantService.purgeTenantData(tenantId)`, which deletes the tokens
|
|
203
|
+
with the rest of the tenant's data, or `deleteAllForTenant(tenantId)` for the tokens alone — nothing
|
|
204
|
+
does it for you, and until you do, the tenant's tokens keep authenticating (the core has no tenant
|
|
205
|
+
model to notice the deletion).
|
|
204
206
|
- **Banned users.** If you add Better-Auth's admin plugin, override `loadTokenUser()` so a banned
|
|
205
207
|
user's tokens are refused as well — the framework does not know that flag.
|
|
206
208
|
- **Brute force.** Failed attempts are not limited per IP (guessing 256 bits is not the risk); add an
|
|
@@ -472,7 +472,7 @@ const config = {
|
|
|
472
472
|
template: 'email-verification', // Default: 'email-verification'
|
|
473
473
|
expiresIn: 86400, // Default: 86400 (24 hours)
|
|
474
474
|
callbackURL: '/auth/verify-email', // Frontend verification page
|
|
475
|
-
autoSignInAfterVerification:
|
|
475
|
+
autoSignInAfterVerification: false, // Default: false — true signs in whoever opens the link (see README)
|
|
476
476
|
resendCooldownSeconds: 60, // Default: 60
|
|
477
477
|
},
|
|
478
478
|
},
|
|
@@ -273,6 +273,10 @@ Email verification is **enabled by default** via Better-Auth's `emailVerificatio
|
|
|
273
273
|
- Users receive a verification email after sign-up
|
|
274
274
|
- Email contains a verification link with token
|
|
275
275
|
- After verification, `verifiedAt` is automatically set on the user
|
|
276
|
+
- The user is **not** signed in by the verification link and signs in once afterwards (12.3.0+, as in Better-Auth).
|
|
277
|
+
`emailVerification: { autoSignInAfterVerification: true }` signs in whoever opens the link, to the link owner's
|
|
278
|
+
account — a forwarded link signs its recipient in to the sender's account. Your frontend's verify page should lead
|
|
279
|
+
to the sign-in rather than expect a session
|
|
276
280
|
- Templates are provided in German and English
|
|
277
281
|
|
|
278
282
|
### Configuration
|
|
@@ -448,6 +452,9 @@ After integration, verify:
|
|
|
448
452
|
- [ ] GraphQL `betterAuthSignUp` returns error `LTNS_0026`
|
|
449
453
|
- [ ] `GET /iam/features` reports `signUpEnabled: false`
|
|
450
454
|
- [ ] Sign-in still works for existing users
|
|
455
|
+
- [ ] With a social provider: a first social sign-in of an unknown user creates no account (`error=signup_disabled`),
|
|
456
|
+
an existing user still signs in through it. A provider meant for self-registration carries
|
|
457
|
+
`disableSignUp: false` (12.3.0+)
|
|
451
458
|
|
|
452
459
|
### Additional checks for Migration scenario:
|
|
453
460
|
|
|
@@ -550,6 +550,13 @@ const config = {
|
|
|
550
550
|
|
|
551
551
|
**Brevo Integration:** When `brevoTemplateId` is set and Brevo is configured (`config.brevo`), verification emails are sent via Brevo's transactional API instead of SMTP/EJS templates. The same template variables (`name`, `link`, `appName`, `expiresIn`) are passed to the Brevo template.
|
|
552
552
|
|
|
553
|
+
**Auto sign-in after verification (`autoSignInAfterVerification`, default `false` since 12.3.0):** with it on,
|
|
554
|
+
`GET /iam/verify-email` signs in the browser that opens the link — to the account the link belongs to, whoever opens
|
|
555
|
+
it. Someone who signs up with their own address and forwards the unopened link then has the recipient's browser signed
|
|
556
|
+
in to their account, and what the recipient enters there lands with them (login CSRF). Off, the user verifies and then
|
|
557
|
+
signs in once, as in Better-Auth's own default. Switch it on (`emailVerification: { autoSignInAfterVerification: true }`)
|
|
558
|
+
only where a verification link reaches nobody but its owner.
|
|
559
|
+
|
|
553
560
|
### Sign-Up Checks (v11.13.0+)
|
|
554
561
|
|
|
555
562
|
Sign-up validation is **enabled by default** requiring `termsAndPrivacyAccepted: true`.
|
|
@@ -622,13 +629,31 @@ When disabled:
|
|
|
622
629
|
- REST `POST /iam/sign-up/email` returns `400 Bad Request` with error `LTNS_0026`
|
|
623
630
|
- GraphQL `betterAuthSignUp` mutation returns error `LTNS_0026`
|
|
624
631
|
- `betterAuthFeatures` reports `signUpEnabled: false`
|
|
625
|
-
-
|
|
632
|
+
- A first sign-in through a social provider creates no account (since 12.3.0): the OAuth callback redirects with
|
|
633
|
+
`error=signup_disabled`, `/sign-in/social` with an `idToken` answers 401 `signup disabled`
|
|
634
|
+
- Sign-in continues to work for existing users, through e-mail and every social provider
|
|
626
635
|
|
|
627
636
|
**Defense in Depth:** The flag is enforced at two layers:
|
|
628
637
|
|
|
629
638
|
1. **Custom check** (`CoreBetterAuthService.ensureSignUpEnabled()`) runs in Controller/Resolver _before_ any BetterAuth API call and returns a structured `LTNS_0026` error.
|
|
630
639
|
2. **Native BetterAuth** `emailAndPassword.disableSignUp` acts as a safety net for any direct API access that bypasses the custom check.
|
|
631
640
|
|
|
641
|
+
**Social providers:** Better-Auth reads `emailAndPassword.disableSignUp` for `/sign-up/email` only and decides a social
|
|
642
|
+
sign-up per provider, so `buildSocialProviders()` hands every provider the flag as its own `disableSignUp`. One provider
|
|
643
|
+
can deviate on purpose:
|
|
644
|
+
|
|
645
|
+
```typescript
|
|
646
|
+
socialProviders: {
|
|
647
|
+
google: { clientId: '...', clientSecret: '...', disableSignUp: false }, // new users through Google despite the global flag
|
|
648
|
+
github: { clientId: '...', clientSecret: '...', disableSignUp: true }, // closed for GitHub even while sign-up is open
|
|
649
|
+
},
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
Only a boolean counts on the provider; any other value follows `emailAndPassword.disableSignUp`, so a string from
|
|
653
|
+
`NEST_SERVER_CONFIG` cannot open a closed registration. Plugins a project adds itself through `betterAuth.options.plugins`
|
|
654
|
+
(Generic OAuth, Magic Link, …) are not covered and need their own sign-up options. Before 12.3.0 every provider created
|
|
655
|
+
accounts whatever `disableSignUp` said.
|
|
656
|
+
|
|
632
657
|
**Default:** `false` (sign-up enabled) - fully backward compatible.
|
|
633
658
|
|
|
634
659
|
### Additional User Fields
|
|
@@ -2205,6 +2230,12 @@ These endpoints bypass rate limiting entirely:
|
|
|
2205
2230
|
- `/session` - Session checks (frequent client-side calls)
|
|
2206
2231
|
- `/callback` - OAuth callbacks
|
|
2207
2232
|
|
|
2233
|
+
Entries match whole path segments of the path the request is served under (dot segments resolved, case-insensitive, as Express routes): `/sign-in` covers
|
|
2234
|
+
`/sign-in/email`, `/session` does not cover `/sessions-export`. A request whose path under the base path carries a
|
|
2235
|
+
`.` or `..` segment (also `%2e`-encoded), a backslash, an encoded `/` or `\`, or a `#` is refused with 400
|
|
2236
|
+
(`#LTNS_0305: Invalid request path`) before the limiter runs — no route needs such a path, and the limiter and
|
|
2237
|
+
Better-Auth would otherwise read it as two different routes.
|
|
2238
|
+
|
|
2208
2239
|
### Response Headers
|
|
2209
2240
|
|
|
2210
2241
|
When rate limiting is enabled, the following headers are added to responses:
|
|
@@ -444,6 +444,7 @@ interface PasskeyNormalizationResult {
|
|
|
444
444
|
interface SocialProviderConfig {
|
|
445
445
|
clientId: string;
|
|
446
446
|
clientSecret: string;
|
|
447
|
+
disableSignUp: boolean;
|
|
447
448
|
}
|
|
448
449
|
|
|
449
450
|
/**
|
|
@@ -955,7 +956,9 @@ function buildEmailVerificationConfig(
|
|
|
955
956
|
// Get configuration values (with defaults)
|
|
956
957
|
const configObj = typeof emailVerificationConfig === 'object' ? emailVerificationConfig : {};
|
|
957
958
|
const expiresIn = configObj.expiresIn ?? 86400; // 24 hours
|
|
958
|
-
|
|
959
|
+
// Off unless switched on, as Better-Auth has it: on verification Better-Auth signs in the browser that opens the
|
|
960
|
+
// link, to the account the link belongs to — a forwarded link signs its recipient in to the sender's account
|
|
961
|
+
const autoSignInAfterVerification = configObj.autoSignInAfterVerification === true;
|
|
959
962
|
|
|
960
963
|
const result: Record<string, unknown> = {
|
|
961
964
|
autoSignInAfterVerification,
|
|
@@ -1118,9 +1121,16 @@ function serverOnlyPlugin<T extends BetterAuthPlugin>(plugin: T): T {
|
|
|
1118
1121
|
* Builds the social providers configuration object dynamically.
|
|
1119
1122
|
* Iterates over all configured providers and includes those that are enabled
|
|
1120
1123
|
* with valid clientId and clientSecret.
|
|
1124
|
+
*
|
|
1125
|
+
* `emailAndPassword.disableSignUp: true` closes every way to a new account (12.3.0). Better-Auth reads that flag for
|
|
1126
|
+
* `/sign-up/email` only and decides a social sign-up per provider (`provider.options.disableSignUp`, in the OAuth
|
|
1127
|
+
* callback and in `/sign-in/social` with an `idToken`), so each provider receives it explicitly. A boolean
|
|
1128
|
+
* `disableSignUp` on the provider is the deliberate exception for that provider; any other value follows the global
|
|
1129
|
+
* flag, so a string arriving through `NEST_SERVER_CONFIG` cannot open a closed registration.
|
|
1121
1130
|
*/
|
|
1122
1131
|
function buildSocialProviders(config: IBetterAuth): Record<string, SocialProviderConfig> {
|
|
1123
1132
|
const socialProvidersConfig: Record<string, SocialProviderConfig> = {};
|
|
1133
|
+
const signUpClosed = config.emailAndPassword?.disableSignUp === true;
|
|
1124
1134
|
|
|
1125
1135
|
// Iterate over all configured social providers dynamically
|
|
1126
1136
|
// A provider is enabled by default if it has credentials configured
|
|
@@ -1131,6 +1141,7 @@ function buildSocialProviders(config: IBetterAuth): Record<string, SocialProvide
|
|
|
1131
1141
|
socialProvidersConfig[name] = {
|
|
1132
1142
|
clientId: provider.clientId,
|
|
1133
1143
|
clientSecret: provider.clientSecret,
|
|
1144
|
+
disableSignUp: typeof provider.disableSignUp === 'boolean' ? provider.disableSignUp : signUpClosed,
|
|
1134
1145
|
};
|
|
1135
1146
|
}
|
|
1136
1147
|
}
|
|
@@ -38,7 +38,7 @@ type ResolvedEmailVerificationConfig = Pick<
|
|
|
38
38
|
* Default configuration for email verification
|
|
39
39
|
*/
|
|
40
40
|
const DEFAULT_CONFIG: ResolvedEmailVerificationConfig = {
|
|
41
|
-
autoSignInAfterVerification:
|
|
41
|
+
autoSignInAfterVerification: false,
|
|
42
42
|
callbackURL: '/auth/verify-email',
|
|
43
43
|
enabled: true,
|
|
44
44
|
expiresIn: 86400, // 24 hours in seconds
|
|
@@ -192,9 +192,12 @@ export class CoreBetterAuthEmailVerificationService {
|
|
|
192
192
|
|
|
193
193
|
/**
|
|
194
194
|
* Check if auto sign-in after verification is enabled
|
|
195
|
+
*
|
|
196
|
+
* `=== true` as in `better-auth.config.ts`, which decides what Better-Auth does: a value arriving as
|
|
197
|
+
* a string in `NEST_SERVER_CONFIG` must not report a sign-in that Better-Auth never performs.
|
|
195
198
|
*/
|
|
196
199
|
shouldAutoSignIn(): boolean {
|
|
197
|
-
return this.config.autoSignInAfterVerification;
|
|
200
|
+
return this.config.autoSignInAfterVerification === true;
|
|
198
201
|
}
|
|
199
202
|
|
|
200
203
|
/**
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { BadRequestException, Injectable, NestMiddleware } from '@nestjs/common';
|
|
2
|
+
import type { NextFunction, Request, Response } from 'express';
|
|
3
|
+
|
|
4
|
+
import { ErrorCode } from '../error-code/error-codes.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Whether a raw request path contains something URL resolution rewrites: a `.` or `..` segment (also
|
|
8
|
+
* percent-encoded), a backslash (a segment separator for WHATWG URLs), an encoded slash or backslash, or a `#`
|
|
9
|
+
* (Express routes the part before it, the forwarding to Better-Auth reads the whole string).
|
|
10
|
+
*
|
|
11
|
+
* Every check in front of Better-Auth (rate limiter, controller routes, password routes) reads the RAW path, while
|
|
12
|
+
* Better-Auth routes the path `new URL()` resolved. A path the two read differently passes those checks as one route
|
|
13
|
+
* and runs as another. No route under the Better-Auth base path needs any of these, so such a request is refused.
|
|
14
|
+
*
|
|
15
|
+
* A loop over segments, no regex over the whole path: the path comes from an unauthenticated request and may be as
|
|
16
|
+
* long as Node's header limit allows.
|
|
17
|
+
*/
|
|
18
|
+
export function hasNonCanonicalPathSegments(path: string): boolean {
|
|
19
|
+
if (path.includes('\\') || path.includes('#')) {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
for (const segment of path.split('/')) {
|
|
23
|
+
if (segment.includes('%')) {
|
|
24
|
+
const lower = segment.toLowerCase();
|
|
25
|
+
if (lower.includes('%2f') || lower.includes('%5c')) {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
// The longest dot segment is `%2e%2e`
|
|
30
|
+
if (segment.length === 0 || segment.length > 6) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
const dots = segment.replace(/%2e/gi, '.');
|
|
34
|
+
if (dots === '.' || dots === '..') {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The path as Better-Auth routes it: dot segments resolved (also percent-encoded ones), backslashes read as slashes —
|
|
43
|
+
* what `new URL()` makes of it. Query string and fragment are dropped. A path `new URL()` refuses comes back as given.
|
|
44
|
+
*/
|
|
45
|
+
export function canonicalRequestPath(path: string): string {
|
|
46
|
+
try {
|
|
47
|
+
return new URL(path, 'http://canonical.invalid').pathname;
|
|
48
|
+
} catch {
|
|
49
|
+
return path;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Whether `path` contains the segments of `endpoint` as a contiguous run of WHOLE segments: `/sign-in` matches
|
|
55
|
+
* `/sign-in/email` and `/phone-number/sign-in`, never `/sign-in-legacy`. Segments are compared as they are; pass a
|
|
56
|
+
* {@link canonicalRequestPath} result, so `..` is already resolved.
|
|
57
|
+
*/
|
|
58
|
+
export function pathContainsSegments(path: string, endpoint: string): boolean {
|
|
59
|
+
const wanted = endpoint.split('/').filter(Boolean);
|
|
60
|
+
if (wanted.length === 0) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
const segments = path.split('/').filter(Boolean);
|
|
64
|
+
for (let start = 0; start + wanted.length <= segments.length; start++) {
|
|
65
|
+
if (wanted.every((segment, offset) => segments[start + offset] === segment)) {
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* First middleware under the Better-Auth base path: refuses a request whose path is not in canonical form (see
|
|
74
|
+
* {@link hasNonCanonicalPathSegments}) with 400, before any check reads that path.
|
|
75
|
+
*/
|
|
76
|
+
@Injectable()
|
|
77
|
+
export class CoreBetterAuthPathGuardMiddleware implements NestMiddleware {
|
|
78
|
+
use(req: Request, _res: Response, next: NextFunction) {
|
|
79
|
+
const path = (req.originalUrl || req.url || '').split('?')[0];
|
|
80
|
+
if (hasNonCanonicalPathSegments(path)) {
|
|
81
|
+
throw new BadRequestException(ErrorCode.INVALID_REQUEST_PATH);
|
|
82
|
+
}
|
|
83
|
+
next();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -50,9 +50,12 @@ export class CoreBetterAuthRateLimitMiddleware implements NestMiddleware {
|
|
|
50
50
|
// Get client IP
|
|
51
51
|
const ip = this.getClientIp(req);
|
|
52
52
|
|
|
53
|
-
//
|
|
53
|
+
// The path relative to basePath, read from `originalUrl`: Nest mounts this middleware on `${basePath}/*path`, and
|
|
54
|
+
// Express moves the whole match into `req.baseUrl`, so `req.path` is "/" for every request here. Reading it made
|
|
55
|
+
// every IAM request of one client share a single counter with the full limit — no skip list, no strict limit
|
|
54
56
|
const basePath = this.betterAuthService.getBasePath();
|
|
55
|
-
const
|
|
57
|
+
const requestPath = (req.originalUrl || req.url || '').split('?')[0];
|
|
58
|
+
const path = requestPath.startsWith(basePath) ? requestPath.substring(basePath.length) : requestPath;
|
|
56
59
|
|
|
57
60
|
// Check rate limit
|
|
58
61
|
const result = await this.rateLimiter.check(ip, path);
|
|
@@ -2,6 +2,7 @@ import { Injectable, Logger, Optional } from '@nestjs/common';
|
|
|
2
2
|
|
|
3
3
|
import { IBetterAuthRateLimit } from '../../common/interfaces/server-options.interface.js';
|
|
4
4
|
import { CoreRedisService } from '../../common/services/core-redis.service.js';
|
|
5
|
+
import { canonicalRequestPath, pathContainsSegments } from './core-better-auth-path-guard.middleware.js';
|
|
5
6
|
import {
|
|
6
7
|
InMemoryRateLimitStore,
|
|
7
8
|
rateLimitKey,
|
|
@@ -134,10 +135,10 @@ export class CoreBetterAuthRateLimiter {
|
|
|
134
135
|
* Check if a request is allowed under the rate limit
|
|
135
136
|
*
|
|
136
137
|
* @param ip - Client IP address
|
|
137
|
-
* @param
|
|
138
|
+
* @param requestPath - Request path (relative to basePath), as sent; dot segments are resolved here
|
|
138
139
|
* @returns Rate limit check result
|
|
139
140
|
*/
|
|
140
|
-
async check(ip: string,
|
|
141
|
+
async check(ip: string, requestPath: string): Promise<RateLimitResult> {
|
|
141
142
|
// If rate limiting is disabled, always allow
|
|
142
143
|
if (!this.config.enabled) {
|
|
143
144
|
return {
|
|
@@ -149,6 +150,13 @@ export class CoreBetterAuthRateLimiter {
|
|
|
149
150
|
};
|
|
150
151
|
}
|
|
151
152
|
|
|
153
|
+
// Decide on the path the request is served under, not the one as sent: `/callback/../sign-in/email` reaches the
|
|
154
|
+
// sign-in handler, so it must not be skipped as a callback (CoreBetterAuthPathGuardMiddleware refuses such paths
|
|
155
|
+
// before they get here; this keeps the limiter correct where that guard is not registered). Lower-cased because
|
|
156
|
+
// Express routes case-insensitively: `/sign-in/EMAIL` is the same sign-in and must share its counter and its
|
|
157
|
+
// strict limit
|
|
158
|
+
const path = canonicalRequestPath(requestPath).toLowerCase();
|
|
159
|
+
|
|
152
160
|
// Check if this endpoint should skip rate limiting
|
|
153
161
|
if (this.shouldSkip(path)) {
|
|
154
162
|
return {
|
|
@@ -249,10 +257,11 @@ export class CoreBetterAuthRateLimiter {
|
|
|
249
257
|
}
|
|
250
258
|
|
|
251
259
|
/**
|
|
252
|
-
* Determine if an endpoint should skip rate limiting
|
|
260
|
+
* Determine if an endpoint should skip rate limiting: an entry matches whole path segments, so `/session` skips
|
|
261
|
+
* `/session` but not `/sessions-export`
|
|
253
262
|
*/
|
|
254
263
|
private shouldSkip(path: string): boolean {
|
|
255
|
-
return this.config.skipEndpoints.some((skip) => path
|
|
264
|
+
return this.config.skipEndpoints.some((skip) => pathContainsSegments(path, skip.toLowerCase()));
|
|
256
265
|
}
|
|
257
266
|
|
|
258
267
|
/**
|
|
@@ -260,9 +269,7 @@ export class CoreBetterAuthRateLimiter {
|
|
|
260
269
|
* Strict endpoints get half the normal limit
|
|
261
270
|
*/
|
|
262
271
|
private getLimit(path: string): number {
|
|
263
|
-
const isStrict = this.config.strictEndpoints.some(
|
|
264
|
-
(strict) => path === strict || path.endsWith(strict) || path.includes(strict),
|
|
265
|
-
);
|
|
272
|
+
const isStrict = this.config.strictEndpoints.some((strict) => pathContainsSegments(path, strict.toLowerCase()));
|
|
266
273
|
|
|
267
274
|
return isStrict ? Math.ceil(this.config.max / 2) : this.config.max;
|
|
268
275
|
}
|