@lenne.tech/nest-server 11.42.8 → 11.42.9
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 +24 -0
- package/.claude/rules/configurable-features.md +5 -3
- package/.claude/rules/testing.md +4 -4
- package/FRAMEWORK-API.md +3 -2
- package/dist/core/common/decorators/api-methods.decorator.d.ts +2 -0
- package/dist/core/common/decorators/api-methods.decorator.js +8 -0
- package/dist/core/common/decorators/api-methods.decorator.js.map +1 -0
- package/dist/core/common/helpers/swagger.helper.js +312 -1
- package/dist/core/common/helpers/swagger.helper.js.map +1 -1
- package/dist/core/common/interfaces/server-options.interface.d.ts +1 -0
- package/dist/core/modules/ai/core-ai-mcp.controller.js +15 -1
- package/dist/core/modules/ai/core-ai-mcp.controller.js.map +1 -1
- package/dist/core/modules/ai/helpers/ai-mcp-oauth.helper.js +8 -1
- 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 +7 -0
- package/dist/core/modules/ai/helpers/ai-mcp-oauth.registry.js +12 -0
- package/dist/core/modules/ai/helpers/ai-mcp-oauth.registry.js.map +1 -0
- package/dist/core/modules/better-auth/better-auth.config.js +12 -0
- package/dist/core/modules/better-auth/better-auth.config.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-api.middleware.js +2 -2
- package/dist/core/modules/better-auth/core-better-auth-api.middleware.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-openapi.helper.d.ts +62 -0
- package/dist/core/modules/better-auth/core-better-auth-openapi.helper.js +95 -0
- package/dist/core/modules/better-auth/core-better-auth-openapi.helper.js.map +1 -0
- package/dist/core/modules/better-auth/core-better-auth.service.d.ts +10 -2
- package/dist/core/modules/better-auth/core-better-auth.service.js +89 -0
- package/dist/core/modules/better-auth/core-better-auth.service.js.map +1 -1
- package/dist/core/modules/tus/core-tus.controller.js +3 -0
- package/dist/core/modules/tus/core-tus.controller.js.map +1 -1
- package/dist/core/modules/tus/core-tus.service.d.ts +3 -0
- package/dist/core/modules/tus/core-tus.service.js +58 -25
- package/dist/core/modules/tus/core-tus.service.js.map +1 -1
- package/dist/core/modules/tus/interfaces/tus-config.interface.js +1 -0
- package/dist/core/modules/tus/interfaces/tus-config.interface.js.map +1 -1
- package/dist/core/modules/tus/tus.module.d.ts +2 -0
- package/dist/core/modules/tus/tus.module.js +23 -1
- package/dist/core/modules/tus/tus.module.js.map +1 -1
- package/dist/core.module.js +1 -1
- 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 +9 -1
- package/migration-guides/11.42.8-to-11.42.9.md +186 -0
- package/package.json +1 -1
- package/src/core/common/decorators/api-methods.decorator.ts +25 -0
- package/src/core/common/helpers/swagger.helper.ts +454 -1
- package/src/core/common/interfaces/server-options.interface.ts +46 -8
- package/src/core/common/interfaces/swagger-setup-options.interface.ts +5 -2
- package/src/core/modules/ai/README.md +7 -0
- package/src/core/modules/ai/core-ai-mcp.controller.ts +19 -2
- package/src/core/modules/ai/helpers/ai-mcp-oauth.helper.ts +12 -2
- package/src/core/modules/ai/helpers/ai-mcp-oauth.registry.ts +29 -0
- package/src/core/modules/better-auth/README.md +34 -0
- package/src/core/modules/better-auth/better-auth.config.ts +27 -1
- package/src/core/modules/better-auth/core-better-auth-api.middleware.ts +4 -17
- package/src/core/modules/better-auth/core-better-auth-openapi.helper.ts +219 -0
- package/src/core/modules/better-auth/core-better-auth.service.ts +166 -2
- package/src/core/modules/tus/README.md +50 -14
- package/src/core/modules/tus/core-tus.controller.ts +6 -0
- package/src/core/modules/tus/core-tus.service.ts +97 -26
- package/src/core/modules/tus/interfaces/tus-config.interface.ts +1 -0
- package/src/core/modules/tus/tus.module.ts +43 -1
- package/src/core.module.ts +2 -1
- package/src/index.ts +1 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Controller, Delete, Get, Logger, OnModuleDestroy, Optional, Post, Req, Res } from '@nestjs/common';
|
|
2
|
-
import {
|
|
2
|
+
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
|
3
3
|
import { Request, Response } from 'express';
|
|
4
4
|
import { hostname } from 'node:os';
|
|
5
5
|
|
|
@@ -62,7 +62,10 @@ interface McpSessionEntry {
|
|
|
62
62
|
user: AiToolUser;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
|
|
65
|
+
// The route guard admits everybody because the endpoint authenticates itself (session/JWT bearer, or an
|
|
66
|
+
// MCP OAuth token): the bearer requirement is declared for Swagger, which setupSwagger() keeps.
|
|
67
|
+
@ApiBearerAuth('bearer')
|
|
68
|
+
@ApiTags('AI MCP')
|
|
66
69
|
@Controller('ai/mcp')
|
|
67
70
|
@Roles(RoleEnum.S_EVERYONE)
|
|
68
71
|
export class CoreAiMcpController implements OnModuleDestroy {
|
|
@@ -108,6 +111,12 @@ export class CoreAiMcpController implements OnModuleDestroy {
|
|
|
108
111
|
@Optional() protected readonly redisService?: CoreRedisService,
|
|
109
112
|
) {}
|
|
110
113
|
|
|
114
|
+
@ApiOperation({
|
|
115
|
+
description:
|
|
116
|
+
'Model Context Protocol endpoint (Streamable HTTP) for MCP clients: JSON-RPC 2.0 messages, starting with `initialize`. ' +
|
|
117
|
+
'The response carries the `mcp-session-id` header that later requests send back. Requires a bearer token (session, JWT or MCP OAuth token).',
|
|
118
|
+
summary: 'MCP request',
|
|
119
|
+
})
|
|
111
120
|
@Post()
|
|
112
121
|
async handlePost(@Req() req: Request, @Res() res: Response): Promise<void> {
|
|
113
122
|
const user = await this.resolveUser(req);
|
|
@@ -206,11 +215,19 @@ export class CoreAiMcpController implements OnModuleDestroy {
|
|
|
206
215
|
}
|
|
207
216
|
}
|
|
208
217
|
|
|
218
|
+
@ApiOperation({
|
|
219
|
+
description: 'Server-sent event stream of an MCP session (`mcp-session-id` header). Requires a bearer token.',
|
|
220
|
+
summary: 'MCP event stream',
|
|
221
|
+
})
|
|
209
222
|
@Get()
|
|
210
223
|
async handleGet(@Req() req: Request, @Res() res: Response): Promise<void> {
|
|
211
224
|
await this.handleSessionRequest(req, res);
|
|
212
225
|
}
|
|
213
226
|
|
|
227
|
+
@ApiOperation({
|
|
228
|
+
description: 'Ends an MCP session (`mcp-session-id` header). Requires a bearer token.',
|
|
229
|
+
summary: 'End MCP session',
|
|
230
|
+
})
|
|
214
231
|
@Delete()
|
|
215
232
|
async handleDelete(@Req() req: Request, @Res() res: Response): Promise<void> {
|
|
216
233
|
await this.handleSessionRequest(req, res);
|
|
@@ -3,6 +3,7 @@ import { Logger } from '@nestjs/common';
|
|
|
3
3
|
import { resolveServerUrls } from '../../../common/helpers/cookies.helper';
|
|
4
4
|
import { ConfigService } from '../../../common/services/config.service';
|
|
5
5
|
import { CoreAiMcpOAuthService } from '../services/core-ai-mcp-oauth.service';
|
|
6
|
+
import { setMountedMcpOAuth } from './ai-mcp-oauth.registry';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Mount the MCP OAuth 2.1 router (`mcpAuthRouter`) on a NestJS application.
|
|
@@ -10,7 +11,9 @@ import { CoreAiMcpOAuthService } from '../services/core-ai-mcp-oauth.service';
|
|
|
10
11
|
* Call this in `main.ts` AFTER `app.init()` when `ai.mcp.oauth` is enabled. It
|
|
11
12
|
* lazy-imports `@modelcontextprotocol/sdk` and wires the OAuth provider built from
|
|
12
13
|
* {@link CoreAiMcpOAuthService}, exposing the standard discovery + token endpoints
|
|
13
|
-
* (`/.well-known/oauth-*`, `/authorize`, `/token`, `/register
|
|
14
|
+
* (`/.well-known/oauth-*`, `/authorize`, `/token`, `/register`; `/revoke` only for a provider that
|
|
15
|
+
* implements `revokeToken`, which the built-in one does not). `setupSwagger()` documents exactly the
|
|
16
|
+
* endpoints mounted here.
|
|
14
17
|
*
|
|
15
18
|
* The interactive consent step requires `CoreAiMcpOAuthService.authorizeConsent`
|
|
16
19
|
* to be overridden with your login/consent UI (see INTEGRATION-CHECKLIST).
|
|
@@ -46,10 +49,11 @@ export async function mountAiMcpOAuth(
|
|
|
46
49
|
const mcpPath = options.mcpPath ?? '/ai/mcp';
|
|
47
50
|
|
|
48
51
|
let router: unknown;
|
|
52
|
+
const provider = oauthService.buildOAuthProvider() as any;
|
|
49
53
|
try {
|
|
50
54
|
router = mcpAuthRouter({
|
|
51
55
|
issuerUrl,
|
|
52
|
-
provider
|
|
56
|
+
provider,
|
|
53
57
|
resourceServerUrl: new URL(`${baseUrl.replace(/\/$/, '')}${mcpPath}`),
|
|
54
58
|
});
|
|
55
59
|
} catch (error) {
|
|
@@ -63,6 +67,12 @@ export async function mountAiMcpOAuth(
|
|
|
63
67
|
}
|
|
64
68
|
|
|
65
69
|
app.use(router);
|
|
70
|
+
// The same conditions mcpAuthRouter() uses to decide whether /register and /revoke exist.
|
|
71
|
+
setMountedMcpOAuth({
|
|
72
|
+
mcpPath,
|
|
73
|
+
registration: !!provider?.clientsStore?.registerClient,
|
|
74
|
+
revocation: !!provider?.revokeToken,
|
|
75
|
+
});
|
|
66
76
|
new Logger('mountAiMcpOAuth').log(`MCP OAuth issuer: ${issuerUrl.href} (from ${source})`);
|
|
67
77
|
}
|
|
68
78
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What `mountAiMcpOAuth()` mounted, for `setupSwagger()`.
|
|
3
|
+
*
|
|
4
|
+
* The MCP OAuth endpoints are an Express router mounted with `app.use()`, not Nest controllers, so
|
|
5
|
+
* `@nestjs/swagger` cannot see them. The mount records here what it put in place — only then are the
|
|
6
|
+
* endpoints documented, so the document follows the actual call in `main.ts`, not a config flag.
|
|
7
|
+
*
|
|
8
|
+
* Imports nothing on purpose: read by the common Swagger helper without pulling in the AI module.
|
|
9
|
+
*/
|
|
10
|
+
export interface IMountedMcpOAuth {
|
|
11
|
+
/** Path of the protected MCP resource (`/ai/mcp`) */
|
|
12
|
+
mcpPath: string;
|
|
13
|
+
/** Whether `/register` (dynamic client registration) is mounted */
|
|
14
|
+
registration: boolean;
|
|
15
|
+
/** Whether `/revoke` is mounted */
|
|
16
|
+
revocation: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let mounted: IMountedMcpOAuth | undefined;
|
|
20
|
+
|
|
21
|
+
/** Records the mounted MCP OAuth endpoints (called by `mountAiMcpOAuth()`). */
|
|
22
|
+
export function setMountedMcpOAuth(value: IMountedMcpOAuth | undefined): void {
|
|
23
|
+
mounted = value;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The mounted MCP OAuth endpoints, or `undefined` when `mountAiMcpOAuth()` was not called. */
|
|
27
|
+
export function getMountedMcpOAuth(): IMountedMcpOAuth | undefined {
|
|
28
|
+
return mounted;
|
|
29
|
+
}
|
|
@@ -1146,6 +1146,40 @@ When enabled, Better-Auth exposes the following endpoints at the configured `bas
|
|
|
1146
1146
|
| `/iam/reset-password` | POST | Reset password with token |
|
|
1147
1147
|
| `/iam/verify-email` | POST | Verify email address |
|
|
1148
1148
|
|
|
1149
|
+
### In the Swagger document (since 11.42.9)
|
|
1150
|
+
|
|
1151
|
+
`setupSwagger()` documents every route this module makes reachable under the base path, as the server
|
|
1152
|
+
is configured:
|
|
1153
|
+
|
|
1154
|
+
- The routes `CoreBetterAuthController` handles itself (`sign-in/email`, `sign-up/email`, `sign-out`,
|
|
1155
|
+
`session`, `features`) come from the controller, as for any Nest controller.
|
|
1156
|
+
- The routes Better-Auth serves itself — everything the configured plugins add (two-factor, passkey,
|
|
1157
|
+
sessions, password change, …) — come from Better-Auth's own OpenAPI generator
|
|
1158
|
+
(`CoreBetterAuthService.getOpenApiSchema()`, prepared at bootstrap). They are documented under the
|
|
1159
|
+
project's Better-Auth controller: its tags, its `operationTags`, operation ids
|
|
1160
|
+
`<Controller>_betterAuth_<betterAuthOperationId>`, schemas prefixed `BetterAuth` (`BetterAuthUser`).
|
|
1161
|
+
- Only what can succeed is documented. `switchedOffBetterAuthPaths()` reads the nest-server switches
|
|
1162
|
+
(sign-in/sign-up under `emailAndPassword.enabled: false`, `sign-up/email` under `disableSignUp`, the
|
|
1163
|
+
reset routes under `passwordReset: false`); `unavailableBetterAuthOperations()` reads the LIVE
|
|
1164
|
+
instance at bootstrap — so `betterAuth.options` and project plugins count — and drops e.g. POST
|
|
1165
|
+
`/get-session` without `session.deferSessionRefresh`, social routes without a provider, two-factor OTP
|
|
1166
|
+
routes without `otpOptions.sendOTP`. Both apply to the controller's own routes as well.
|
|
1167
|
+
- Security says what Better-Auth enforces, not what its generator writes (every route bearer-only): a
|
|
1168
|
+
route guarded by one of Better-Auth's session middlewares — or one of the three social-account routes
|
|
1169
|
+
that check the session in their handler (`HANDLER_SESSION_PATHS`) — carries the document's global
|
|
1170
|
+
requirement; every other route is public (`security: []`): sign-in, password reset, get-session, the
|
|
1171
|
+
two-factor verification steps of a sign-in. Routes of a plugin the module does not register itself
|
|
1172
|
+
(`admin()`, `organization()`, anything in `betterAuth.plugins`) always keep the global requirement:
|
|
1173
|
+
such a plugin may check the session in a middleware of its own, which cannot be recognised from outside.
|
|
1174
|
+
- Under a global prefix or URI versioning the middleware does not forward Better-Auth's own routes, so
|
|
1175
|
+
they are not documented then (a warning says so).
|
|
1176
|
+
|
|
1177
|
+
Better-Auth exposes its generator only through its `openAPI()` plugin, so the module registers that plugin
|
|
1178
|
+
itself (`disableDefaultReference: true`). Its own route, `/iam/open-api/generate-schema`, is not part of
|
|
1179
|
+
the API: the plugin's endpoints are marked server-only, so Better-Auth never routes them (404, however the
|
|
1180
|
+
path is spelled) and the module calls the generator in-process. A project that adds `openAPI()` to
|
|
1181
|
+
`betterAuth.plugins` itself keeps those routes — then it is the project's decision.
|
|
1182
|
+
|
|
1149
1183
|
### JWT Token Endpoint
|
|
1150
1184
|
|
|
1151
1185
|
The `/iam/token` endpoint returns a fresh JWT token for the current session. Use this when your JWT has expired but your session is still valid.
|
|
@@ -2,7 +2,7 @@ import { passkey } from '@better-auth/passkey';
|
|
|
2
2
|
import { Logger } from '@nestjs/common';
|
|
3
3
|
import { betterAuth, BetterAuthPlugin } from 'better-auth';
|
|
4
4
|
import { mongodbAdapter } from 'better-auth/adapters/mongodb';
|
|
5
|
-
import { jwt, twoFactor } from 'better-auth/plugins';
|
|
5
|
+
import { jwt, openAPI, twoFactor } from 'better-auth/plugins';
|
|
6
6
|
import * as crypto from 'crypto';
|
|
7
7
|
import * as fs from 'fs';
|
|
8
8
|
import * as path from 'path';
|
|
@@ -11,6 +11,7 @@ import { resolveServerUrls, strippedApiHostname } from '../../common/helpers/coo
|
|
|
11
11
|
import { revalidateWsConnectionsOf } from '../../common/helpers/graphql-ws-connection.helper';
|
|
12
12
|
import { IBetterAuth, ICorsConfig } from '../../common/interfaces/server-options.interface';
|
|
13
13
|
import { detectCookiePrefixDrift, resolveBetterAuthCookiePrefix } from './better-auth-cookie-prefix.helper';
|
|
14
|
+
import { projectProvidesOpenApiPlugin } from './core-better-auth-openapi.helper';
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Type for better-auth instance with plugins
|
|
@@ -978,6 +979,16 @@ function buildPlugins(
|
|
|
978
979
|
plugins.push(passkey(passkeyOptions));
|
|
979
980
|
}
|
|
980
981
|
|
|
982
|
+
// Better-Auth's OpenAPI generator, so setupSwagger() can document exactly the routes the configured
|
|
983
|
+
// plugins add (CoreBetterAuthService.getOpenApiSchema()). Better-Auth exposes the generator only
|
|
984
|
+
// through this plugin, and the plugin only as HTTP routes — which are not part of the API. They are
|
|
985
|
+
// kept off the router rather than refused by path: a path check sees the request as sent, while
|
|
986
|
+
// Better-Auth resolves `/iam/./open-api/…` or `/iam/x/%2e%2e/open-api/…` to the very same route.
|
|
987
|
+
// A project that registers openAPI() itself keeps its routes.
|
|
988
|
+
if (!projectProvidesOpenApiPlugin(config)) {
|
|
989
|
+
plugins.push(serverOnlyPlugin(openAPI({ disableDefaultReference: true })));
|
|
990
|
+
}
|
|
991
|
+
|
|
981
992
|
// Merge custom plugins from configuration
|
|
982
993
|
// This allows projects to add any Better-Auth plugin without modifying this package
|
|
983
994
|
if (config.plugins?.length) {
|
|
@@ -987,6 +998,21 @@ function buildPlugins(
|
|
|
987
998
|
return plugins;
|
|
988
999
|
}
|
|
989
1000
|
|
|
1001
|
+
/**
|
|
1002
|
+
* Keeps every endpoint of a plugin off the HTTP router. better-call skips an endpoint whose metadata
|
|
1003
|
+
* carries `SERVER_ONLY` when it builds the router, whatever path a request names, while
|
|
1004
|
+
* `auth.api.<endpoint>()` still calls it in-process. Better-Auth's own generator leaves such endpoints
|
|
1005
|
+
* out of its schema too.
|
|
1006
|
+
*/
|
|
1007
|
+
function serverOnlyPlugin<T extends BetterAuthPlugin>(plugin: T): T {
|
|
1008
|
+
for (const endpoint of Object.values(plugin.endpoints ?? {}) as { options?: { metadata?: object } }[]) {
|
|
1009
|
+
if (endpoint?.options) {
|
|
1010
|
+
endpoint.options.metadata = { ...endpoint.options.metadata, SERVER_ONLY: true };
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
return plugin;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
990
1016
|
/**
|
|
991
1017
|
* Builds the social providers configuration object dynamically.
|
|
992
1018
|
* Iterates over all configured providers and includes those that are enabled
|
|
@@ -6,26 +6,12 @@ import { ConfigService } from '../../common/services/config.service';
|
|
|
6
6
|
import { CoreBetterAuthChallengeService } from './core-better-auth-challenge.service';
|
|
7
7
|
import { BetterAuthCookieHelper, createCookieHelper } from './core-better-auth-cookie.helper';
|
|
8
8
|
import { wrapBetterAuthErrorResponse } from './core-better-auth-error-codes.helper';
|
|
9
|
+
import { isControllerHandledPath } from './core-better-auth-openapi.helper';
|
|
9
10
|
import { runWithResetPassword } from './core-better-auth-password-reset.registry';
|
|
10
11
|
import { CoreBetterAuthUserMapper } from './core-better-auth-user.mapper';
|
|
11
12
|
import { extractSessionToken, sendWebResponse, signCookieValue, toWebRequest } from './core-better-auth-web.helper';
|
|
12
13
|
import { CoreBetterAuthService } from './core-better-auth.service';
|
|
13
14
|
|
|
14
|
-
/**
|
|
15
|
-
* List of paths that are handled by CoreBetterAuthController
|
|
16
|
-
* These should NOT be forwarded to Better Auth's native handler
|
|
17
|
-
*
|
|
18
|
-
* Only paths with nest-server-specific logic belong here:
|
|
19
|
-
* - sign-in/email: Legacy user migration, password normalization
|
|
20
|
-
* - sign-up/email: User linking to own DB, password sync
|
|
21
|
-
* - sign-out: Custom cookie clearing
|
|
22
|
-
* - session: Custom response format with mapped user
|
|
23
|
-
*
|
|
24
|
-
* All other paths (Passkey, 2FA, etc.) go directly to Better Auth's
|
|
25
|
-
* native handler via this middleware for maximum compatibility.
|
|
26
|
-
*/
|
|
27
|
-
const CONTROLLER_HANDLED_PATHS = ['/features', '/sign-in/email', '/sign-up/email', '/sign-out', '/session'];
|
|
28
|
-
|
|
29
15
|
/**
|
|
30
16
|
* Native Better-Auth routes that set a NEW password from a token or OTP, with the body
|
|
31
17
|
* field each of them carries it in.
|
|
@@ -227,8 +213,9 @@ export class CoreBetterAuthApiMiddleware implements NestMiddleware {
|
|
|
227
213
|
// Get the path relative to the base path
|
|
228
214
|
const relativePath = requestPath.slice(basePath.length);
|
|
229
215
|
|
|
230
|
-
// Skip paths that are handled by CoreBetterAuthController (nest-server-specific logic)
|
|
231
|
-
|
|
216
|
+
// Skip paths that are handled by CoreBetterAuthController (nest-server-specific logic).
|
|
217
|
+
// All other paths (Passkey, 2FA, etc.) go directly to Better Auth's native handler.
|
|
218
|
+
if (isControllerHandledPath(relativePath)) {
|
|
232
219
|
return next();
|
|
233
220
|
}
|
|
234
221
|
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import type { IBetterAuth } from '../../common/interfaces/server-options.interface';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* What the Swagger document needs to know about the routes under the Better-Auth base path.
|
|
5
|
+
*
|
|
6
|
+
* Two kinds of route live there. The few with nest-server-specific logic are handled by
|
|
7
|
+
* `CoreBetterAuthController` (`CONTROLLER_HANDLED_PATHS`); every other request is forwarded by
|
|
8
|
+
* `CoreBetterAuthApiMiddleware` to Better-Auth's own handler, which serves whatever the configured
|
|
9
|
+
* plugins add (two-factor, passkey, sessions, password change, …). Those forwarded routes have no
|
|
10
|
+
* Nest handler, so `@nestjs/swagger` cannot see them; `setupSwagger()` documents them from Better-Auth's
|
|
11
|
+
* OpenAPI generator instead (see `CoreBetterAuthService.getOpenApiSchema()`).
|
|
12
|
+
*
|
|
13
|
+
* This file imports types only, so the middleware, the plugin builder and the Swagger helper can all
|
|
14
|
+
* read the same rules without importing each other.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Paths (relative to the base path) handled by `CoreBetterAuthController` rather than forwarded.
|
|
19
|
+
*
|
|
20
|
+
* Only paths with nest-server-specific logic belong here:
|
|
21
|
+
* - sign-in/email: legacy user migration, password normalization
|
|
22
|
+
* - sign-up/email: user linking to own DB, password sync
|
|
23
|
+
* - sign-out: custom cookie clearing
|
|
24
|
+
* - session: custom response format with mapped user
|
|
25
|
+
* - features: nest-server's own feature report
|
|
26
|
+
*/
|
|
27
|
+
export const CONTROLLER_HANDLED_PATHS = ['/features', '/sign-in/email', '/sign-up/email', '/sign-out', '/session'];
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Routes Better-Auth guards with a session check in the HANDLER instead of a session middleware: the
|
|
31
|
+
* social-account routes resolve their caller through `resolveUserId()` (better-auth `api/routes/account`),
|
|
32
|
+
* which refuses an HTTP caller without a session. Every other route without a session middleware is
|
|
33
|
+
* public. Pinned against better-auth's source by tests/unit/better-auth-openapi-availability.spec.ts.
|
|
34
|
+
*/
|
|
35
|
+
export const HANDLER_SESSION_PATHS = ['/account-info', '/get-access-token', '/refresh-token'];
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The plugins `buildPlugins()` registers itself. Their routes — like Better-Auth's core routes — are
|
|
39
|
+
* classified by the session middleware they carry. Any other plugin's routes keep the global security
|
|
40
|
+
* requirement: such a plugin may check the session in a middleware of its own (`admin()` does), which
|
|
41
|
+
* nothing outside it can recognise, and a session route documented as public sends clients without one.
|
|
42
|
+
*/
|
|
43
|
+
export const BUILT_IN_BETTER_AUTH_PLUGIN_IDS = ['jwt', 'lt-ws-session-revocation', 'open-api', 'passkey', 'two-factor'];
|
|
44
|
+
|
|
45
|
+
/** Better-Auth's `:param` route syntax as OpenAPI's `{param}`, which its generator writes. */
|
|
46
|
+
export function toOpenApiPath(path: string): string {
|
|
47
|
+
return path.replace(/:([^/]+)/g, '{$1}');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Base of the routes Better-Auth's `openAPI()` plugin adds (`/open-api/generate-schema`). */
|
|
51
|
+
export const BETTER_AUTH_OPEN_API_PATH = '/open-api';
|
|
52
|
+
|
|
53
|
+
/** Id of Better-Auth's `openAPI()` plugin */
|
|
54
|
+
export const BETTER_AUTH_OPEN_API_PLUGIN_ID = 'open-api';
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Minimal shape of the document Better-Auth's OpenAPI generator returns (OpenAPI 3.1, paths relative to
|
|
58
|
+
* the base path).
|
|
59
|
+
*/
|
|
60
|
+
export interface IBetterAuthOpenApiDocument {
|
|
61
|
+
components?: { schemas?: Record<string, unknown> };
|
|
62
|
+
paths: Record<string, Record<string, unknown>>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Whether a path relative to the base path is handled by `CoreBetterAuthController`. */
|
|
66
|
+
export function isControllerHandledPath(relativePath: string): boolean {
|
|
67
|
+
return CONTROLLER_HANDLED_PATHS.some((path) => relativePath === path || relativePath.startsWith(`${path}/`));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Whether a path relative to the base path belongs to Better-Auth's `openAPI()` plugin. */
|
|
71
|
+
export function isBetterAuthOpenApiPath(relativePath: string): boolean {
|
|
72
|
+
return relativePath === BETTER_AUTH_OPEN_API_PATH || relativePath.startsWith(`${BETTER_AUTH_OPEN_API_PATH}/`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Whether the project registered `openAPI()` itself in `betterAuth.plugins`. Then its routes are the
|
|
77
|
+
* project's decision and stay reachable; otherwise nest-server registers the plugin only to feed the
|
|
78
|
+
* Swagger document, with its endpoints kept off the router (`serverOnlyPlugin()` in better-auth.config.ts).
|
|
79
|
+
*/
|
|
80
|
+
export function projectProvidesOpenApiPlugin(config: IBetterAuth | null | undefined): boolean {
|
|
81
|
+
return !!config?.plugins?.some(
|
|
82
|
+
(plugin) => (plugin as { id?: unknown } | null)?.id === BETTER_AUTH_OPEN_API_PLUGIN_ID,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The parts of a running Better-Auth instance (`await auth.$context`) that decide whether a route it
|
|
88
|
+
* serves can succeed. Typed loosely on purpose: these are Better-Auth's resolved options, including
|
|
89
|
+
* whatever a project passed through `betterAuth.options` or added as a plugin.
|
|
90
|
+
*/
|
|
91
|
+
export interface IBetterAuthRuntime {
|
|
92
|
+
options?: {
|
|
93
|
+
account?: { accountLinking?: { allowUnlinkingAll?: boolean } };
|
|
94
|
+
emailAndPassword?: { disableSignUp?: boolean; enabled?: boolean; sendResetPassword?: unknown };
|
|
95
|
+
emailVerification?: { sendVerificationEmail?: unknown };
|
|
96
|
+
plugins?: {
|
|
97
|
+
id?: string;
|
|
98
|
+
options?: Record<string, any>;
|
|
99
|
+
schema?: { session?: { fields?: Record<string, { input?: boolean }> } };
|
|
100
|
+
}[];
|
|
101
|
+
session?: { additionalFields?: Record<string, { input?: boolean }>; deferSessionRefresh?: boolean };
|
|
102
|
+
user?: { changeEmail?: { enabled?: boolean }; deleteUser?: { enabled?: boolean } };
|
|
103
|
+
};
|
|
104
|
+
socialProviders?: unknown[];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Operations Better-Auth serves but answers with an error for EVERY caller under the running
|
|
109
|
+
* configuration. Better-Auth's OpenAPI generator lists each registered endpoint regardless of the options
|
|
110
|
+
* it checks at request time, so `CoreBetterAuthService` removes these from the description it hands to
|
|
111
|
+
* `setupSwagger()`. Keys: `/path` (every method) or `<method> /path`.
|
|
112
|
+
*
|
|
113
|
+
* Each row mirrors a check in better-auth's own source (1.7.7, `dist/…`), pinned by
|
|
114
|
+
* `tests/unit/better-auth-openapi-availability.spec.ts`:
|
|
115
|
+
*
|
|
116
|
+
* | Unavailable | Unless | Better-Auth answers |
|
|
117
|
+
* |---|---|---|
|
|
118
|
+
* | `post /get-session` | `session.deferSessionRefresh` | 405 `METHOD_NOT_ALLOWED_DEFER_SESSION_REQUIRED` (api/routes/session.mjs) |
|
|
119
|
+
* | update-session | a session field accepts input (`session.additionalFields` or a plugin's session schema) | "No fields to update" (update-session.mjs) |
|
|
120
|
+
* | social sign-in, callback, link, provider tokens | a social provider is configured | `PROVIDER_NOT_FOUND` / "Provider … is not supported" (sign-in, callback, account.mjs) |
|
|
121
|
+
* | unlink-account | a social provider, or `account.accountLinking.allowUnlinkingAll` | `FAILED_TO_UNLINK_LAST_ACCOUNT` — without a provider a user has one account (account.mjs) |
|
|
122
|
+
* | send-verification-email, verify-email | `emailVerification.sendVerificationEmail` | "Verification email isn't enabled" (email-verification.mjs) |
|
|
123
|
+
* | request + reset password | `emailAndPassword.sendResetPassword` | `RESET_PASSWORD_DISABLED` (password.mjs) |
|
|
124
|
+
* | sign-in + sign-up with email | `emailAndPassword.enabled` | `EMAIL_PASSWORD_DISABLED` (sign-in.mjs, sign-up.mjs). Not change/verify password: Better-Auth does not check `enabled` there |
|
|
125
|
+
* | `/sign-up/email` | not `emailAndPassword.disableSignUp` | "Email and password sign up is not enabled" (sign-up.mjs) |
|
|
126
|
+
* | delete-user (+ callback) | `user.deleteUser.enabled` | error (update-user.mjs) |
|
|
127
|
+
* | change-email | `user.changeEmail.enabled` | `CHANGE_EMAIL_DISABLED` (update-user.mjs) |
|
|
128
|
+
* | two-factor OTP send/verify | the two-factor plugin's `otpOptions.sendOTP` | `OTP_NOT_CONFIGURED` (plugins/two-factor) |
|
|
129
|
+
* | two-factor TOTP uri/verify | not the two-factor plugin's `totpOptions.disable` | `TOTP_NOT_CONFIGURED` (plugins/two-factor) |
|
|
130
|
+
*
|
|
131
|
+
* Add a row when an upgrade of better-auth introduces a new option-dependent refusal.
|
|
132
|
+
*/
|
|
133
|
+
export function unavailableBetterAuthOperations(runtime: IBetterAuthRuntime | null | undefined): Set<string> {
|
|
134
|
+
const options = runtime?.options ?? {};
|
|
135
|
+
const unavailable = new Set<string>();
|
|
136
|
+
const add = (...keys: string[]) => keys.forEach((key) => unavailable.add(key));
|
|
137
|
+
|
|
138
|
+
if (!options.session?.deferSessionRefresh) {
|
|
139
|
+
add('post /get-session');
|
|
140
|
+
}
|
|
141
|
+
const acceptsInput = (fields: Record<string, { input?: boolean }> | undefined) =>
|
|
142
|
+
Object.values(fields ?? {}).some((field) => field?.input !== false);
|
|
143
|
+
if (
|
|
144
|
+
!acceptsInput(options.session?.additionalFields) &&
|
|
145
|
+
!options.plugins?.some((plugin) => acceptsInput(plugin?.schema?.session?.fields))
|
|
146
|
+
) {
|
|
147
|
+
add('/update-session');
|
|
148
|
+
}
|
|
149
|
+
if (!runtime?.socialProviders?.length) {
|
|
150
|
+
add('/sign-in/social', '/callback/{id}', '/link-social', '/refresh-token', '/get-access-token', '/account-info');
|
|
151
|
+
if (!options.account?.accountLinking?.allowUnlinkingAll) {
|
|
152
|
+
add('/unlink-account');
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (!options.emailVerification?.sendVerificationEmail) {
|
|
156
|
+
add('/send-verification-email', '/verify-email');
|
|
157
|
+
}
|
|
158
|
+
if (!options.emailAndPassword?.sendResetPassword) {
|
|
159
|
+
add('/request-password-reset', '/reset-password', '/reset-password/{token}');
|
|
160
|
+
}
|
|
161
|
+
if (!options.emailAndPassword?.enabled) {
|
|
162
|
+
add('/sign-in/email', '/sign-up/email');
|
|
163
|
+
}
|
|
164
|
+
if (options.emailAndPassword?.disableSignUp) {
|
|
165
|
+
add('/sign-up/email');
|
|
166
|
+
}
|
|
167
|
+
if (!options.user?.deleteUser?.enabled) {
|
|
168
|
+
add('/delete-user', '/delete-user/callback');
|
|
169
|
+
}
|
|
170
|
+
if (!options.user?.changeEmail?.enabled) {
|
|
171
|
+
add('/change-email');
|
|
172
|
+
}
|
|
173
|
+
const twoFactor = options.plugins?.find((plugin) => plugin?.id === 'two-factor');
|
|
174
|
+
if (twoFactor) {
|
|
175
|
+
if (!twoFactor.options?.otpOptions?.sendOTP) {
|
|
176
|
+
add('/two-factor/send-otp', '/two-factor/verify-otp');
|
|
177
|
+
}
|
|
178
|
+
if (twoFactor.options?.totpOptions?.disable) {
|
|
179
|
+
add('/two-factor/get-totp-uri', '/two-factor/verify-totp');
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return unavailable;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Whether an operation is listed by `unavailableBetterAuthOperations()` (or `switchedOffBetterAuthPaths()`). */
|
|
186
|
+
export function isUnavailableOperation(unavailable: Set<string>, method: string, relativePath: string): boolean {
|
|
187
|
+
return unavailable.has(relativePath) || unavailable.has(`${method.toLowerCase()} ${relativePath}`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Paths (relative to the base path) the configuration switches off for every caller. Documented, they
|
|
192
|
+
* could only ever answer an error, so the Swagger document leaves them out — explicit controller routes
|
|
193
|
+
* and generated ones alike. Each rule mirrors the switch it reads:
|
|
194
|
+
*
|
|
195
|
+
* | Configuration | Paths | Why they cannot succeed |
|
|
196
|
+
* |---|---|---|
|
|
197
|
+
* | `emailAndPassword.enabled: false` | sign-in + sign-up with email | `EMAIL_PASSWORD_DISABLED` (the only routes Better-Auth checks it on) |
|
|
198
|
+
* | `emailAndPassword.disableSignUp: true` | `/sign-up/email` | `SIGNUP_DISABLED` (`isSignUpEnabled()`) |
|
|
199
|
+
* | `emailAndPassword.passwordReset: false` | request + reset | no reset hook, so no token can be minted |
|
|
200
|
+
*/
|
|
201
|
+
export function switchedOffBetterAuthPaths(config: IBetterAuth | null | undefined): Set<string> {
|
|
202
|
+
const emailAndPassword = config?.emailAndPassword;
|
|
203
|
+
const paths = new Set<string>();
|
|
204
|
+
if (emailAndPassword?.enabled === false) {
|
|
205
|
+
// Better-Auth checks `enabled` in sign-in and sign-up only; password change/verify/reset keep
|
|
206
|
+
// working for accounts that have a password.
|
|
207
|
+
paths.add('/sign-in/email');
|
|
208
|
+
paths.add('/sign-up/email');
|
|
209
|
+
}
|
|
210
|
+
if (emailAndPassword?.disableSignUp === true) {
|
|
211
|
+
paths.add('/sign-up/email');
|
|
212
|
+
}
|
|
213
|
+
if (emailAndPassword?.passwordReset === false) {
|
|
214
|
+
for (const path of ['/request-password-reset', '/reset-password', '/reset-password/{token}']) {
|
|
215
|
+
paths.add(path);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return paths;
|
|
219
|
+
}
|