@lenne.tech/nest-server 11.42.15 → 11.43.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/better-auth.md +22 -0
- package/.claude/rules/configurable-features.md +4 -3
- package/.claude/rules/testing.md +4 -4
- package/FRAMEWORK-API.md +2 -2
- package/dist/core/common/filters/mongoose-cast-error.filter.d.ts +17 -0
- package/dist/core/common/filters/mongoose-cast-error.filter.js +171 -0
- package/dist/core/common/filters/mongoose-cast-error.filter.js.map +1 -0
- package/dist/core/common/interfaces/server-options.interface.d.ts +4 -0
- package/dist/core/modules/ai/core-ai-mcp-oauth.initializer.d.ts +19 -0
- package/dist/core/modules/ai/core-ai-mcp-oauth.initializer.js +145 -0
- package/dist/core/modules/ai/core-ai-mcp-oauth.initializer.js.map +1 -0
- package/dist/core/modules/ai/core-ai-mcp.controller.js +6 -5
- package/dist/core/modules/ai/core-ai-mcp.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 +6 -1
- package/dist/core/modules/ai/core-ai.module.js.map +1 -1
- package/dist/core/modules/ai/helpers/ai-mcp-oauth-consent.helper.d.ts +33 -0
- package/dist/core/modules/ai/helpers/ai-mcp-oauth-consent.helper.js +282 -0
- package/dist/core/modules/ai/helpers/ai-mcp-oauth-consent.helper.js.map +1 -0
- package/dist/core/modules/ai/helpers/ai-mcp-oauth.helper.d.ts +15 -2
- package/dist/core/modules/ai/helpers/ai-mcp-oauth.helper.js +73 -19
- package/dist/core/modules/ai/helpers/ai-mcp-oauth.helper.js.map +1 -1
- package/dist/core/modules/ai/helpers/ai-mcp-oauth.registry.d.ts +5 -0
- package/dist/core/modules/ai/helpers/ai-mcp-oauth.registry.js.map +1 -1
- package/dist/core/modules/ai/index.d.ts +1 -0
- package/dist/core/modules/ai/index.js +1 -0
- package/dist/core/modules/ai/index.js.map +1 -1
- package/dist/core/modules/ai/services/core-ai-mcp-oauth.service.d.ts +30 -2
- package/dist/core/modules/ai/services/core-ai-mcp-oauth.service.js +222 -15
- package/dist/core/modules/ai/services/core-ai-mcp-oauth.service.js.map +1 -1
- package/dist/core/modules/better-auth/better-auth.config.d.ts +4 -0
- package/dist/core/modules/better-auth/better-auth.config.js +29 -2
- package/dist/core/modules/better-auth/better-auth.config.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.controller.d.ts +2 -0
- package/dist/core/modules/better-auth/core-better-auth.controller.js +36 -1
- package/dist/core/modules/better-auth/core-better-auth.controller.js.map +1 -1
- package/dist/core.module.js +7 -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/tsconfig.build.tsbuildinfo +1 -1
- package/docs/REQUEST-LIFECYCLE.md +30 -1
- package/docs/security-overrides.md +3 -2
- package/migration-guides/11.42.15-to-11.42.16.md +114 -0
- package/migration-guides/11.42.x-to-11.43.x.md +195 -0
- package/package.json +7 -7
- package/src/core/common/filters/mongoose-cast-error.filter.ts +310 -0
- package/src/core/common/helpers/swagger.helper.ts +4 -4
- package/src/core/common/interfaces/server-options.interface.ts +59 -9
- package/src/core/modules/ai/INTEGRATION-CHECKLIST.md +50 -24
- package/src/core/modules/ai/README.md +98 -13
- package/src/core/modules/ai/core-ai-mcp-oauth.initializer.ts +199 -0
- package/src/core/modules/ai/core-ai-mcp.controller.ts +13 -6
- package/src/core/modules/ai/core-ai.module.ts +14 -2
- package/src/core/modules/ai/helpers/ai-mcp-oauth-consent.helper.ts +369 -0
- package/src/core/modules/ai/helpers/ai-mcp-oauth.helper.ts +149 -43
- package/src/core/modules/ai/helpers/ai-mcp-oauth.registry.ts +18 -5
- package/src/core/modules/ai/index.ts +1 -0
- package/src/core/modules/ai/services/core-ai-mcp-oauth.service.ts +385 -28
- package/src/core/modules/better-auth/better-auth.config.ts +49 -5
- package/src/core/modules/better-auth/core-better-auth.controller.ts +75 -1
- package/src/core.module.ts +13 -1
- package/src/index.ts +1 -0
|
@@ -483,6 +483,27 @@ normalise them away; mutation `openapi-plugin-routable` drops `serverOnlyPlugin(
|
|
|
483
483
|
red) and the "framework routes" cases in `tests/unit/swagger-helper.spec.ts`, which run against the
|
|
484
484
|
REAL generator output rather than a fixture.
|
|
485
485
|
|
|
486
|
+
## 10. Direct `api.*` calls must hand Better-Auth the request — or its CSRF protection does not run
|
|
487
|
+
|
|
488
|
+
`signInEmail` and `signUpEmail` carry Better-Auth's `formCsrfMiddleware`: it refuses a cross-site navigation
|
|
489
|
+
(`Sec-Fetch-Site: cross-site` + `Sec-Fetch-Mode: navigate`) and validates a present `Origin` / `Referer` against
|
|
490
|
+
`trustedOrigins`. It reads them from `ctx.request`. `CoreBetterAuthController` called both via `api.*({ body })` — no
|
|
491
|
+
request, so the check was skipped and a hostile page could sign a visitor in to the attacker's account (login CSRF,
|
|
492
|
+
fixed in 11.43.0; on the MCP consent page that turned into "approve a client for the attacker's account").
|
|
493
|
+
|
|
494
|
+
| Rule | Why |
|
|
495
|
+
|------|-----|
|
|
496
|
+
| Pass `request: this.csrfCheckedRequest(req, '<path>')` AND `asResponse: false` | with a request Better-Auth answers a `Response` by default; the controller reads the data |
|
|
497
|
+
| Forward `Origin`, `Referer`, `Sec-Fetch-*` only — never the cookie | a cookie alone makes Better-Auth demand an `Origin`, refusing non-browser clients with a cookie jar |
|
|
498
|
+
| Forward them only from a BROWSER request: one carrying `Origin`, `Referer` or `Sec-Fetch-Site` | Node's built-in `fetch` (undici) sends a lone `Sec-Fetch-Mode: cors` and no `Origin`; Better-Auth reads any fetch metadata without an origin as `MISSING_OR_NULL_ORIGIN`, so forwarding it unconditionally answered every server-side `fetch` sign-in with 403. A browser always sends `Origin` on a POST and `Sec-Fetch-Site` with its metadata, so nothing a hostile page can make it send escapes the check (mutations `iam-signin-csrf-nonbrowser-metadata-forwarded`, `iam-signin-csrf-site-not-a-browser-signal`) |
|
|
499
|
+
| Use `globalThis.Request`, not `Request` | the file imports Express's `Request` type; under Vitest (Vite SSR) the bare name resolves to that import, which is `undefined` at runtime — tsc happens to emit the global |
|
|
500
|
+
| Map `CROSS_SITE_NAVIGATION_LOGIN_BLOCKED` / `INVALID_ORIGIN` / `MISSING_OR_NULL_ORIGIN` to 403 (`rethrowCsrfRefusal`) | "invalid credentials" would send whoever debugs a missing `trustedOrigins` entry looking at the password |
|
|
501
|
+
|
|
502
|
+
**Tests:** Better-Auth skips its origin/CSRF checks under a test runner (`skipOriginCheck: isTest()`). A spec that pins
|
|
503
|
+
this protection must set `betterAuth.options.advanced.disableOriginCheck: false` — otherwise it is green with the check
|
|
504
|
+
removed. `tests/better-auth-signin-csrf.e2e-spec.ts` does; mutations `iam-signin-csrf-check-skipped` /
|
|
505
|
+
`iam-signup-csrf-check-skipped`.
|
|
506
|
+
|
|
486
507
|
## Summary
|
|
487
508
|
|
|
488
509
|
| Principle | Requirement |
|
|
@@ -496,3 +517,4 @@ REAL generator output rather than a fixture.
|
|
|
496
517
|
| Session cookie | Always the opaque session token — never the body JWT (§7) |
|
|
497
518
|
| `account` reads / writes | Reads filter on `providerId` alone, never `issuer`. Writes set `issuer` only on better-auth 1.7.0–1.7.2, via the helper (§8) |
|
|
498
519
|
| `openAPI()` plugin | Registered internally for `setupSwagger()`, kept off the router (`SERVER_ONLY`) unless the project registered it — never by a path check (§9) |
|
|
520
|
+
| Direct `api.*` calls | Hand Better-Auth the request (CSRF headers only) + `asResponse: false`, or its login-CSRF check is skipped (§10) |
|
|
@@ -319,6 +319,7 @@ This pattern is currently applied to:
|
|
|
319
319
|
| Mongoose Audit Fields Plugin | `security.mongooseAuditFieldsPlugin` | Boolean Shorthand | `true` (enabled) |
|
|
320
320
|
| Response Model Interceptor | `security.responseModelInterceptor` | Boolean Shorthand | `true` (enabled), `debug: false` |
|
|
321
321
|
| Translate Response Interceptor | `security.translateResponseInterceptor` | Boolean Shorthand | `true` (enabled) |
|
|
322
|
+
| Mongoose CastError Filter | `security.mongooseCastErrorFilter` | Explicit Boolean — **safe default** | **`true` since 11.42.16.** `MongooseCastErrorFilter`, registered by `CoreModule` as `APP_FILTER` (so `main.ts` and `Test.createTestingModule()` setups need nothing). A malformed id reached Mongoose and its `CastError` — not an `HttpException` — escaped every `@Catch(HttpException)` filter, so `GET /users/<malformed-id>` and every other `:id` route answered **500**. The filter decides by WHERE the failing value came from: a route parameter or a top-level GraphQL argument → **404** `ErrorCode.RESOURCE_NOT_FOUND` (answered like an unknown id); the query string, an update or filter body, or a nested GraphQL input → **400** `ErrorCode.INVALID_FIELD_FORMAT [<path>]`; a value the request does not contain (a broken stored id, a server-computed value) → **stays a 500**, with Nest's own body and log line (context `ExceptionsHandler`), because that is a data or code defect monitoring must keep seeing — relabelling it as a client error is the mistake the refusal tests pin (mutations `cast-error-server-value-relabelled-*`). Only primitives and arrays of primitives (`$in`) are matched: an object cast as a whole yields no candidates, so its inner strings cannot match the body by coincidence. Never sends the Mongoose message (it names the model). REST bodies use the `HttpExceptionLogFilter` shape (`{ ...exception }`). A project's own catch-all filter via `app.useGlobalFilters()` is consulted first and wins. Covers casts Mongoose raises while running a QUERY or an UPDATE (`findById`, filters, `findByIdAndUpdate` — `CrudService.update()`). NOT covered, both stay a 500: a malformed id in a document being CREATED (`save()` / `create()` / `insertMany()` — `CrudService.create()`), which Mongoose reports as a `ValidationError` wrapping the CastError (validate in the input: `validator: () => [IsMongoId()]`); and a `BSONError` from `new Types.ObjectId(bad)`, which carries no value (check the id in the service, `CoreFileService.isMalformedFileId()`). `false` restores the 500. Implementation: `common/filters/mongoose-cast-error.filter.ts` |
|
|
322
323
|
| Secret Fields Removal | `security.secretFields` | Array | `['password', 'verificationToken', ...]` |
|
|
323
324
|
| Multi-Tenancy | `multiTenancy` | Presence Implies Enabled | **`excludeSchemas` is an OFF SWITCH for isolation, per model** — a listed schema gets no tenant filter at all; since 11.35.0 the plugin WARNS once per model when the excluded schema declares `tenantId` (i.e. was built for isolation). The framework's own docs recommended `['User', 'Session']` until then; that is withdrawn — it is only right for the global-user model. Since 11.35.0 GraphQL operations over the WEBSOCKET also run inside a `RequestContext` (`CoreModule` installs a context-aware `execute`/`subscribe` pair inside `subscriptions`, where `ApolloDriver` forwards it): the tenant comes from the handshake header validated against an active membership, else from the subscriber's memberships, and an unresolvable tenant leaves the safety net to refuse the read rather than returning everything. `headerName: 'x-tenant-id'`, `membershipModel: 'TenantMember'`, `adminBypass: true`, `excludeSchemas: []`, `roleHierarchy: { member: 1, manager: 2, owner: 3 }`, `cacheTtlMs: 30000` (0 disables, process-local). System roles (`S_EVERYONE`, `S_USER`, `S_VERIFIED`) are checked as OR alternatives before real roles; method-level system roles take precedence; membership validated for context when system role grants access + header present. Hierarchy roles use level comparison, normal roles use exact match. Use `DefaultHR` or `createHierarchyRoles()` for type-safe role constants. Bypass: `RequestContext.runWithBypassTenantGuard()`. Cache invalidation: `invalidateUser(userId)` / `invalidateAll()` are **instance** methods on the singleton `CoreTenantGuard` — inject it and call `this.tenantGuard?.invalidateUser(userId)`, never `CoreTenantGuard.invalidateUser(...)` (there is no static). With `redis` configured, both additionally BROADCAST the invalidation to every replica (`<keyPrefix>:tenant-cache:invalidate` pub/sub); without Redis they clear the local process only, so other replicas stay stale until `cacheTtlMs`. A received broadcast clears locally without re-publishing. **`isTenantActive` (11.42.0)**: optional hook `(tenantId, { connection }) => boolean \| Promise<boolean>`, absent = every tenant active (no change). Asked AFTER the membership check on every path that establishes a tenant: 403 on header paths and for tenant API tokens, no enrichment on `S_EVERYONE`, dropped from the header-less tenant list, no tenant for a NEW WebSocket subscription, and since 11.42.3 an OPEN one is closed with 4403 on `invalidateTenant()` (or at its next event once `graphQl.subscriptionRevalidationMs` has passed — see the GraphQL Subscription Re-check row); an admin under `adminBypass` is exempt (someone must be able to switch it back on). Only `false` deactivates. Cached for `cacheTtlMs`; `invalidateTenant(tenantId)` clears and broadcasts (`{ scope: 'tenant' }`). A function, so `config.env.ts` only — not reachable through `NSC__*`. **`adminBypass: false` (11.42.0)** additionally withholds two things that went around it: `CoreUserService.update()` refuses changing another account's e-mail or password (`assertCredentialChangeAllowed()`, administrator targets included — promote-then-change — system work without a current user exempt), and the AI tool registry grants no ADMIN bypass |
|
|
324
325
|
| GraphQL Subscription Re-check | `graphQl.subscriptionRevalidationMs` | Numeric — `0` = STRICTEST, `false` = off, invalid → default (a fifth shape: Family C's "invalid never means unbounded", but `0` asks for MORE checking, not less) | **30000 since 11.42.3.** Before, a WebSocket was authorized once, at the handshake, and every event and operation on it ran on that snapshot until the client reconnected. Every connection a `CoreModule` `onConnect` accepts is now registered (`registerWsConnection()`) and re-checked — at once when an invalidation names it (`CoreTenantGuard.invalidateUser()` / `invalidateTenant()` / `invalidateAll()`, `CoreUserService.setRoles()` and role/verification `update()`s, the legacy password reset, and Better-Auth deleting a session through `wsSessionRevocationPlugin()`), and otherwise before the next event or operation once the last check is older than this value. The check re-runs the handshake's own authentication (session store, Better-Auth JWT signature/expiry, or for legacy tokens `CoreAuthService.verifyJwt()` — signature/expiry, verified on the WebSocket since 11.42.3 — plus the device binding) and the tenant resolution; any difference in user id, roles, verification state or tenant scope closes the socket with **4403**, which the graphql-ws client retries — the user continues with current rights or is refused at the handshake. `0` re-checks on every event and operation (one session lookup each); `false` switches only the TIME-based check off, invalidations still apply. Process-local registry: an invalidation reaches other replicas only where the tenant guard's Redis broadcast carries it; everything else is bounded by the interval. A project that replaces `onConnect` through `graphQl.driver.subscriptions` must call `registerWsConnection()` itself to keep this. Implementation: `common/helpers/graphql-ws-connection.helper.ts` |
|
|
@@ -342,8 +343,8 @@ This pattern is currently applied to:
|
|
|
342
343
|
| Outbound URL Guard (SSRF) | `security.allowPrivateOutboundTargets` | Explicit Boolean with DERIVED default | **Opt-in helper for URLs a USER entered** (webhooks, export targets, ERP addresses) — `outboundFetch()`, `assertOutboundUrlAllowed()`, `createOutboundDispatcher()` in `common/helpers/outbound-url.helper.ts` (11.42.0). Nothing is applied globally: configured targets (AI providers, SMTP, S3, Redis, Brevo) never go through it. Refuses loopback, RFC 1918, link-local incl. `169.254.169.254`, CGNAT, `0/8`, benchmarking, multicast/reserved, IPv6 unspecified/loopback/IPv4-compatible (`::/96`), NAT64, ULA, link-local and multicast, and IPv4-mapped IPv6 in every notation (canonicalised through the URL parser). Checked TWICE: on the URL before sending (readable `OutboundUrlBlockedError`) and on every connection through an undici `Agent` with a guarded connector — the only place that sees a redirect's next hop and a DNS answer at connect time (rebinding). **Unset → derived from the environment**: allowed in `local`, `e2e`, `ci`, refused everywhere else — deliberately including `test`, a DEPLOYED stage in starter-based projects; `true`/`false` (also the strings, from `NSC__SECURITY__ALLOW_PRIVATE_OUTBOUND_TARGETS`) decide explicitly. Per call, `allowedHosts` frees single internal hosts (`'erp.local:8443'` = that port, `'erp.local'` = any port) without opening the rest — re-checked per connection, so a redirect does not inherit it. `outboundFetch()` uses undici's OWN `fetch` (runtime dependency `undici` since 11.42.0), because Node 22's built-in fetch bundles a different undici major than the agent |
|
|
343
344
|
| AI Egress Allowlist (SSRF) | `ai.allowedBaseUrlHosts` | Explicit Value — **unset = permissive**, malformed = INTERPRETED or reported, never silently off | `undefined` → no restriction, so a local provider (Ollama on `localhost:11434`) works out of the box. Set → only those hosts are reachable, matched against `URL.host` (incl. port) and `URL.hostname`. **Accepts an array OR a comma-separated string, and the string form is not a convenience** — it is what the framework's own env mapping produces: `NSC__AI__ALLOWED_BASE_URL_HOSTS` becomes `{ ai: { allowedBaseUrlHosts: '<string>' } }` and lodash `merge` assigns that scalar straight over a configured array. Until 11.39.x a bare `!Array.isArray(...) -> return` read that as "not configured" and skipped the check entirely, so the canonical `NSC__` spelling silently disabled SSRF egress control with no log line at all. Entries are trimmed, lowercased and stripped of a fully-qualifying trailing dot, and the SAME normalisation is applied to the URL, so neither side wins by spelling one DNS name differently; a bare hostname entry matches any port, and an entry naming the scheme's default port (`example.com:443` on https) also matches the portless URL. A value that is neither array nor string carries no hostnames — the allowlist is then inactive and that is LOGGED as an error, because from the outside it looks exactly like "correctly unset". Enforced on **all three** outbound paths (`chat()`, the capability `probe()`, and `probeContextWindow()` — the last was unguarded before 11.39.x and is reachable from an ordinary user prompt via `detectAndPersistCapabilities()`, before the rate limit). Implementation: `openai-compatible.provider.ts` → `assertBaseUrlAllowed()` / `resolveAllowedBaseUrlHosts()` |
|
|
344
345
|
| AI Run Time Limit | `ai.maxRunMs` | **Numeric Sentinel — Family A** (`0` = no limit) | `0` (omitted = unbounded, the previous behaviour). Wall-clock ceiling for ONE prompt run, checked before each agent-loop iteration; once exceeded the run stops and answers with whatever it has. **Without it a run's only bound is `maxIterations` MULTIPLIED BY the connection's per-call timeout** — 8 iterations at the 120s default is a request that can legitimately hold a socket, its message buffer and a request context for 16 minutes, and compaction can add a call per iteration on top. Guarded as `maxRunMs > 0`, never `<= 0`: a value arriving as a non-numeric string through `NSC__AI__MAX_RUN_MS` (the reader coerces only `'true'`, `'false'` and numbers) yields `NaN`, and `NaN > 0` is `false`, so a misconfiguration degrades to "no limit" rather than to an instantly-expired deadline that would break every prompt. Set it to something a client would actually wait for. Implementation: `core-ai.service.ts` -> `runDeadlineExceeded()` |
|
|
345
|
-
| AI Assistant | `ai` | Presence Implies Enabled | Core: `maxIterations: 5`, `defaultMode: 'auto'` (or `'plan'`), `rateLimit` (presence implies enabled: `max: 20`, `windowSeconds: 60`), `systemPrompt`, `documentation` (injected into the system prompt), `encryptionSecret`. **DB-backed LLM connections** (`aiConnections`, admin CRUD) with AES-256-GCM-encrypted API keys (`AiCryptoService`, secret from `ai.encryptionSecret` / `NSC__AI__ENCRYPTION_SECRET` / `SECRETS_ENCRYPTION_KEY`; `apiKeyEncrypted` is a global `secretFields` entry, never returned — only `hasApiKey`); optional `defaultConnection` one-time seed. **Provider abstraction** (`ILlmProvider`, default `OpenAiCompatibleProvider` for any OpenAI-compatible endpoint via `fetch`; per-connection `supportsNativeTools`/`supportsJsonResponse` capabilities, emulated tool calling when native tools are unavailable). **Tool registry** (`AiToolRegistry`, tools self-register, role-filtered — under multi-tenancy (11.42.0+) a tool's roles resolve like `@Roles()` in `CoreTenantGuard`: global roles against `user.roles`, tenant roles against the request's membership role, ADMIN bypass only where `adminBypass` grants it, so chat and MCP never exceed the user's own rights; an MCP session reads the user per request, so a revoked role stops working without a reconnect; tools may be `mutating`/`destructive` and define `authorize()` for pre-flight data-level checks). **Plan mode** (`input.mode: 'plan'`): full plan → pre-flight authorize ALL steps → all-or-nothing execution with a translated (de/en) error when any step is not permitted. **Confirmation policy**: `confirmation.mutating: { default, enforced }` + client `input.requireConfirmation` (ignored when enforced); `destructive` always confirms. **Client metadata** (`input.metadata`: URL/nav/console logs, untrusted+capped). **Multi-turn conversations** (`aiConversations`, owner-scoped). **SSE streaming** (`POST /ai/stream`). **Audit** (`audit: false` → persist to `aiInteractions`, admin-readable). **Token budgets** (`budget: { period: 'day'|'month'|'none', user: { maxTokens?, maxPrompts? }, tenant: { maxTokens?, maxPrompts? } }`, requires audit): per-user AND per-tenant limits with config defaults; admins override per user/tenant at runtime (`aiBudgetLimits`, `CoreAiBudgetService`). Resolution: override → default → unlimited (missing/0 = unlimited). Enforced before the run (HTTP 429 + translated). Each response carries a compact `budget` summary (promptTokens, usedTokens, remainingTokens, resetAt); full breakdown via `aiUsage` query / `GET /ai/usage`. **Self-optimizing prompts**: the system prompt is assembled from keyed fragments (`CoreAiPromptBuilderService` ships built-in defaults; works with zero rows). Admin-editable overrides per slot (`aiSlots`, admin CRUD, `/ai/slots`) scoped by `key`/`locale`/`capability`/`tenantId`, with tenant override/reset semantics and placeholder tokens resolved at run time via the placeholder registry. **Governed learning loop** (`promptLearning: { enabled: true, autoApply: false }`): tool errors record `suggested` hints (`aiPromptHints`, admin CRUD, `/ai/prompt-hints`) that only reach the prompt once admin-approved (or auto-approved when `autoApply`); hints only ADD guidance, never relax permissions. **Context window** (`contextWindow`, default 8192; auto-detected per connection via `ILlmProvider.detectContextWindow()` — Ollama `/api/show` probe / known-model table / Claude alias — and persisted): per-user/session history is trimmed (oldest non-system turns dropped, last truncated) and tool-results capped to `maxToolResultChars` (default 12000) so a session never overflows the model. A connection's window can be seeded via `ai.defaultConnection.contextWindow` (validated: a non-positive/non-integer value is dropped with a warning). **Capability drift check** (`capabilityDriftCheck`, default `false`): opt-in boot self-check that probes each enabled connection with an EXPLICIT `supportsNativeTools`/`supportsJsonResponse` (built with those flags cleared so the endpoint is actually re-probed) and logs a warning on mismatch — the stored value is never changed. OFF by default because it makes outbound calls to the LLM endpoints on every boot; also skipped in the ci/e2e runners. **Deferred tool schemas** (`deferToolSchemas`, default `false`): the system-prompt tool catalog then lists only tool NAMES + descriptions instead of full JSON schemas, and the model fetches a schema on demand via the built-in `search_tools` meta-tool — with a large registry the schemas alone can dominate a small context window. `deferToolSummaryChars` (default `0` = untruncated) additionally caps each description in that DEFERRED catalog: whole sentences up to the cap (always at least the first), word-boundary cut when the first sentence already exceeds it, and a `…` marker appended ON TOP of the cap. The default of `0` keeps the saving opt-in, so enabling `deferToolSchemas` alone never changes what a description says; set roughly 200–400 alongside it to actually reclaim the context. Both apply to EMULATED providers only — a connection with `supportsNativeTools: true` receives every full description + schema via `buildToolSchemas()` regardless, so truncation and the banner are skipped there rather than asserting a cut the tool payload contradicts. The omitted tail is where preconditions and role restrictions usually live — the catalog banner tells the model to fetch the full text via `search_tools` first, but this is model GUIDANCE only: authorization is enforced server-side by the registry's role filter (`forUser()`), the execution-time re-check, and the `mutating`/`destructive` flags read by the confirmation gate — never by what the catalog shows. (`AiTool.authorize()` runs in PLAN MODE only; in auto mode and over MCP, data-level checks must live inside `execute()`.) **MCP server** (`mcp: false` → `/ai/mcp` Streamable HTTP, Bearer auth, lazy `@modelcontextprotocol/sdk`; `mcp: { oauth: true, oauthSecret }` adds OAuth 2.1 — HMAC tokens + PKCE S256 + dynamic registration
|
|
346
|
-
| Swagger API-Token View | `setupSwagger(app, { apiTokenView })` (helper option, not server config) | Boolean Shorthand / Presence Implies Enabled | **Off without the option** (11.42.7+). `true` / `{}` serves a second document with only the routes that release a scope via `@ApiTokenScopes()`: UI at `swagger-api-tokens`, JSON at `/api-docs-api-tokens-json`, title `<title> — API tokens`, bearer as the only scheme, unused schemas pruned; the tenant header stays while `apiTokens.userTokens` is on (a personal token chooses the company like a session) and is dropped with company tokens only. Keep its path OUTSIDE the full UI's path: Swagger UI serves its assets below its own path. The full document's enrichment (global bearer + session-cookie requirement, `security: []` on public routes, `x-api-token-scopes` + description line, `TenantHeader` parameter) is not optional — it is what `setupSwagger()` is for, and it reads the same metadata as the guards. Implementation: `common/helpers/swagger.helper.ts` **Better-Auth routes (11.42.9):** the full document also lists every route Better-Auth serves itself under the base path — whatever the configured plugins add — from Better-Auth's own generator, under the project's Better-Auth controller (its tags and `operationTags`, ids `<Controller>_betterAuth_<id>`, schemas prefixed `BetterAuth`, OpenAPI 3.1 constructs converted to 3.0). Their security is what Better-Auth enforces, not the generator's blanket bearer-only: a route behind one of Better-Auth's session middlewares (or one of the three social-account routes in `HANDLER_SESSION_PATHS`, which check in the handler) gets the global requirement, every other route `security: []` — except routes of plugins nest-server does not register itself (`BUILT_IN_BETTER_AUTH_PLUGIN_IDS`), which always keep the global requirement — and if the instance's middlewares cannot be identified (two copies of better-auth), all of them keep the global requirement rather than risk documenting a session route as public. They carry the "not allowed" API-token line, and never reach the API-token view. The `openAPI()` plugin this needs is registered internally; its routes answer 404 (see `.claude/rules/better-auth.md` §9). **Only usable operations (11.42.9):** an operation that answers an error to every caller under the running configuration is left out — `@ApiMethods()` on `@All()` handlers (TUS), legacy `/auth/*` while `auth.legacyEndpoints` keeps REST closed, Better-Auth routes refused by the configuration or the live instance, Hub endpoints of switched-off panels. `/ai/mcp` and the MCP OAuth endpoints
|
|
346
|
+
| AI Assistant | `ai` | Presence Implies Enabled | Core: `maxIterations: 5`, `defaultMode: 'auto'` (or `'plan'`), `rateLimit` (presence implies enabled: `max: 20`, `windowSeconds: 60`), `systemPrompt`, `documentation` (injected into the system prompt), `encryptionSecret`. **DB-backed LLM connections** (`aiConnections`, admin CRUD) with AES-256-GCM-encrypted API keys (`AiCryptoService`, secret from `ai.encryptionSecret` / `NSC__AI__ENCRYPTION_SECRET` / `SECRETS_ENCRYPTION_KEY`; `apiKeyEncrypted` is a global `secretFields` entry, never returned — only `hasApiKey`); optional `defaultConnection` one-time seed. **Provider abstraction** (`ILlmProvider`, default `OpenAiCompatibleProvider` for any OpenAI-compatible endpoint via `fetch`; per-connection `supportsNativeTools`/`supportsJsonResponse` capabilities, emulated tool calling when native tools are unavailable). **Tool registry** (`AiToolRegistry`, tools self-register, role-filtered — under multi-tenancy (11.42.0+) a tool's roles resolve like `@Roles()` in `CoreTenantGuard`: global roles against `user.roles`, tenant roles against the request's membership role, ADMIN bypass only where `adminBypass` grants it, so chat and MCP never exceed the user's own rights; an MCP session reads the user per request, so a revoked role stops working without a reconnect; tools may be `mutating`/`destructive` and define `authorize()` for pre-flight data-level checks). **Plan mode** (`input.mode: 'plan'`): full plan → pre-flight authorize ALL steps → all-or-nothing execution with a translated (de/en) error when any step is not permitted. **Confirmation policy**: `confirmation.mutating: { default, enforced }` + client `input.requireConfirmation` (ignored when enforced); `destructive` always confirms. **Client metadata** (`input.metadata`: URL/nav/console logs, untrusted+capped). **Multi-turn conversations** (`aiConversations`, owner-scoped). **SSE streaming** (`POST /ai/stream`). **Audit** (`audit: false` → persist to `aiInteractions`, admin-readable). **Token budgets** (`budget: { period: 'day'|'month'|'none', user: { maxTokens?, maxPrompts? }, tenant: { maxTokens?, maxPrompts? } }`, requires audit): per-user AND per-tenant limits with config defaults; admins override per user/tenant at runtime (`aiBudgetLimits`, `CoreAiBudgetService`). Resolution: override → default → unlimited (missing/0 = unlimited). Enforced before the run (HTTP 429 + translated). Each response carries a compact `budget` summary (promptTokens, usedTokens, remainingTokens, resetAt); full breakdown via `aiUsage` query / `GET /ai/usage`. **Self-optimizing prompts**: the system prompt is assembled from keyed fragments (`CoreAiPromptBuilderService` ships built-in defaults; works with zero rows). Admin-editable overrides per slot (`aiSlots`, admin CRUD, `/ai/slots`) scoped by `key`/`locale`/`capability`/`tenantId`, with tenant override/reset semantics and placeholder tokens resolved at run time via the placeholder registry. **Governed learning loop** (`promptLearning: { enabled: true, autoApply: false }`): tool errors record `suggested` hints (`aiPromptHints`, admin CRUD, `/ai/prompt-hints`) that only reach the prompt once admin-approved (or auto-approved when `autoApply`); hints only ADD guidance, never relax permissions. **Context window** (`contextWindow`, default 8192; auto-detected per connection via `ILlmProvider.detectContextWindow()` — Ollama `/api/show` probe / known-model table / Claude alias — and persisted): per-user/session history is trimmed (oldest non-system turns dropped, last truncated) and tool-results capped to `maxToolResultChars` (default 12000) so a session never overflows the model. A connection's window can be seeded via `ai.defaultConnection.contextWindow` (validated: a non-positive/non-integer value is dropped with a warning). **Capability drift check** (`capabilityDriftCheck`, default `false`): opt-in boot self-check that probes each enabled connection with an EXPLICIT `supportsNativeTools`/`supportsJsonResponse` (built with those flags cleared so the endpoint is actually re-probed) and logs a warning on mismatch — the stored value is never changed. OFF by default because it makes outbound calls to the LLM endpoints on every boot; also skipped in the ci/e2e runners. **Deferred tool schemas** (`deferToolSchemas`, default `false`): the system-prompt tool catalog then lists only tool NAMES + descriptions instead of full JSON schemas, and the model fetches a schema on demand via the built-in `search_tools` meta-tool — with a large registry the schemas alone can dominate a small context window. `deferToolSummaryChars` (default `0` = untruncated) additionally caps each description in that DEFERRED catalog: whole sentences up to the cap (always at least the first), word-boundary cut when the first sentence already exceeds it, and a `…` marker appended ON TOP of the cap. The default of `0` keeps the saving opt-in, so enabling `deferToolSchemas` alone never changes what a description says; set roughly 200–400 alongside it to actually reclaim the context. Both apply to EMULATED providers only — a connection with `supportsNativeTools: true` receives every full description + schema via `buildToolSchemas()` regardless, so truncation and the banner are skipped there rather than asserting a cut the tool payload contradicts. The omitted tail is where preconditions and role restrictions usually live — the catalog banner tells the model to fetch the full text via `search_tools` first, but this is model GUIDANCE only: authorization is enforced server-side by the registry's role filter (`forUser()`), the execution-time re-check, and the `mutating`/`destructive` flags read by the confirmation gate — never by what the catalog shows. (`AiTool.authorize()` runs in PLAN MODE only; in auto mode and over MCP, data-level checks must live inside `execute()`.) **MCP server** (`mcp: false` → `/ai/mcp` Streamable HTTP, Bearer auth, lazy `@modelcontextprotocol/sdk`; `mcp: { oauth: true, oauthSecret }` adds OAuth 2.1 — HMAC tokens + PKCE S256 + dynamic registration. **Since 11.43.0 `CoreAiMcpOAuthInitializer` mounts the endpoints during `app.init()` (`onModuleInit`)** — behind the Nest middleware and routes, ahead of Nest's not-found handler; `mountAiMcpOAuth(app)` after `app.init()`, the documented way before, sat behind that handler and answered 404 everywhere, and is a no-op now. The protected resource is described at the path Nest registered `/ai/mcp` at (global prefix + exclusions, URI versioning, `RouterModule`, via Nest's own `RoutePathFactory`), and the MCP 401 names that metadata URL — plain `Bearer` without OAuth. Issuer: `mcp.oauthBaseUrl` (its origin is added to Better-Auth's `trustedOrigins`, explicit list included, via `withMcpOAuthIssuerOrigin()`), else `baseUrl`; a deployed environment with neither fails the boot. Signing secret required everywhere except `local` / `ci` / `e2e` (`allowsDevelopmentSecret()` → `isLocalEnv`), deployed `develop` / `test` stages included. Under a global prefix the consent page resolves the session itself (the Better-Auth middleware runs below the prefix only) and signs in at the prefixed IAM path. Express only (boot error otherwise); an application context without HTTP server mounts nothing. Consent: a built-in page (`authorizeConsent()` default) — sign-in through Better-Auth on the API origin (password + TOTP/backup code; passkey/social via `mcp.oauthLoginUrl`, default `<appUrl>/auth/login`), then Allow/Deny as a form POST with a CSRF token signed and bound to user, client, redirect URI and PKCE challenge (10 min); Allow is `disabled` until a nonce'd guard arms it and counts a click only after intent (visible + focused + pointer move / key / touch ≥ 600 ms later — against double-clickjacking); the account is shown first with "use another account"; own CSP with nonces + `frame-ancestors 'none'`, everything escaped; boot warning without Better-Auth; restyle via `renderConsentPage()`, replace via the `mcpOAuthService` override. A code saved while answering a GET to `/authorize` is refused for every consent implementation. Refusals are SDK `OAuthError` classes (`invalid_grant` etc.), never a plain `Error`, which the SDK answers as 500). Overrides via `CoreModule.forRoot(env, { ai: { budgetService, connectionResolver, connectionService, controller, conversationService, interactionService, mcpClientService, mcpOAuthService, modeService, placeholderRegistry, preferenceService, promptBuilder, promptHintService, promptService, resolver, service, slotService, toolGrantService, toolPolicyService } })` |
|
|
347
|
+
| Swagger API-Token View | `setupSwagger(app, { apiTokenView })` (helper option, not server config) | Boolean Shorthand / Presence Implies Enabled | **Off without the option** (11.42.7+). `true` / `{}` serves a second document with only the routes that release a scope via `@ApiTokenScopes()`: UI at `swagger-api-tokens`, JSON at `/api-docs-api-tokens-json`, title `<title> — API tokens`, bearer as the only scheme, unused schemas pruned; the tenant header stays while `apiTokens.userTokens` is on (a personal token chooses the company like a session) and is dropped with company tokens only. Keep its path OUTSIDE the full UI's path: Swagger UI serves its assets below its own path. The full document's enrichment (global bearer + session-cookie requirement, `security: []` on public routes, `x-api-token-scopes` + description line, `TenantHeader` parameter) is not optional — it is what `setupSwagger()` is for, and it reads the same metadata as the guards. Implementation: `common/helpers/swagger.helper.ts` **Better-Auth routes (11.42.9):** the full document also lists every route Better-Auth serves itself under the base path — whatever the configured plugins add — from Better-Auth's own generator, under the project's Better-Auth controller (its tags and `operationTags`, ids `<Controller>_betterAuth_<id>`, schemas prefixed `BetterAuth`, OpenAPI 3.1 constructs converted to 3.0). Their security is what Better-Auth enforces, not the generator's blanket bearer-only: a route behind one of Better-Auth's session middlewares (or one of the three social-account routes in `HANDLER_SESSION_PATHS`, which check in the handler) gets the global requirement, every other route `security: []` — except routes of plugins nest-server does not register itself (`BUILT_IN_BETTER_AUTH_PLUGIN_IDS`), which always keep the global requirement — and if the instance's middlewares cannot be identified (two copies of better-auth), all of them keep the global requirement rather than risk documenting a session route as public. They carry the "not allowed" API-token line, and never reach the API-token view. The `openAPI()` plugin this needs is registered internally; its routes answer 404 (see `.claude/rules/better-auth.md` §9). **Only usable operations (11.42.9):** an operation that answers an error to every caller under the running configuration is left out — `@ApiMethods()` on `@All()` handlers (TUS), legacy `/auth/*` while `auth.legacyEndpoints` keeps REST closed, Better-Auth routes refused by the configuration or the live instance, Hub endpoints of switched-off panels. `/ai/mcp` and the MCP OAuth endpoints that are actually mounted are documented. An explicit `@ApiBearerAuth()` keeps a guard-public route from being documented as public. Rules: `isSwitchedOffOperation()` in `swagger.helper.ts` |
|
|
347
348
|
|
|
348
349
|
## Module Override Pattern (via `ICoreModuleOverrides`)
|
|
349
350
|
|
|
@@ -375,7 +376,7 @@ CoreModule.forRoot(CoreAuthService, AuthModule.forRoot(envConfig.jwt), envConfig
|
|
|
375
376
|
|
|
376
377
|
| Module | Fields | Description |
|
|
377
378
|
|--------|--------|-------------|
|
|
378
|
-
| `ai` | `budgetService`, `connectionResolver`, `connectionService`, `controller`, `conversationService`, `interactionService`, `mcpClientService`, `modeService`, `placeholderRegistry`, `preferenceService`, `promptBuilder`, `promptHintService`, `promptService`, `resolver`, `service`, `slotService`, `toolGrantService`, `toolPolicyService` | Custom AI orchestrator, connection-resolution chain, connection/preference/budget/conversation/interaction/MCP-client services, slot + user-prompt + learning-hint stores, mode service, placeholder registry, tool-grant + tool-policy services, endpoints |
|
|
379
|
+
| `ai` | `budgetService`, `connectionResolver`, `connectionService`, `controller`, `conversationService`, `interactionService`, `mcpClientService`, `mcpOAuthService`, `modeService`, `placeholderRegistry`, `preferenceService`, `promptBuilder`, `promptHintService`, `promptService`, `resolver`, `service`, `slotService`, `toolGrantService`, `toolPolicyService` | Custom AI orchestrator, connection-resolution chain, connection/preference/budget/conversation/interaction/MCP-client services, MCP OAuth service (the consent step), slot + user-prompt + learning-hint stores, mode service, placeholder registry, tool-grant + tool-policy services, endpoints |
|
|
379
380
|
| `errorCode` | `controller`, `service` | Custom error code endpoint and/or service |
|
|
380
381
|
| `betterAuth` | `controller`, `resolver` | Custom IAM REST controller and/or GraphQL resolver |
|
|
381
382
|
| `hub` | `controller`, `actionsController`, `service`, `htmlService`, `actionsService` | Custom Hub page/actions controllers and aggregator/HTML/actions services (each must extend its `Core*` base) |
|
package/.claude/rules/testing.md
CHANGED
|
@@ -268,7 +268,7 @@ pnpm run check:mutations -- --id=<id> # one mutation
|
|
|
268
268
|
pnpm run check:mutations -- --list # the registry, without running anything
|
|
269
269
|
pnpm run check:mutations -- --allow-dirty # when the fix and its evidence share a working tree
|
|
270
270
|
pnpm run check:mutations -- --jobs=4 # N mutations at a time (default: 2, or 4 on >=12 cores)
|
|
271
|
-
pnpm run check:mutations -- --no-infra # only the
|
|
271
|
+
pnpm run check:mutations -- --no-infra # only the 164 that need no MongoDB
|
|
272
272
|
pnpm run check:mutations -- --since=<ref> # only mutations touching files changed since <ref>
|
|
273
273
|
```
|
|
274
274
|
|
|
@@ -287,7 +287,7 @@ when the registry, a vitest config or a setup file changed, since those can move
|
|
|
287
287
|
commit, and in CI alongside the publish without blocking it. The price of caching is a cache that
|
|
288
288
|
has to model each spec's full dependency closure correctly, and getting that wrong produces a stale
|
|
289
289
|
PASS for a test that has since gone vacuous — exactly what the gate is there to prevent. Bad trade
|
|
290
|
-
at
|
|
290
|
+
at 243 mutations.
|
|
291
291
|
|
|
292
292
|
**Decided 2026-10-03 (repo owner): keep the full run.** Measured at 128 mutations, the CI job for
|
|
293
293
|
11.41.6 took **14 minutes**. The trigger to revisit is that wall time, not the mutation count —
|
|
@@ -332,9 +332,9 @@ exactly the environment where the answer matters.
|
|
|
332
332
|
|
|
333
333
|
### The cost is vitest's cold start, not the tests
|
|
334
334
|
|
|
335
|
-
Worth knowing before optimising the wrong thing: the specs behind all
|
|
335
|
+
Worth knowing before optimising the wrong thing: the specs behind all 79 e2e mutations add up to
|
|
336
336
|
**~40 seconds**. The step takes ~740s. The remaining ~700s is paying vitest's startup — process
|
|
337
|
-
spawn, transform, module graph, mongod connect, DB create and drop — once per mutation,
|
|
337
|
+
spawn, transform, module graph, mongod connect, DB create and drop — once per mutation, 243 times.
|
|
338
338
|
That work is largely single-threaded I/O and barely scales with cores: the full registry measures
|
|
339
339
|
**744s on a 12-core laptop and 777s on a 4-vCPU CI runner**.
|
|
340
340
|
|
package/FRAMEWORK-API.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @lenne.tech/nest-server — Framework API Reference
|
|
2
2
|
|
|
3
|
-
> Auto-generated from source code as of 2026-10-
|
|
3
|
+
> Auto-generated from source code as of 2026-10-18 (v11.43.0)
|
|
4
4
|
> File: `FRAMEWORK-API.md` — compact, machine-readable API surface for Claude Code
|
|
5
5
|
|
|
6
6
|
## CoreModule.forRoot()
|
|
@@ -131,7 +131,7 @@ When `passkey` is enabled, `trustedOrigins` is required (compile-time enforcemen
|
|
|
131
131
|
- `maxRunMs?`: `number | undefined` (default: `0`) — Wall-clock ceiling for ONE prompt run, in milliseconds. Checked before each
|
|
132
132
|
- `maxToolResultChars?`: `number | undefined` (default: `12000`) — Maximum characters of a tool-results payload fed back to the model.
|
|
133
133
|
- `promptLearning?`: `{ autoApply?: boolean; enabled?: boolean; minOccurrences?: number; } | undefined` — Governed self-improvement loop for the system prompt. The orchestrator records
|
|
134
|
-
- `mcp?`: `boolean | { enabled?: boolean; oauth?: boolean;
|
|
134
|
+
- `mcp?`: `boolean | { enabled?: boolean; oauth?: boolean; oauthBaseUrl?: string; oauthL...` (default: `false`) — Expose the tool registry as an MCP server at `/ai/mcp` (Streamable HTTP) for
|
|
135
135
|
- `rateLimit?`: `IAiRateLimit | undefined` — Rate limiting for prompts.
|
|
136
136
|
- `systemPrompt?`: `string | undefined` — Base system prompt prepended to every conversation.
|
|
137
137
|
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ArgumentsHost, ExceptionFilter, HttpException, Logger } from '@nestjs/common';
|
|
2
|
+
import { Error as MongooseError } from 'mongoose';
|
|
3
|
+
import type { Request, Response } from 'express';
|
|
4
|
+
export type CastErrorOrigin = 'identifier' | 'input' | 'server';
|
|
5
|
+
export declare function castErrorCandidates(value: unknown): string[];
|
|
6
|
+
export declare function containsCastValue(source: unknown, candidates: readonly string[]): boolean;
|
|
7
|
+
export declare function classifyHttpCastError(value: unknown, request: Partial<Pick<Request, 'body' | 'params' | 'query'>> | undefined): CastErrorOrigin;
|
|
8
|
+
export declare function classifyGraphQlCastError(value: unknown, args: unknown): CastErrorOrigin;
|
|
9
|
+
export declare class MongooseCastErrorFilter implements ExceptionFilter {
|
|
10
|
+
protected static readonly logger: Logger;
|
|
11
|
+
protected static readonly unknownErrorLogger: Logger;
|
|
12
|
+
catch(exception: MongooseError.CastError, host: ArgumentsHost): HttpException | undefined;
|
|
13
|
+
protected catchGraphQl(exception: MongooseError.CastError, host: ArgumentsHost): HttpException | undefined;
|
|
14
|
+
protected catchHttp(exception: MongooseError.CastError, host: ArgumentsHost): void;
|
|
15
|
+
protected toHttpException(exception: MongooseError.CastError, origin: 'identifier' | 'input'): HttpException;
|
|
16
|
+
protected replyAsUnknownError(exception: Error, response: Response): void;
|
|
17
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var MongooseCastErrorFilter_1;
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.MongooseCastErrorFilter = void 0;
|
|
11
|
+
exports.castErrorCandidates = castErrorCandidates;
|
|
12
|
+
exports.containsCastValue = containsCastValue;
|
|
13
|
+
exports.classifyHttpCastError = classifyHttpCastError;
|
|
14
|
+
exports.classifyGraphQlCastError = classifyGraphQlCastError;
|
|
15
|
+
const common_1 = require("@nestjs/common");
|
|
16
|
+
const mongoose_1 = require("mongoose");
|
|
17
|
+
const error_codes_1 = require("../../modules/error-code/error-codes");
|
|
18
|
+
const MAX_WALK_DEPTH = 32;
|
|
19
|
+
const MAX_WALK_NODES = 10_000;
|
|
20
|
+
function isPrimitive(value) {
|
|
21
|
+
return (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint');
|
|
22
|
+
}
|
|
23
|
+
function castErrorCandidates(value) {
|
|
24
|
+
if (isPrimitive(value)) {
|
|
25
|
+
return [String(value)];
|
|
26
|
+
}
|
|
27
|
+
if (Array.isArray(value)) {
|
|
28
|
+
return value.filter(isPrimitive).map((item) => String(item));
|
|
29
|
+
}
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
function boundedChildren(value, room) {
|
|
33
|
+
if (Array.isArray(value)) {
|
|
34
|
+
return value.length > room ? undefined : value;
|
|
35
|
+
}
|
|
36
|
+
const children = [];
|
|
37
|
+
for (const key in value) {
|
|
38
|
+
if (!Object.prototype.hasOwnProperty.call(value, key)) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (children.length >= room) {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
children.push(value[key]);
|
|
45
|
+
}
|
|
46
|
+
return children;
|
|
47
|
+
}
|
|
48
|
+
function containsCastValue(source, candidates) {
|
|
49
|
+
if (!candidates.length) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
const wanted = new Set(candidates);
|
|
53
|
+
const seen = new Set();
|
|
54
|
+
const stack = [{ depth: 0, value: source }];
|
|
55
|
+
let nodes = 0;
|
|
56
|
+
for (let item = stack.pop(); item; item = stack.pop()) {
|
|
57
|
+
const { depth, value } = item;
|
|
58
|
+
if (++nodes > MAX_WALK_NODES) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
if (isPrimitive(value)) {
|
|
62
|
+
if (wanted.has(String(value))) {
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (!value || typeof value !== 'object' || depth >= MAX_WALK_DEPTH || ArrayBuffer.isView(value)) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (seen.has(value)) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
seen.add(value);
|
|
74
|
+
const children = boundedChildren(value, MAX_WALK_NODES - nodes - stack.length);
|
|
75
|
+
for (const child of children ?? []) {
|
|
76
|
+
stack.push({ depth: depth + 1, value: child });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
function isDirectValue(values, candidates) {
|
|
82
|
+
const wanted = new Set(candidates);
|
|
83
|
+
const matches = (value) => isPrimitive(value) && wanted.has(String(value));
|
|
84
|
+
return values.some((value) => Array.isArray(value) ? value.length <= MAX_WALK_NODES && value.some(matches) : matches(value));
|
|
85
|
+
}
|
|
86
|
+
function classifyHttpCastError(value, request) {
|
|
87
|
+
const candidates = castErrorCandidates(value);
|
|
88
|
+
if (!candidates.length || !request) {
|
|
89
|
+
return 'server';
|
|
90
|
+
}
|
|
91
|
+
if (isDirectValue(Object.values(request.params ?? {}), candidates)) {
|
|
92
|
+
return 'identifier';
|
|
93
|
+
}
|
|
94
|
+
if (containsCastValue(request.query, candidates) || containsCastValue(request.body, candidates)) {
|
|
95
|
+
return 'input';
|
|
96
|
+
}
|
|
97
|
+
return 'server';
|
|
98
|
+
}
|
|
99
|
+
function classifyGraphQlCastError(value, args) {
|
|
100
|
+
const candidates = castErrorCandidates(value);
|
|
101
|
+
if (!candidates.length || !args || typeof args !== 'object') {
|
|
102
|
+
return 'server';
|
|
103
|
+
}
|
|
104
|
+
if (isDirectValue(Object.values(args), candidates)) {
|
|
105
|
+
return 'identifier';
|
|
106
|
+
}
|
|
107
|
+
if (containsCastValue(args, candidates)) {
|
|
108
|
+
return 'input';
|
|
109
|
+
}
|
|
110
|
+
return 'server';
|
|
111
|
+
}
|
|
112
|
+
let MongooseCastErrorFilter = class MongooseCastErrorFilter {
|
|
113
|
+
static { MongooseCastErrorFilter_1 = this; }
|
|
114
|
+
static logger = new common_1.Logger('MongooseCastErrorFilter');
|
|
115
|
+
static unknownErrorLogger = new common_1.Logger('ExceptionsHandler');
|
|
116
|
+
catch(exception, host) {
|
|
117
|
+
const type = host.getType();
|
|
118
|
+
if (type === 'graphql') {
|
|
119
|
+
return this.catchGraphQl(exception, host);
|
|
120
|
+
}
|
|
121
|
+
if (type === 'http') {
|
|
122
|
+
this.catchHttp(exception, host);
|
|
123
|
+
}
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
catchGraphQl(exception, host) {
|
|
127
|
+
const origin = classifyGraphQlCastError(exception.value, host.getArgByIndex(1));
|
|
128
|
+
if (origin === 'server') {
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
return this.toHttpException(exception, origin);
|
|
132
|
+
}
|
|
133
|
+
catchHttp(exception, host) {
|
|
134
|
+
const ctx = host.switchToHttp();
|
|
135
|
+
const response = ctx.getResponse();
|
|
136
|
+
const origin = classifyHttpCastError(exception.value, ctx.getRequest());
|
|
137
|
+
if (origin === 'server') {
|
|
138
|
+
this.replyAsUnknownError(exception, response);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const httpException = this.toHttpException(exception, origin);
|
|
142
|
+
if (response.headersSent) {
|
|
143
|
+
response.end();
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
response.status(httpException.getStatus()).json({ ...httpException });
|
|
147
|
+
}
|
|
148
|
+
toHttpException(exception, origin) {
|
|
149
|
+
MongooseCastErrorFilter_1.logger.debug(`Cast failed on a value from the request (${origin === 'identifier' ? 404 : 400}): ${exception.message}`);
|
|
150
|
+
if (origin === 'identifier') {
|
|
151
|
+
return new common_1.NotFoundException(error_codes_1.ErrorCode.RESOURCE_NOT_FOUND);
|
|
152
|
+
}
|
|
153
|
+
return new common_1.BadRequestException(exception.path ? `${error_codes_1.ErrorCode.INVALID_FIELD_FORMAT} [${exception.path}]` : error_codes_1.ErrorCode.INVALID_FIELD_FORMAT);
|
|
154
|
+
}
|
|
155
|
+
replyAsUnknownError(exception, response) {
|
|
156
|
+
if (response.headersSent) {
|
|
157
|
+
response.end();
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
response
|
|
161
|
+
.status(common_1.HttpStatus.INTERNAL_SERVER_ERROR)
|
|
162
|
+
.json({ statusCode: common_1.HttpStatus.INTERNAL_SERVER_ERROR, message: 'Internal server error' });
|
|
163
|
+
}
|
|
164
|
+
MongooseCastErrorFilter_1.unknownErrorLogger.error(exception);
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
exports.MongooseCastErrorFilter = MongooseCastErrorFilter;
|
|
168
|
+
exports.MongooseCastErrorFilter = MongooseCastErrorFilter = MongooseCastErrorFilter_1 = __decorate([
|
|
169
|
+
(0, common_1.Catch)(mongoose_1.Error.CastError)
|
|
170
|
+
], MongooseCastErrorFilter);
|
|
171
|
+
//# sourceMappingURL=mongoose-cast-error.filter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mongoose-cast-error.filter.js","sourceRoot":"","sources":["../../../../src/core/common/filters/mongoose-cast-error.filter.ts"],"names":[],"mappings":";;;;;;;;;;AAqDA,kDAQC;AAsCD,8CAmCC;AAsBD,sDAeC;AAQD,4DAYC;AA/LD,2CASwB;AACxB,uCAAkD;AAElD,sEAAiE;AAsBjE,MAAM,cAAc,GAAG,EAAE,CAAC;AAC1B,MAAM,cAAc,GAAG,MAAM,CAAC;AAI9B,SAAS,WAAW,CAAC,KAAc;IACjC,OAAO,CACL,OAAO,KAAK,KAAK,QAAQ,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,OAAO,KAAK,KAAK,SAAS,IAAI,OAAO,KAAK,KAAK,QAAQ,CAClH,CAAC;AACJ,CAAC;AAUD,SAAgB,mBAAmB,CAAC,KAAc;IAChD,IAAI,WAAW,CAAC,KAAK,CAAC,EAAE,CAAC;QACvB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IACzB,CAAC;IACD,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,KAAK,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;IAC/D,CAAC;IACD,OAAO,EAAE,CAAC;AACZ,CAAC;AAQD,SAAS,eAAe,CAAC,KAAa,EAAE,IAAY;IAClD,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC;IACjD,CAAC;IACD,MAAM,QAAQ,GAAc,EAAE,CAAC;IAG/B,KAAK,MAAM,GAAG,IAAI,KAAK,EAAE,CAAC;QACxB,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,KAAK,EAAE,GAAG,CAAC,EAAE,CAAC;YACtD,SAAS;QACX,CAAC;QACD,IAAI,QAAQ,CAAC,MAAM,IAAI,IAAI,EAAE,CAAC;YAC5B,OAAO,SAAS,CAAC;QACnB,CAAC;QACD,QAAQ,CAAC,IAAI,CAAE,KAAiC,CAAC,GAAG,CAAC,CAAC,CAAC;IACzD,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAaD,SAAgB,iBAAiB,CAAC,MAAe,EAAE,UAA6B;IAC9E,IAAI,CAAC,UAAU,CAAC,MAAM,EAAE,CAAC;QACvB,OAAO,KAAK,CAAC;IACf,CAAC;IACD,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,UAAU,CAAC,CAAC;IACnC,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,MAAM,KAAK,GAAwC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC;IACjF,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,KAAK,IAAI,IAAI,GAAG,KAAK,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE,IAAI,GAAG,KAAK,CAAC,GAAG,EAAE,EAAE,CAAC;QACtD,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,IAAI,CAAC;QAG9B,IAAI,EAAE,KAAK,GAAG,cAAc,EAAE,CAAC;YAC7B,OAAO,KAAK,CAAC;QACf,CAAC;QACD,IAAI,WAAW,CAAC,KAAK,CAAC,EAAE,CAAC;YACvB,IAAI,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;gBAC9B,OAAO,IAAI,CAAC;YACd,CAAC;YACD,SAAS;QACX,CAAC;QACD,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,IAAI,cAAc,IAAI,WAAW,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;YAChG,SAAS;QACX,CAAC;QACD,IAAI,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;YACpB,SAAS;QACX,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QAEhB,MAAM,QAAQ,GAAG,eAAe,CAAC,KAAK,EAAE,cAAc,GAAG,KAAK,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC;QAC/E,KAAK,MAAM,KAAK,IAAI,QAAQ,IAAI,EAAE,EAAE,CAAC;YACnC,KAAK,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,KAAK,GAAG,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;QACjD,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAMD,SAAS,aAAa,CAAC,MAA0B,EAAE,UAA6B;IAC9E,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,UAAU,CAAC,CAAC;IACnC,MAAM,OAAO,GAAG,CAAC,KAAc,EAAE,EAAE,CAAC,WAAW,CAAC,KAAK,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IAEpF,OAAO,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAC3B,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,IAAI,cAAc,IAAI,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAC9F,CAAC;AACJ,CAAC;AASD,SAAgB,qBAAqB,CACnC,KAAc,EACd,OAAwE;IAExE,MAAM,UAAU,GAAG,mBAAmB,CAAC,KAAK,CAAC,CAAC;IAC9C,IAAI,CAAC,UAAU,CAAC,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;QACnC,OAAO,QAAQ,CAAC;IAClB,CAAC;IACD,IAAI,aAAa,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,IAAI,EAAE,CAAC,EAAE,UAAU,CAAC,EAAE,CAAC;QACnE,OAAO,YAAY,CAAC;IACtB,CAAC;IACD,IAAI,iBAAiB,CAAC,OAAO,CAAC,KAAK,EAAE,UAAU,CAAC,IAAI,iBAAiB,CAAC,OAAO,CAAC,IAAI,EAAE,UAAU,CAAC,EAAE,CAAC;QAChG,OAAO,OAAO,CAAC;IACjB,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAQD,SAAgB,wBAAwB,CAAC,KAAc,EAAE,IAAa;IACpE,MAAM,UAAU,GAAG,mBAAmB,CAAC,KAAK,CAAC,CAAC;IAC9C,IAAI,CAAC,UAAU,CAAC,MAAM,IAAI,CAAC,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC5D,OAAO,QAAQ,CAAC;IAClB,CAAC;IACD,IAAI,aAAa,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,UAAU,CAAC,EAAE,CAAC;QACnD,OAAO,YAAY,CAAC;IACtB,CAAC;IACD,IAAI,iBAAiB,CAAC,IAAI,EAAE,UAAU,CAAC,EAAE,CAAC;QACxC,OAAO,OAAO,CAAC;IACjB,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAiCM,IAAM,uBAAuB,GAA7B,MAAM,uBAAuB;;IACxB,MAAM,CAAU,MAAM,GAAG,IAAI,eAAM,CAAC,yBAAyB,CAAC,CAAC;IAM/D,MAAM,CAAU,kBAAkB,GAAG,IAAI,eAAM,CAAC,mBAAmB,CAAC,CAAC;IAE/E,KAAK,CAAC,SAAkC,EAAE,IAAmB;QAC3D,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,EAAkB,CAAC;QAC5C,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;YACvB,OAAO,IAAI,CAAC,YAAY,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;QAC5C,CAAC;QACD,IAAI,IAAI,KAAK,MAAM,EAAE,CAAC;YACpB,IAAI,CAAC,SAAS,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;QAClC,CAAC;QAED,OAAO,SAAS,CAAC;IACnB,CAAC;IAMS,YAAY,CAAC,SAAkC,EAAE,IAAmB;QAC5E,MAAM,MAAM,GAAG,wBAAwB,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC;QAChF,IAAI,MAAM,KAAK,QAAQ,EAAE,CAAC;YACxB,OAAO,SAAS,CAAC;QACnB,CAAC;QACD,OAAO,IAAI,CAAC,eAAe,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;IACjD,CAAC;IAMS,SAAS,CAAC,SAAkC,EAAE,IAAmB;QACzE,MAAM,GAAG,GAAG,IAAI,CAAC,YAAY,EAAE,CAAC;QAChC,MAAM,QAAQ,GAAG,GAAG,CAAC,WAAW,EAAY,CAAC;QAC7C,MAAM,MAAM,GAAG,qBAAqB,CAAC,SAAS,CAAC,KAAK,EAAE,GAAG,CAAC,UAAU,EAAW,CAAC,CAAC;QACjF,IAAI,MAAM,KAAK,QAAQ,EAAE,CAAC;YACxB,IAAI,CAAC,mBAAmB,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;YAC9C,OAAO;QACT,CAAC;QACD,MAAM,aAAa,GAAG,IAAI,CAAC,eAAe,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;QAC9D,IAAI,QAAQ,CAAC,WAAW,EAAE,CAAC;YACzB,QAAQ,CAAC,GAAG,EAAE,CAAC;YACf,OAAO;QACT,CAAC;QAED,QAAQ,CAAC,MAAM,CAAC,aAAa,CAAC,SAAS,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,GAAG,aAAa,EAAE,CAAC,CAAC;IACxE,CAAC;IAMS,eAAe,CAAC,SAAkC,EAAE,MAA8B;QAC1F,yBAAuB,CAAC,MAAM,CAAC,KAAK,CAClC,4CAA4C,MAAM,KAAK,YAAY,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,MAAM,SAAS,CAAC,OAAO,EAAE,CACzG,CAAC;QACF,IAAI,MAAM,KAAK,YAAY,EAAE,CAAC;YAC5B,OAAO,IAAI,0BAAiB,CAAC,uBAAS,CAAC,kBAAkB,CAAC,CAAC;QAC7D,CAAC;QACD,OAAO,IAAI,4BAAmB,CAC5B,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,uBAAS,CAAC,oBAAoB,KAAK,SAAS,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,uBAAS,CAAC,oBAAoB,CAC1G,CAAC;IACJ,CAAC;IAOS,mBAAmB,CAAC,SAAgB,EAAE,QAAkB;QAChE,IAAI,QAAQ,CAAC,WAAW,EAAE,CAAC;YACzB,QAAQ,CAAC,GAAG,EAAE,CAAC;QACjB,CAAC;aAAM,CAAC;YACN,QAAQ;iBACL,MAAM,CAAC,mBAAU,CAAC,qBAAqB,CAAC;iBACxC,IAAI,CAAC,EAAE,UAAU,EAAE,mBAAU,CAAC,qBAAqB,EAAE,OAAO,EAAE,uBAAuB,EAAE,CAAC,CAAC;QAC9F,CAAC;QACD,yBAAuB,CAAC,kBAAkB,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;IAC9D,CAAC;;AApFU,0DAAuB;kCAAvB,uBAAuB;IADnC,IAAA,cAAK,EAAC,gBAAa,CAAC,SAAS,CAAC;GAClB,uBAAuB,CAqFnC"}
|
|
@@ -260,6 +260,8 @@ export interface IAi {
|
|
|
260
260
|
mcp?: boolean | {
|
|
261
261
|
enabled?: boolean;
|
|
262
262
|
oauth?: boolean;
|
|
263
|
+
oauthBaseUrl?: string;
|
|
264
|
+
oauthLoginUrl?: string;
|
|
263
265
|
oauthSecret?: string;
|
|
264
266
|
};
|
|
265
267
|
rateLimit?: IAiRateLimit;
|
|
@@ -386,6 +388,7 @@ export interface IServerOptions {
|
|
|
386
388
|
mapAndValidatePipe?: boolean | {
|
|
387
389
|
nonWhitelistedFields?: 'strip' | 'error' | false;
|
|
388
390
|
};
|
|
391
|
+
mongooseCastErrorFilter?: boolean;
|
|
389
392
|
mongoosePasswordPlugin?: boolean | {
|
|
390
393
|
skipPatterns?: (string | RegExp)[];
|
|
391
394
|
};
|
|
@@ -531,6 +534,7 @@ export interface ICoreModuleOverrides {
|
|
|
531
534
|
conversationService?: Type<any>;
|
|
532
535
|
interactionService?: Type<any>;
|
|
533
536
|
mcpClientService?: Type<any>;
|
|
537
|
+
mcpOAuthService?: Type<any>;
|
|
534
538
|
modeService?: Type<any>;
|
|
535
539
|
placeholderRegistry?: Type<any>;
|
|
536
540
|
preferenceService?: Type<any>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Logger, OnApplicationBootstrap, OnApplicationShutdown, OnModuleInit } from '@nestjs/common';
|
|
2
|
+
import { HttpAdapterHost, ModuleRef, ModulesContainer } from '@nestjs/core';
|
|
3
|
+
import { CoreAiMcpOAuthService } from './services/core-ai-mcp-oauth.service';
|
|
4
|
+
export declare class CoreAiMcpOAuthInitializer implements OnApplicationBootstrap, OnApplicationShutdown, OnModuleInit {
|
|
5
|
+
protected readonly oauthService: CoreAiMcpOAuthService;
|
|
6
|
+
protected readonly httpAdapterHost?: HttpAdapterHost;
|
|
7
|
+
protected readonly moduleRef?: ModuleRef;
|
|
8
|
+
protected readonly modulesContainer?: ModulesContainer;
|
|
9
|
+
protected readonly logger: Logger;
|
|
10
|
+
private mounted?;
|
|
11
|
+
constructor(oauthService: CoreAiMcpOAuthService, httpAdapterHost?: HttpAdapterHost, moduleRef?: ModuleRef, modulesContainer?: ModulesContainer);
|
|
12
|
+
onModuleInit(): Promise<void>;
|
|
13
|
+
onApplicationBootstrap(): void;
|
|
14
|
+
onApplicationShutdown(): void;
|
|
15
|
+
protected resolveMcpPath(): string;
|
|
16
|
+
protected resolveAuthBasePath(): string;
|
|
17
|
+
private controllerPath;
|
|
18
|
+
private modulePath;
|
|
19
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
12
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
13
|
+
};
|
|
14
|
+
var CoreAiMcpOAuthInitializer_1;
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.CoreAiMcpOAuthInitializer = void 0;
|
|
17
|
+
const common_1 = require("@nestjs/common");
|
|
18
|
+
const constants_1 = require("@nestjs/common/constants");
|
|
19
|
+
const shared_utils_1 = require("@nestjs/common/utils/shared.utils");
|
|
20
|
+
const core_1 = require("@nestjs/core");
|
|
21
|
+
const route_path_factory_1 = require("@nestjs/core/router/route-path-factory");
|
|
22
|
+
const config_service_1 = require("../../common/services/config.service");
|
|
23
|
+
const core_better_auth_registry_1 = require("../better-auth/core-better-auth.registry");
|
|
24
|
+
const core_ai_mcp_controller_1 = require("./core-ai-mcp.controller");
|
|
25
|
+
const ai_mcp_oauth_helper_1 = require("./helpers/ai-mcp-oauth.helper");
|
|
26
|
+
const ai_mcp_oauth_registry_1 = require("./helpers/ai-mcp-oauth.registry");
|
|
27
|
+
const core_ai_mcp_oauth_service_1 = require("./services/core-ai-mcp-oauth.service");
|
|
28
|
+
let CoreAiMcpOAuthInitializer = CoreAiMcpOAuthInitializer_1 = class CoreAiMcpOAuthInitializer {
|
|
29
|
+
oauthService;
|
|
30
|
+
httpAdapterHost;
|
|
31
|
+
moduleRef;
|
|
32
|
+
modulesContainer;
|
|
33
|
+
logger = new common_1.Logger(CoreAiMcpOAuthInitializer_1.name);
|
|
34
|
+
mounted;
|
|
35
|
+
constructor(oauthService, httpAdapterHost, moduleRef, modulesContainer) {
|
|
36
|
+
this.oauthService = oauthService;
|
|
37
|
+
this.httpAdapterHost = httpAdapterHost;
|
|
38
|
+
this.moduleRef = moduleRef;
|
|
39
|
+
this.modulesContainer = modulesContainer;
|
|
40
|
+
}
|
|
41
|
+
async onModuleInit() {
|
|
42
|
+
if (!this.oauthService.isOAuthEnabled()) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const adapter = this.httpAdapterHost?.httpAdapter;
|
|
46
|
+
if (!adapter) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
this.oauthService.assertProductionSafe();
|
|
50
|
+
if (adapter.getType() !== 'express') {
|
|
51
|
+
throw new Error(`ai.mcp.oauth requires the Express adapter, but the application runs on "${adapter.getType()}": ` +
|
|
52
|
+
'the MCP OAuth endpoints are an Express router.');
|
|
53
|
+
}
|
|
54
|
+
this.mounted = await (0, ai_mcp_oauth_helper_1.mountAiMcpOAuthRouter)(adapter.getInstance(), (router) => adapter.use(router), this.oauthService, {
|
|
55
|
+
baseUrl: config_service_1.ConfigService.get('ai.mcp.oauthBaseUrl'),
|
|
56
|
+
authBasePath: this.resolveAuthBasePath(),
|
|
57
|
+
baseUrlSource: 'ai.mcp.oauthBaseUrl',
|
|
58
|
+
mcpPath: this.resolveMcpPath(),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
onApplicationBootstrap() {
|
|
62
|
+
if (!this.mounted || !this.oauthService.usesBuiltInConsent() || (0, core_better_auth_registry_1.getBetterAuthTokenService)()) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
this.logger.warn('ai.mcp.oauth is enabled, but Better-Auth is not active: the built-in consent page has no way to sign a user ' +
|
|
66
|
+
'in, so no MCP client can be authorized. Enable betterAuth, or register a CoreAiMcpOAuthService subclass ' +
|
|
67
|
+
'with an authorizeConsent() of your own via CoreModule.forRoot(env, { ai: { mcpOAuthService } }).');
|
|
68
|
+
}
|
|
69
|
+
onApplicationShutdown() {
|
|
70
|
+
if (this.mounted && (0, ai_mcp_oauth_registry_1.getMountedMcpOAuth)() === this.mounted) {
|
|
71
|
+
(0, ai_mcp_oauth_registry_1.setMountedMcpOAuth)(undefined);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
resolveMcpPath() {
|
|
75
|
+
const container = this.moduleRef
|
|
76
|
+
?.container;
|
|
77
|
+
const applicationConfig = container?.applicationConfig;
|
|
78
|
+
const ctrlPath = this.controllerPath();
|
|
79
|
+
if (!applicationConfig) {
|
|
80
|
+
return ctrlPath;
|
|
81
|
+
}
|
|
82
|
+
const versioningOptions = applicationConfig.getVersioning();
|
|
83
|
+
const controllerVersion = versioningOptions
|
|
84
|
+
? (Reflect.getMetadata(constants_1.VERSION_METADATA, core_ai_mcp_controller_1.CoreAiMcpController) ?? versioningOptions.defaultVersion)
|
|
85
|
+
: undefined;
|
|
86
|
+
const paths = new route_path_factory_1.RoutePathFactory(applicationConfig).create({
|
|
87
|
+
controllerVersion,
|
|
88
|
+
ctrlPath,
|
|
89
|
+
globalPrefix: (0, shared_utils_1.addLeadingSlash)(applicationConfig.getGlobalPrefix()),
|
|
90
|
+
methodPath: '/',
|
|
91
|
+
modulePath: this.modulePath(),
|
|
92
|
+
versioningOptions,
|
|
93
|
+
}, common_1.RequestMethod.POST);
|
|
94
|
+
if (paths.length > 1) {
|
|
95
|
+
this.logger.warn(`The MCP endpoint is registered at ${paths.join(', ')}; the OAuth metadata describes the first one only.`);
|
|
96
|
+
}
|
|
97
|
+
return paths[0];
|
|
98
|
+
}
|
|
99
|
+
resolveAuthBasePath() {
|
|
100
|
+
const basePath = `/${String(config_service_1.ConfigService.get('betterAuth.basePath') || 'iam').replace(/^\//, '')}`;
|
|
101
|
+
const applicationConfig = this.moduleRef
|
|
102
|
+
?.container?.applicationConfig;
|
|
103
|
+
if (!applicationConfig) {
|
|
104
|
+
return basePath;
|
|
105
|
+
}
|
|
106
|
+
const versioningOptions = applicationConfig.getVersioning();
|
|
107
|
+
const [signIn] = new route_path_factory_1.RoutePathFactory(applicationConfig).create({
|
|
108
|
+
controllerVersion: versioningOptions?.defaultVersion,
|
|
109
|
+
ctrlPath: basePath,
|
|
110
|
+
globalPrefix: (0, shared_utils_1.addLeadingSlash)(applicationConfig.getGlobalPrefix()),
|
|
111
|
+
methodPath: '/sign-in/email',
|
|
112
|
+
versioningOptions,
|
|
113
|
+
}, common_1.RequestMethod.POST);
|
|
114
|
+
return signIn.replace(/\/sign-in\/email$/, '');
|
|
115
|
+
}
|
|
116
|
+
controllerPath() {
|
|
117
|
+
const declared = Reflect.getMetadata(constants_1.PATH_METADATA, core_ai_mcp_controller_1.CoreAiMcpController);
|
|
118
|
+
const path = Array.isArray(declared) ? declared[0] : declared;
|
|
119
|
+
return `/${String(path ?? 'ai/mcp').replace(/^\//, '')}`;
|
|
120
|
+
}
|
|
121
|
+
modulePath() {
|
|
122
|
+
if (!this.modulesContainer) {
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
for (const module of this.modulesContainer.values()) {
|
|
126
|
+
if (module.controllers.has(core_ai_mcp_controller_1.CoreAiMcpController)) {
|
|
127
|
+
return (Reflect.getMetadata(constants_1.MODULE_PATH + this.modulesContainer.applicationId, module.metatype) ??
|
|
128
|
+
Reflect.getMetadata(constants_1.MODULE_PATH, module.metatype));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
exports.CoreAiMcpOAuthInitializer = CoreAiMcpOAuthInitializer;
|
|
135
|
+
exports.CoreAiMcpOAuthInitializer = CoreAiMcpOAuthInitializer = CoreAiMcpOAuthInitializer_1 = __decorate([
|
|
136
|
+
(0, common_1.Injectable)(),
|
|
137
|
+
__param(1, (0, common_1.Optional)()),
|
|
138
|
+
__param(2, (0, common_1.Optional)()),
|
|
139
|
+
__param(3, (0, common_1.Optional)()),
|
|
140
|
+
__metadata("design:paramtypes", [core_ai_mcp_oauth_service_1.CoreAiMcpOAuthService,
|
|
141
|
+
core_1.HttpAdapterHost,
|
|
142
|
+
core_1.ModuleRef,
|
|
143
|
+
core_1.ModulesContainer])
|
|
144
|
+
], CoreAiMcpOAuthInitializer);
|
|
145
|
+
//# sourceMappingURL=core-ai-mcp-oauth.initializer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"core-ai-mcp-oauth.initializer.js","sourceRoot":"","sources":["../../../../src/core/modules/ai/core-ai-mcp-oauth.initializer.ts"],"names":[],"mappings":"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"}
|
|
@@ -23,6 +23,7 @@ const config_service_1 = require("../../common/services/config.service");
|
|
|
23
23
|
const core_redis_service_1 = require("../../common/services/core-redis.service");
|
|
24
24
|
const core_better_auth_module_1 = require("../better-auth/core-better-auth.module");
|
|
25
25
|
const error_codes_1 = require("../error-code/error-codes");
|
|
26
|
+
const ai_mcp_oauth_registry_1 = require("./helpers/ai-mcp-oauth.registry");
|
|
26
27
|
const core_ai_mcp_oauth_service_1 = require("./services/core-ai-mcp-oauth.service");
|
|
27
28
|
const core_ai_mcp_service_1 = require("./services/core-ai-mcp.service");
|
|
28
29
|
let CoreAiMcpController = CoreAiMcpController_1 = class CoreAiMcpController {
|
|
@@ -43,7 +44,7 @@ let CoreAiMcpController = CoreAiMcpController_1 = class CoreAiMcpController {
|
|
|
43
44
|
async handlePost(req, res) {
|
|
44
45
|
const user = await this.resolveUser(req);
|
|
45
46
|
if (!user?.id) {
|
|
46
|
-
this.unauthorized(
|
|
47
|
+
this.unauthorized(res);
|
|
47
48
|
return;
|
|
48
49
|
}
|
|
49
50
|
const sessionId = req.headers['mcp-session-id'];
|
|
@@ -141,7 +142,7 @@ let CoreAiMcpController = CoreAiMcpController_1 = class CoreAiMcpController {
|
|
|
141
142
|
async handleSessionRequest(req, res) {
|
|
142
143
|
const user = await this.resolveUser(req);
|
|
143
144
|
if (!user?.id) {
|
|
144
|
-
this.unauthorized(
|
|
145
|
+
this.unauthorized(res);
|
|
145
146
|
return;
|
|
146
147
|
}
|
|
147
148
|
const sessionId = req.headers['mcp-session-id'];
|
|
@@ -243,11 +244,11 @@ let CoreAiMcpController = CoreAiMcpController_1 = class CoreAiMcpController {
|
|
|
243
244
|
statusCode: 503,
|
|
244
245
|
});
|
|
245
246
|
}
|
|
246
|
-
unauthorized(
|
|
247
|
-
const
|
|
247
|
+
unauthorized(res) {
|
|
248
|
+
const oauth = (0, ai_mcp_oauth_registry_1.getMountedMcpOAuth)();
|
|
248
249
|
res
|
|
249
250
|
.status(401)
|
|
250
|
-
.set({ 'WWW-Authenticate': `Bearer resource_metadata="${
|
|
251
|
+
.set({ 'WWW-Authenticate': oauth ? `Bearer resource_metadata="${oauth.resourceMetadataUrl}"` : 'Bearer' })
|
|
251
252
|
.json({ error: 'Unauthorized: a valid Bearer token is required' });
|
|
252
253
|
}
|
|
253
254
|
evictIfNeeded(ownerId) {
|