@lenne.tech/nest-server 11.42.16 → 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.
Files changed (51) hide show
  1. package/.claude/rules/better-auth.md +22 -0
  2. package/.claude/rules/configurable-features.md +3 -3
  3. package/.claude/rules/testing.md +4 -4
  4. package/FRAMEWORK-API.md +2 -2
  5. package/dist/core/common/interfaces/server-options.interface.d.ts +3 -0
  6. package/dist/core/modules/ai/core-ai-mcp-oauth.initializer.d.ts +19 -0
  7. package/dist/core/modules/ai/core-ai-mcp-oauth.initializer.js +145 -0
  8. package/dist/core/modules/ai/core-ai-mcp-oauth.initializer.js.map +1 -0
  9. package/dist/core/modules/ai/core-ai-mcp.controller.js +6 -5
  10. package/dist/core/modules/ai/core-ai-mcp.controller.js.map +1 -1
  11. package/dist/core/modules/ai/core-ai.module.d.ts +2 -0
  12. package/dist/core/modules/ai/core-ai.module.js +6 -1
  13. package/dist/core/modules/ai/core-ai.module.js.map +1 -1
  14. package/dist/core/modules/ai/helpers/ai-mcp-oauth-consent.helper.d.ts +33 -0
  15. package/dist/core/modules/ai/helpers/ai-mcp-oauth-consent.helper.js +282 -0
  16. package/dist/core/modules/ai/helpers/ai-mcp-oauth-consent.helper.js.map +1 -0
  17. package/dist/core/modules/ai/helpers/ai-mcp-oauth.helper.d.ts +15 -2
  18. package/dist/core/modules/ai/helpers/ai-mcp-oauth.helper.js +73 -19
  19. package/dist/core/modules/ai/helpers/ai-mcp-oauth.helper.js.map +1 -1
  20. package/dist/core/modules/ai/helpers/ai-mcp-oauth.registry.d.ts +5 -0
  21. package/dist/core/modules/ai/helpers/ai-mcp-oauth.registry.js.map +1 -1
  22. package/dist/core/modules/ai/index.d.ts +1 -0
  23. package/dist/core/modules/ai/index.js +1 -0
  24. package/dist/core/modules/ai/index.js.map +1 -1
  25. package/dist/core/modules/ai/services/core-ai-mcp-oauth.service.d.ts +30 -2
  26. package/dist/core/modules/ai/services/core-ai-mcp-oauth.service.js +222 -15
  27. package/dist/core/modules/ai/services/core-ai-mcp-oauth.service.js.map +1 -1
  28. package/dist/core/modules/better-auth/better-auth.config.d.ts +4 -0
  29. package/dist/core/modules/better-auth/better-auth.config.js +29 -2
  30. package/dist/core/modules/better-auth/better-auth.config.js.map +1 -1
  31. package/dist/core/modules/better-auth/core-better-auth.controller.d.ts +2 -0
  32. package/dist/core/modules/better-auth/core-better-auth.controller.js +36 -1
  33. package/dist/core/modules/better-auth/core-better-auth.controller.js.map +1 -1
  34. package/dist/tsconfig.build.tsbuildinfo +1 -1
  35. package/docs/REQUEST-LIFECYCLE.md +1 -1
  36. package/migration-guides/11.42.x-to-11.43.x.md +195 -0
  37. package/package.json +4 -4
  38. package/src/core/common/helpers/swagger.helper.ts +4 -4
  39. package/src/core/common/interfaces/server-options.interface.ts +37 -9
  40. package/src/core/modules/ai/INTEGRATION-CHECKLIST.md +50 -24
  41. package/src/core/modules/ai/README.md +98 -13
  42. package/src/core/modules/ai/core-ai-mcp-oauth.initializer.ts +199 -0
  43. package/src/core/modules/ai/core-ai-mcp.controller.ts +13 -6
  44. package/src/core/modules/ai/core-ai.module.ts +14 -2
  45. package/src/core/modules/ai/helpers/ai-mcp-oauth-consent.helper.ts +369 -0
  46. package/src/core/modules/ai/helpers/ai-mcp-oauth.helper.ts +149 -43
  47. package/src/core/modules/ai/helpers/ai-mcp-oauth.registry.ts +18 -5
  48. package/src/core/modules/ai/index.ts +1 -0
  49. package/src/core/modules/ai/services/core-ai-mcp-oauth.service.ts +385 -28
  50. package/src/core/modules/better-auth/better-auth.config.ts +49 -5
  51. package/src/core/modules/better-auth/core-better-auth.controller.ts +75 -1
@@ -3,6 +3,7 @@ import {
3
3
  BadRequestException,
4
4
  Body,
5
5
  Controller,
6
+ ForbiddenException,
6
7
  Get,
7
8
  HttpCode,
8
9
  HttpException,
@@ -399,6 +400,9 @@ export class CoreBetterAuthController {
399
400
  email: input.email,
400
401
  password: normalizedPassword,
401
402
  },
403
+ // With a request, Better-Auth answers a Response by default; this controller reads the data.
404
+ asResponse: false,
405
+ request: this.csrfCheckedRequest(req ?? (res as any)?.req, '/sign-in/email'),
402
406
  })) as BetterAuthSignInResponse | null;
403
407
 
404
408
  if (!response) {
@@ -436,7 +440,7 @@ export class CoreBetterAuthController {
436
440
  const signInUrl = new URL(`${basePath}/sign-in/email`, baseUrl);
437
441
 
438
442
  // Create a new Web Request for Better Auth's native handler
439
- const webRequest = new Request(signInUrl.toString(), {
443
+ const webRequest = new globalThis.Request(signInUrl.toString(), {
440
444
  body: modifiedBody,
441
445
  headers: new Headers({
442
446
  'Content-Type': 'application/json',
@@ -507,6 +511,7 @@ export class CoreBetterAuthController {
507
511
  if (error instanceof UnauthorizedException) {
508
512
  throw error;
509
513
  }
514
+ this.rethrowCsrfRefusal(error, 'sign-in');
510
515
 
511
516
  throw new UnauthorizedException(ErrorCode.INVALID_CREDENTIALS);
512
517
  }
@@ -569,6 +574,9 @@ export class CoreBetterAuthController {
569
574
  name: input.name || input.email.split('@')[0],
570
575
  password: normalizedPassword,
571
576
  },
577
+ // With a request, Better-Auth answers a Response by default; this controller reads the data.
578
+ asResponse: false,
579
+ request: this.csrfCheckedRequest((res as any)?.req, '/sign-up/email'),
572
580
  });
573
581
 
574
582
  if (!response) {
@@ -637,6 +645,7 @@ export class CoreBetterAuthController {
637
645
  } catch (error) {
638
646
  const errorMessage = error instanceof Error ? error.message : 'Unknown error';
639
647
  this.logger.debug(`Sign-up error: ${errorMessage}`);
648
+ this.rethrowCsrfRefusal(error, 'sign-up');
640
649
  if (errorMessage.includes('already exists')) {
641
650
  throw new BadRequestException(ErrorCode.EMAIL_ALREADY_EXISTS);
642
651
  }
@@ -644,6 +653,71 @@ export class CoreBetterAuthController {
644
653
  }
645
654
  }
646
655
 
656
+ /**
657
+ * The Web `Request` handed to `api.signInEmail()` / `api.signUpEmail()`, so Better-Auth's own CSRF
658
+ * protection runs. Both endpoints carry `formCsrfMiddleware`: it refuses a cross-site NAVIGATION
659
+ * (`Sec-Fetch-Site: cross-site` + `Sec-Fetch-Mode: navigate`, i.e. a form a hostile page submits)
660
+ * and checks a present `Origin` / `Referer` against `trustedOrigins`. It reads them from
661
+ * `ctx.request` — and called without one, the API skipped the check entirely, so a hostile page
662
+ * could sign a visitor in to an account of the attacker's choosing (login CSRF): whatever the
663
+ * visitor did next, including approving an MCP client on the consent page, happened in the
664
+ * attacker's account.
665
+ *
666
+ * Only those signals are forwarded, and only from a request that comes from a browser — login CSRF
667
+ * needs a victim's browser, so a client without one has nothing to protect. A browser sends `Origin`
668
+ * on every POST and `Sec-Fetch-Site` with all fetch metadata; a request carrying neither (nor a
669
+ * `Referer`) is not one. That matters because Node's built-in `fetch` (undici) sends a lone
670
+ * `Sec-Fetch-Mode: cors` and no `Origin`, which Better-Auth reads as a browser request missing its
671
+ * origin and refuses — every server-side script, seeder or SSR route signing in with `fetch` would
672
+ * answer 403. Not the cookie either: its presence alone makes Better-Auth demand an `Origin`, which
673
+ * would refuse non-browser clients that keep a cookie jar (API tests, apps) — a change unrelated to
674
+ * CSRF, since every browser sends `Origin` on a cross-site POST anyway.
675
+ * Returns `undefined` without a request (a subclass calling `super` with a stub), which keeps the
676
+ * call working as before.
677
+ */
678
+ protected csrfCheckedRequest(req: Request | undefined, path: string): globalThis.Request | undefined {
679
+ if (!req?.headers) {
680
+ return undefined;
681
+ }
682
+ const header = (name: string) => {
683
+ const value = req.headers[name];
684
+ return typeof value === 'string' && value ? value : undefined;
685
+ };
686
+ const fromBrowser = ['origin', 'referer', 'sec-fetch-site'].some((name) => header(name));
687
+ const headers = new Headers();
688
+ if (fromBrowser) {
689
+ for (const name of ['origin', 'referer', 'sec-fetch-dest', 'sec-fetch-mode', 'sec-fetch-site']) {
690
+ const value = header(name);
691
+ if (value) {
692
+ headers.set(name, value);
693
+ }
694
+ }
695
+ }
696
+ // The URL only names the endpoint; the checks read the headers. A missing baseUrl must not turn
697
+ // every sign-in into an error, so it falls back to a placeholder origin.
698
+ const url = new URL(
699
+ `${this.betterAuthService.getBasePath()}${path}`,
700
+ this.betterAuthService.getBaseUrl() || 'http://localhost',
701
+ );
702
+ return new globalThis.Request(url, { headers, method: 'POST' });
703
+ }
704
+
705
+ /**
706
+ * Answers a request Better-Auth's CSRF protection refused with 403, not as bad credentials: no
707
+ * sign-in can ever make it succeed, and "invalid credentials" would send whoever debugs a missing
708
+ * `trustedOrigins` entry looking at the password.
709
+ */
710
+ protected rethrowCsrfRefusal(error: unknown, action: string): void {
711
+ const code = (error as { body?: { code?: string } })?.body?.code;
712
+ if (code && ['CROSS_SITE_NAVIGATION_LOGIN_BLOCKED', 'INVALID_ORIGIN', 'MISSING_OR_NULL_ORIGIN'].includes(code)) {
713
+ this.logger.warn(
714
+ `Refused ${action}: ${code} (Better-Auth CSRF protection). If this is your own frontend, add its origin to ` +
715
+ 'cors.allowedOrigins — an explicit betterAuth.trustedOrigins replaces the derived list.',
716
+ );
717
+ throw new ForbiddenException(ErrorCode.ACCESS_DENIED);
718
+ }
719
+ }
720
+
647
721
  /**
648
722
  * Sign out (logout)
649
723
  *