@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,11 +1,29 @@
|
|
|
1
1
|
import { Logger } from '@nestjs/common';
|
|
2
|
+
import { PATH_METADATA } from '@nestjs/common/constants';
|
|
2
3
|
import { ModulesContainer } from '@nestjs/core';
|
|
4
|
+
import { SchedulerRegistry } from '@nestjs/schedule';
|
|
3
5
|
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
|
4
6
|
|
|
7
|
+
import { getMountedMcpOAuth } from '../../modules/ai/helpers/ai-mcp-oauth.registry';
|
|
5
8
|
import { resolveApiTokenConfig } from '../../modules/api-token/core-api-token.helpers';
|
|
6
9
|
import { API_TOKEN_SCOPES_KEY } from '../../modules/api-token/core-api-token.constants';
|
|
10
|
+
import { CoreAuthController } from '../../modules/auth/core-auth.controller';
|
|
11
|
+
import { isLegacyEndpointEnabled } from '../../modules/auth/helpers/legacy-endpoints.helper';
|
|
7
12
|
import { resolveBetterAuthSessionCookieName } from '../../modules/better-auth/better-auth-cookie-prefix.helper';
|
|
13
|
+
import {
|
|
14
|
+
isBetterAuthOpenApiPath,
|
|
15
|
+
isControllerHandledPath,
|
|
16
|
+
isUnavailableOperation,
|
|
17
|
+
switchedOffBetterAuthPaths,
|
|
18
|
+
} from '../../modules/better-auth/core-better-auth-openapi.helper';
|
|
19
|
+
import { CoreBetterAuthUserMapper } from '../../modules/better-auth/core-better-auth-user.mapper';
|
|
20
|
+
import { CoreBetterAuthController } from '../../modules/better-auth/core-better-auth.controller';
|
|
21
|
+
import { CoreBetterAuthService } from '../../modules/better-auth/core-better-auth.service';
|
|
22
|
+
import { CoreHubActionsController } from '../../modules/hub/core-hub-actions.controller';
|
|
23
|
+
import { CoreHubController } from '../../modules/hub/core-hub.controller';
|
|
24
|
+
import { isHubEnabled, normalizeHubConfig } from '../../modules/hub/hub-config.helper';
|
|
8
25
|
import { SKIP_TENANT_CHECK_KEY } from '../../modules/tenant/core-tenant.decorators';
|
|
26
|
+
import { API_METHODS_KEY } from '../decorators/api-methods.decorator';
|
|
9
27
|
import { mergeRolesMetadata } from '../../modules/tenant/core-tenant.helpers';
|
|
10
28
|
import { RoleEnum } from '../enums/role.enum';
|
|
11
29
|
import { ConfigService } from '../services/config.service';
|
|
@@ -20,6 +38,9 @@ import type {
|
|
|
20
38
|
ReferenceObject,
|
|
21
39
|
TagObject,
|
|
22
40
|
} from '@nestjs/swagger';
|
|
41
|
+
import type { IMountedMcpOAuth } from '../../modules/ai/helpers/ai-mcp-oauth.registry';
|
|
42
|
+
import type { IBetterAuthOpenApiDocument } from '../../modules/better-auth/core-better-auth-openapi.helper';
|
|
43
|
+
import type { ResolvedHubConfig } from '../../modules/hub/interfaces/hub-config.interface';
|
|
23
44
|
import type { IServerOptions } from '../interfaces/server-options.interface';
|
|
24
45
|
import type { ISwaggerApiTokenViewOptions, ISwaggerSetupOptions } from '../interfaces/swagger-setup-options.interface';
|
|
25
46
|
|
|
@@ -37,6 +58,15 @@ import type { ISwaggerApiTokenViewOptions, ISwaggerSetupOptions } from '../inter
|
|
|
37
58
|
* (`x-api-token-scopes` plus a line in the description). `@ApiTokenScopes()` on the method replaces
|
|
38
59
|
* the class-level declaration, exactly as in `assertApiTokenScopes()`.
|
|
39
60
|
* - With multi-tenancy on, every operation that reads the tenant header documents it as a parameter.
|
|
61
|
+
* - The routes Better-Auth serves itself (two-factor, passkey, sessions, password change, … — whatever the
|
|
62
|
+
* configured plugins add) are documented from Better-Auth's own OpenAPI generator, under the project's
|
|
63
|
+
* Better-Auth controller (since 11.42.9); so are `/ai/mcp` and the MCP OAuth endpoints
|
|
64
|
+
* `mountAiMcpOAuth()` mounted.
|
|
65
|
+
* - Only what can succeed under the running configuration is documented (since 11.42.9): an `@All()`
|
|
66
|
+
* handler keeps the methods `@ApiMethods()` names; legacy `/auth/*` disappears while
|
|
67
|
+
* `auth.legacyEndpoints` keeps REST closed; Better-Auth routes disappear when the configuration or the
|
|
68
|
+
* live instance refuses them; Hub endpoints disappear with their switched-off panel. Every rule reads
|
|
69
|
+
* the switch the handler itself reads — project modules are documented by the same scan, automatically.
|
|
40
70
|
* - Optionally, a second document lists only the routes an API token may call.
|
|
41
71
|
*
|
|
42
72
|
* Each operation is mapped to its handler through the default operation id (`<Controller>_<method>`),
|
|
@@ -77,6 +107,25 @@ interface IOperationAccess {
|
|
|
77
107
|
skipsTenantHeader: boolean;
|
|
78
108
|
}
|
|
79
109
|
|
|
110
|
+
/** What the document needs to know about the Better-Auth routes of the application. */
|
|
111
|
+
interface IBetterAuthRoutes {
|
|
112
|
+
/** Base path every Better-Auth route lives under (`/iam`) */
|
|
113
|
+
basePath: string;
|
|
114
|
+
/** The project's Better-Auth controller, under which the generated routes are documented */
|
|
115
|
+
controller: Type<unknown>;
|
|
116
|
+
/** The controller's routes are not where its metadata puts them (global prefix, URI versioning) */
|
|
117
|
+
relocated?: boolean;
|
|
118
|
+
/** Better-Auth's own description of the routes it serves itself */
|
|
119
|
+
schema?: IBetterAuthOpenApiDocument;
|
|
120
|
+
/**
|
|
121
|
+
* Operations (relative to the base path) that answer an error for every caller: what the configuration
|
|
122
|
+
* switches off plus what the live Better-Auth instance refuses (`isUnavailableOperation()` keys)
|
|
123
|
+
*/
|
|
124
|
+
switchedOff: Set<string>;
|
|
125
|
+
/** Tags of the controller's explicit routes, as Swagger built them (before `operationTags`) */
|
|
126
|
+
tags?: string[];
|
|
127
|
+
}
|
|
128
|
+
|
|
80
129
|
interface IResolvedApiTokenView {
|
|
81
130
|
description?: string;
|
|
82
131
|
enabled: boolean;
|
|
@@ -179,15 +228,36 @@ export function buildSwaggerDocument(
|
|
|
179
228
|
}
|
|
180
229
|
|
|
181
230
|
const handlers = indexHandlers(app);
|
|
231
|
+
const betterAuthRoutes = resolveBetterAuthRoutes(app, handlers, config);
|
|
232
|
+
const availability = resolveAvailability(app, config, betterAuthRoutes);
|
|
233
|
+
const switchedOff: [string, string][] = [];
|
|
182
234
|
forEachOperation(document, (path, method, operation) => {
|
|
183
235
|
const reference = resolveHandler(operation.operationId, handlers);
|
|
184
236
|
if (!reference) {
|
|
185
237
|
logger.warn(`No handler found for operation ${operation.operationId ?? `${method.toUpperCase()} ${path}`}`);
|
|
186
238
|
return;
|
|
187
239
|
}
|
|
240
|
+
if (betterAuthRoutes && isBetterAuthController(reference.controller)) {
|
|
241
|
+
betterAuthRoutes.tags ??= [...(operation.tags ?? [])];
|
|
242
|
+
// A global prefix or URI versioning puts the controller's routes below another base than its
|
|
243
|
+
// metadata says — and then Better-Auth's own routes are not reachable at all (see
|
|
244
|
+
// addBetterAuthRoutes). Compared by the controller's base only: a handler path is written in Nest's
|
|
245
|
+
// syntax (`:id`, `*path`), the document's in OpenAPI's (`{id}`), so a full comparison would read
|
|
246
|
+
// every parametrized route as relocated.
|
|
247
|
+
const base = declaredControllerBase(reference.controller);
|
|
248
|
+
if (base && path !== base && !path.startsWith(`${base}/`)) {
|
|
249
|
+
betterAuthRoutes.relocated = true;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (isSwitchedOffOperation(reference, method, availability)) {
|
|
253
|
+
switchedOff.push([path, method]);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
188
256
|
const access = describeAccess(reference, multiTenancy);
|
|
189
257
|
|
|
190
|
-
|
|
258
|
+
// An explicit @ApiBearerAuth()/@ApiSecurity() wins over "public": a route the guards admit to
|
|
259
|
+
// everybody may still authenticate the caller itself (the MCP endpoint does).
|
|
260
|
+
if (access.isPublic && !declaresSecurity(reference)) {
|
|
191
261
|
operation.security = [];
|
|
192
262
|
}
|
|
193
263
|
if (apiTokens.enabled) {
|
|
@@ -218,10 +288,393 @@ export function buildSwaggerDocument(
|
|
|
218
288
|
}
|
|
219
289
|
});
|
|
220
290
|
|
|
291
|
+
for (const [path, method] of switchedOff) {
|
|
292
|
+
const item = document.paths[path] as Record<string, unknown>;
|
|
293
|
+
delete item[method];
|
|
294
|
+
if (!operationsOf(document.paths[path]).length) {
|
|
295
|
+
delete document.paths[path];
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const added = { apiTokensEnabled: apiTokens.enabled, operationTags: options.operationTags };
|
|
300
|
+
if (betterAuthRoutes?.schema) {
|
|
301
|
+
if (betterAuthRoutes.relocated) {
|
|
302
|
+
logger.warn(
|
|
303
|
+
"Better-Auth's own routes are not documented: under a global prefix or URI versioning the API " +
|
|
304
|
+
'middleware does not forward them, so none of them would be reachable at the documented path.',
|
|
305
|
+
);
|
|
306
|
+
} else {
|
|
307
|
+
addBetterAuthRoutes(document, betterAuthRoutes, added);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
const mcpOAuth = getMountedMcpOAuth();
|
|
311
|
+
if (mcpOAuth) {
|
|
312
|
+
addMcpOAuthRoutes(document, mcpOAuth, added);
|
|
313
|
+
}
|
|
314
|
+
|
|
221
315
|
document.tags = orderTags(document, options.tags);
|
|
222
316
|
return document;
|
|
223
317
|
}
|
|
224
318
|
|
|
319
|
+
/** What decides whether a registered route can succeed under the running configuration. */
|
|
320
|
+
interface IAvailability {
|
|
321
|
+
/** AI module registered (the Hub's AI panel reads it) */
|
|
322
|
+
ai: boolean;
|
|
323
|
+
betterAuth?: IBetterAuthRoutes;
|
|
324
|
+
/** Better-Auth's user mapper registered (the Hub's auth-migration panel reads it) */
|
|
325
|
+
betterAuthMapper: boolean;
|
|
326
|
+
config: Partial<IServerOptions>;
|
|
327
|
+
/** The Hub's resolved configuration, when the Hub is enabled */
|
|
328
|
+
hub?: ResolvedHubConfig;
|
|
329
|
+
/** `ScheduleModule` registered (the Hub's cron panel and actions read it) */
|
|
330
|
+
scheduler: boolean;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** Legacy-auth REST handlers that answer 410 while `auth.legacyEndpoints` keeps REST closed */
|
|
334
|
+
const LEGACY_REST_HANDLERS = new Set(['logout', 'refreshToken', 'signIn', 'signUp']);
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Hub endpoints that answer `{ available: false }` (or refuse) unless their source is present — keyed by
|
|
338
|
+
* handler name, read the way the Hub's own services decide it.
|
|
339
|
+
*/
|
|
340
|
+
const HUB_AVAILABILITY: Record<string, (availability: IAvailability, hub: ResolvedHubConfig) => boolean> = {
|
|
341
|
+
aiJson: (availability) => availability.ai,
|
|
342
|
+
authMigrationJson: (availability) => availability.betterAuthMapper,
|
|
343
|
+
cron: (availability) => availability.scheduler,
|
|
344
|
+
cronJson: (availability) => availability.scheduler,
|
|
345
|
+
dbJson: (_availability, hub) => hub.db !== false,
|
|
346
|
+
emailPreview: (_availability, hub) => hub.emailPreview,
|
|
347
|
+
emailsJson: (_availability, hub) => hub.emailPreview,
|
|
348
|
+
logsJson: (_availability, hub) => hub.collectors.logs !== false,
|
|
349
|
+
mailboxHtml: (_availability, hub) => hub.mailbox !== false,
|
|
350
|
+
mailboxJson: (_availability, hub) => hub.mailbox !== false,
|
|
351
|
+
migrationsDown: (_availability, hub) => hub.migrations !== false,
|
|
352
|
+
migrationsJson: (_availability, hub) => hub.migrations !== false,
|
|
353
|
+
migrationsRun: (_availability, hub) => hub.migrations !== false,
|
|
354
|
+
queriesJson: (_availability, hub) => hub.collectors.queries !== false,
|
|
355
|
+
tracesJson: (_availability, hub) => hub.collectors.traces !== false,
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
function resolveAvailability(
|
|
359
|
+
app: INestApplication,
|
|
360
|
+
config: Partial<IServerOptions>,
|
|
361
|
+
betterAuth: IBetterAuthRoutes | undefined,
|
|
362
|
+
): IAvailability {
|
|
363
|
+
const ai = config.ai;
|
|
364
|
+
return {
|
|
365
|
+
ai: ai === true || (typeof ai === 'object' && ai !== null && (ai as { enabled?: boolean }).enabled !== false),
|
|
366
|
+
betterAuth,
|
|
367
|
+
betterAuthMapper: isProvided(app, CoreBetterAuthUserMapper),
|
|
368
|
+
config,
|
|
369
|
+
hub: isHubEnabled(config.hub) ? normalizeHubConfig(config.hub!, { env: config.env ?? '', version: '' }) : undefined,
|
|
370
|
+
scheduler: isProvided(app, SchedulerRegistry),
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/** Whether a provider is registered anywhere in the application */
|
|
375
|
+
function isProvided(app: INestApplication, token: Type<unknown>): boolean {
|
|
376
|
+
try {
|
|
377
|
+
return !!app.get(token, { strict: false });
|
|
378
|
+
} catch {
|
|
379
|
+
return false;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Whether an operation of a registered handler answers an error for every caller under the running
|
|
385
|
+
* configuration — documented, it could never succeed. Each rule reads the switch the handler itself reads.
|
|
386
|
+
*/
|
|
387
|
+
function isSwitchedOffOperation(reference: IHandlerReference, method: string, availability: IAvailability): boolean {
|
|
388
|
+
const methods = Reflect.getMetadata(API_METHODS_KEY, reference.handler) as string[] | undefined;
|
|
389
|
+
if (methods && !methods.includes(method)) {
|
|
390
|
+
return true;
|
|
391
|
+
}
|
|
392
|
+
const { controller, handlerName } = reference;
|
|
393
|
+
if (extendsClass(controller, CoreAuthController) && LEGACY_REST_HANDLERS.has(handlerName)) {
|
|
394
|
+
return !isLegacyEndpointEnabled(availability.config.auth?.legacyEndpoints, 'rest');
|
|
395
|
+
}
|
|
396
|
+
if (availability.betterAuth && isBetterAuthController(controller)) {
|
|
397
|
+
const relativePath = declaredHandlerPath(reference.handler);
|
|
398
|
+
return (
|
|
399
|
+
relativePath !== undefined && isUnavailableOperation(availability.betterAuth.switchedOff, method, relativePath)
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
if (
|
|
403
|
+
availability.hub &&
|
|
404
|
+
(extendsClass(controller, CoreHubController) || extendsClass(controller, CoreHubActionsController)) &&
|
|
405
|
+
HUB_AVAILABILITY[handlerName]
|
|
406
|
+
) {
|
|
407
|
+
return !HUB_AVAILABILITY[handlerName](availability, availability.hub);
|
|
408
|
+
}
|
|
409
|
+
return false;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** `base` itself or a class extending it */
|
|
413
|
+
function extendsClass(controller: Type<unknown>, base: Type<unknown>): boolean {
|
|
414
|
+
return controller === base || controller.prototype instanceof base;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/** The handler's own route path (`sign-up/email` → `/sign-up/email`), or `undefined` for several paths */
|
|
418
|
+
function declaredHandlerPath(handler: IHandlerReference['handler']): string | undefined {
|
|
419
|
+
const path = Reflect.getMetadata(PATH_METADATA, handler) as string | string[] | undefined;
|
|
420
|
+
return typeof path === 'string' ? `/${path.replace(/^\/+/, '')}`.replace(/\/$/, '') || '/' : undefined;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/** The path controller + handler metadata declare, without global prefix or version */
|
|
424
|
+
/** The path a controller declares (`/iam`), or `undefined` when it declares none or several. */
|
|
425
|
+
function declaredControllerBase(controller: IHandlerReference['controller']): string | undefined {
|
|
426
|
+
const controllerPath = Reflect.getMetadata(PATH_METADATA, controller) as string | string[] | undefined;
|
|
427
|
+
const trimmed = typeof controllerPath === 'string' ? controllerPath.replace(/^\/+|\/+$/g, '') : '';
|
|
428
|
+
return trimmed ? `/${trimmed}` : undefined;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/** Whether the handler or its controller declares its security itself (`@ApiBearerAuth()`, `@ApiSecurity()`) */
|
|
432
|
+
function declaresSecurity(reference: IHandlerReference): boolean {
|
|
433
|
+
return !!(
|
|
434
|
+
Reflect.getMetadata('swagger/apiSecurity', reference.handler) ??
|
|
435
|
+
Reflect.getMetadata('swagger/apiSecurity', reference.controller)
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Documents the MCP OAuth 2.1 endpoints `mountAiMcpOAuth()` mounted on Express — the ones it mounted,
|
|
441
|
+
* nothing else. They authenticate the client themselves, so they carry no security requirement.
|
|
442
|
+
*/
|
|
443
|
+
function addMcpOAuthRoutes(
|
|
444
|
+
document: OpenAPIObject,
|
|
445
|
+
mounted: IMountedMcpOAuth,
|
|
446
|
+
context: { apiTokensEnabled: boolean; operationTags: ISwaggerSetupOptions['operationTags'] },
|
|
447
|
+
): void {
|
|
448
|
+
const routes: [string, string, string, string][] = [
|
|
449
|
+
[
|
|
450
|
+
'get',
|
|
451
|
+
'/.well-known/oauth-authorization-server',
|
|
452
|
+
'authorizationServerMetadata',
|
|
453
|
+
'OAuth 2.1 authorization server metadata (RFC 8414) for MCP clients.',
|
|
454
|
+
],
|
|
455
|
+
[
|
|
456
|
+
'get',
|
|
457
|
+
`/.well-known/oauth-protected-resource${mounted.mcpPath}`,
|
|
458
|
+
'protectedResourceMetadata',
|
|
459
|
+
`Protected resource metadata (RFC 9728) of the MCP endpoint \`${mounted.mcpPath}\`.`,
|
|
460
|
+
],
|
|
461
|
+
['get', '/authorize', 'authorize', 'Authorization endpoint (authorization code with PKCE S256).'],
|
|
462
|
+
['post', '/authorize', 'authorizePost', 'Authorization endpoint, form post variant.'],
|
|
463
|
+
['post', '/token', 'token', 'Token endpoint: exchanges an authorization code or refresh token.'],
|
|
464
|
+
];
|
|
465
|
+
if (mounted.registration) {
|
|
466
|
+
routes.push(['post', '/register', 'register', 'Dynamic client registration (RFC 7591).']);
|
|
467
|
+
}
|
|
468
|
+
if (mounted.revocation) {
|
|
469
|
+
routes.push(['post', '/revoke', 'revoke', 'Token revocation (RFC 7009).']);
|
|
470
|
+
}
|
|
471
|
+
for (const [method, path, name, description] of routes) {
|
|
472
|
+
const operation: EnrichedOperation = {
|
|
473
|
+
description: context.apiTokensEnabled ? appendLine(description, apiTokenLine(undefined)) : description,
|
|
474
|
+
operationId: `McpOAuth_${name}`,
|
|
475
|
+
responses: { default: { description: 'See the MCP authorization specification' } },
|
|
476
|
+
security: [],
|
|
477
|
+
summary: description.split(':')[0].replace(/\.$/, ''),
|
|
478
|
+
tags: ['MCP OAuth'],
|
|
479
|
+
};
|
|
480
|
+
if (context.operationTags) {
|
|
481
|
+
const tags = context.operationTags({
|
|
482
|
+
controller: 'McpOAuth',
|
|
483
|
+
handler: name,
|
|
484
|
+
isPublic: true,
|
|
485
|
+
method,
|
|
486
|
+
path,
|
|
487
|
+
scopes: undefined,
|
|
488
|
+
tags: [...(operation.tags ?? [])],
|
|
489
|
+
});
|
|
490
|
+
if (tags) {
|
|
491
|
+
operation.tags = tags;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
document.paths[path] = { ...document.paths[path], [method]: operation };
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/** `CoreBetterAuthController` or a project controller extending it */
|
|
499
|
+
function isBetterAuthController(controller: Type<unknown>): boolean {
|
|
500
|
+
return controller === CoreBetterAuthController || controller.prototype instanceof CoreBetterAuthController;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* The Better-Auth routes of the application, or `undefined` when no Better-Auth controller is registered.
|
|
505
|
+
* The base path and Better-Auth's own route description come from `CoreBetterAuthService`; what the
|
|
506
|
+
* configuration switches off is read from `config`, like everything else this helper documents.
|
|
507
|
+
*/
|
|
508
|
+
function resolveBetterAuthRoutes(
|
|
509
|
+
app: INestApplication,
|
|
510
|
+
handlers: Map<string, Type<unknown>[]>,
|
|
511
|
+
config: Partial<IServerOptions>,
|
|
512
|
+
): IBetterAuthRoutes | undefined {
|
|
513
|
+
const controller = [...handlers.values()].flat().find(isBetterAuthController);
|
|
514
|
+
if (!controller) {
|
|
515
|
+
return undefined;
|
|
516
|
+
}
|
|
517
|
+
let service: CoreBetterAuthService | undefined;
|
|
518
|
+
try {
|
|
519
|
+
service = app.get(CoreBetterAuthService, { strict: false });
|
|
520
|
+
} catch {
|
|
521
|
+
service = undefined;
|
|
522
|
+
}
|
|
523
|
+
const betterAuth = typeof config.betterAuth === 'object' && config.betterAuth !== null ? config.betterAuth : {};
|
|
524
|
+
return {
|
|
525
|
+
basePath: service?.getBasePath?.() || betterAuth.basePath || '/iam',
|
|
526
|
+
controller,
|
|
527
|
+
schema: service?.getOpenApiSchema?.(),
|
|
528
|
+
switchedOff: new Set([...switchedOffBetterAuthPaths(betterAuth), ...(service?.getUnavailableOperations?.() ?? [])]),
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Adds the routes Better-Auth serves itself, documented like the routes of the project's Better-Auth
|
|
534
|
+
* controller: its tags and `operationTags`, the global security requirement, the API-token line. Leaves
|
|
535
|
+
* out what the controller handles (an explicit route always wins), what the configuration switches off,
|
|
536
|
+
* and the `openAPI()` plugin's own routes. Their schemas are added with a `BetterAuth` prefix — `User`
|
|
537
|
+
* and `Session` would otherwise collide with the project's models — and converted to OpenAPI 3.0.
|
|
538
|
+
*/
|
|
539
|
+
function addBetterAuthRoutes(
|
|
540
|
+
document: OpenAPIObject,
|
|
541
|
+
routes: IBetterAuthRoutes,
|
|
542
|
+
context: { apiTokensEnabled: boolean; operationTags: ISwaggerSetupOptions['operationTags'] },
|
|
543
|
+
): void {
|
|
544
|
+
const renamed = new Map<string, string>();
|
|
545
|
+
const controllerTags = routes.tags ?? defaultControllerTags(routes.controller);
|
|
546
|
+
for (const [relativePath, item] of Object.entries(routes.schema?.paths ?? {})) {
|
|
547
|
+
if (isControllerHandledPath(relativePath) || isBetterAuthOpenApiPath(relativePath)) {
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
const path = `${routes.basePath}${relativePath}`;
|
|
551
|
+
for (const [method, source] of operationsOf(item as PathItemObject)) {
|
|
552
|
+
const existing = document.paths[path] as Record<string, unknown> | undefined;
|
|
553
|
+
if (existing?.[method] || isUnavailableOperation(routes.switchedOff, method, relativePath)) {
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
const operation = toOpenApi30(renameSchemaRefs(structuredClone(source), renamed)) as EnrichedOperation;
|
|
557
|
+
const betterAuthId = operation.operationId ?? `${method}${relativePath}`;
|
|
558
|
+
operation.operationId = `${routes.controller.name}_betterAuth_${betterAuthId}`;
|
|
559
|
+
// Better-Auth's generator marks every route bearer-only, public ones included.
|
|
560
|
+
// CoreBetterAuthService.prepareOpenApiSchema() replaces that with what Better-Auth enforces:
|
|
561
|
+
// `security: []` on a public route. Anything else gets the global requirement, so "Authorize"
|
|
562
|
+
// sends the session along — the safe reading of a description that did not say.
|
|
563
|
+
const isPublic = Array.isArray(operation.security) && operation.security.length === 0;
|
|
564
|
+
if (!isPublic) {
|
|
565
|
+
delete operation.security;
|
|
566
|
+
}
|
|
567
|
+
operation.tags = [...controllerTags];
|
|
568
|
+
if (context.apiTokensEnabled) {
|
|
569
|
+
// No route here releases a scope via @ApiTokenScopes(), so an API token is refused on all of them.
|
|
570
|
+
operation.description = appendLine(operation.description, apiTokenLine(undefined));
|
|
571
|
+
}
|
|
572
|
+
if (context.operationTags) {
|
|
573
|
+
const tags = context.operationTags({
|
|
574
|
+
controller: routes.controller.name,
|
|
575
|
+
handler: betterAuthId,
|
|
576
|
+
isPublic,
|
|
577
|
+
method,
|
|
578
|
+
path,
|
|
579
|
+
scopes: undefined,
|
|
580
|
+
tags: [...operation.tags],
|
|
581
|
+
});
|
|
582
|
+
if (tags) {
|
|
583
|
+
operation.tags = tags;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
document.paths[path] = { ...document.paths[path], [method]: operation };
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
addRenamedSchemas(document, routes.schema?.components?.schemas ?? {}, renamed);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/** Tags `@ApiTags()` gives a controller, or the one `autoTagControllers` derives from its name */
|
|
593
|
+
function defaultControllerTags(controller: Type<unknown>): string[] {
|
|
594
|
+
const tags = Reflect.getMetadata('swagger/apiUseTags', controller) as string[] | undefined;
|
|
595
|
+
return tags?.length ? [...tags] : [controller.name.replace(/Controller$/, '')];
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/** Rewrites `#/components/schemas/<Name>` to `BetterAuth<Name>`, recording each rename. */
|
|
599
|
+
function renameSchemaRefs<T>(value: T, renamed: Map<string, string>): T {
|
|
600
|
+
const prefix = '#/components/schemas/';
|
|
601
|
+
const visit = (node: unknown): void => {
|
|
602
|
+
if (Array.isArray(node)) {
|
|
603
|
+
node.forEach(visit);
|
|
604
|
+
} else if (typeof node === 'object' && node !== null) {
|
|
605
|
+
const record = node as Record<string, unknown>;
|
|
606
|
+
for (const [key, child] of Object.entries(record)) {
|
|
607
|
+
if (key === '$ref' && typeof child === 'string' && child.startsWith(prefix)) {
|
|
608
|
+
const name = child.slice(prefix.length);
|
|
609
|
+
const target = renamed.get(name) ?? `BetterAuth${name}`;
|
|
610
|
+
renamed.set(name, target);
|
|
611
|
+
record.$ref = `${prefix}${target}`;
|
|
612
|
+
} else {
|
|
613
|
+
visit(child);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
};
|
|
618
|
+
visit(value);
|
|
619
|
+
return value;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/** Copies every renamed schema — and the schemas those refer to — into the document. */
|
|
623
|
+
function addRenamedSchemas(
|
|
624
|
+
document: OpenAPIObject,
|
|
625
|
+
schemas: Record<string, unknown>,
|
|
626
|
+
renamed: Map<string, string>,
|
|
627
|
+
): void {
|
|
628
|
+
const added = new Set<string>();
|
|
629
|
+
let pending = [...renamed.keys()];
|
|
630
|
+
while (pending.length) {
|
|
631
|
+
for (const name of pending) {
|
|
632
|
+
added.add(name);
|
|
633
|
+
if (!schemas[name]) {
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
636
|
+
document.components = document.components ?? {};
|
|
637
|
+
document.components.schemas = document.components.schemas ?? {};
|
|
638
|
+
document.components.schemas[renamed.get(name)!] ??= toOpenApi30(
|
|
639
|
+
renameSchemaRefs(structuredClone(schemas[name]), renamed),
|
|
640
|
+
) as Record<string, unknown>;
|
|
641
|
+
}
|
|
642
|
+
pending = [...renamed.keys()].filter((name) => !added.has(name));
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* OpenAPI 3.1 → 3.0 for what Better-Auth's generator emits: a `type` array (`['string', 'null']`) becomes
|
|
648
|
+
* `type` plus `nullable`, or `anyOf` for several non-null types. The document itself is OpenAPI 3.0.
|
|
649
|
+
*/
|
|
650
|
+
function toOpenApi30<T>(value: T): T {
|
|
651
|
+
const visit = (node: unknown): void => {
|
|
652
|
+
if (Array.isArray(node)) {
|
|
653
|
+
node.forEach(visit);
|
|
654
|
+
} else if (typeof node === 'object' && node !== null) {
|
|
655
|
+
const record = node as Record<string, unknown>;
|
|
656
|
+
if (Array.isArray(record.type)) {
|
|
657
|
+
const types = record.type as string[];
|
|
658
|
+
const nonNull = types.filter((type) => type !== 'null');
|
|
659
|
+
if (nonNull.length > 1) {
|
|
660
|
+
record.anyOf = nonNull.map((type) => ({ type }));
|
|
661
|
+
delete record.type;
|
|
662
|
+
} else if (nonNull.length === 1) {
|
|
663
|
+
record.type = nonNull[0];
|
|
664
|
+
} else {
|
|
665
|
+
delete record.type;
|
|
666
|
+
}
|
|
667
|
+
if (types.includes('null')) {
|
|
668
|
+
record.nullable = true;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
Object.values(record).forEach(visit);
|
|
672
|
+
}
|
|
673
|
+
};
|
|
674
|
+
visit(value);
|
|
675
|
+
return value;
|
|
676
|
+
}
|
|
677
|
+
|
|
225
678
|
/**
|
|
226
679
|
* The document an API-token caller needs: only the operations that release a scope, authenticated with
|
|
227
680
|
* the bearer scheme only, without the tenant header (a tenant token carries its tenant), and without the
|
|
@@ -2801,7 +2801,11 @@ export interface IServerOptions {
|
|
|
2801
2801
|
};
|
|
2802
2802
|
|
|
2803
2803
|
/**
|
|
2804
|
-
* Whether health check
|
|
2804
|
+
* Whether the health check endpoints are registered. The block's presence enables them; `false`
|
|
2805
|
+
* leaves them out (GET /health-check and the `healthCheck` query answer 404 / are absent) while
|
|
2806
|
+
* keeping the configured checks for later. Honoured since 11.42.9 — before, any `healthCheck`
|
|
2807
|
+
* object registered the endpoints, `enabled: false` included.
|
|
2808
|
+
* @default true (when `healthCheck` is set)
|
|
2805
2809
|
*/
|
|
2806
2810
|
enabled?: boolean;
|
|
2807
2811
|
};
|
|
@@ -3455,6 +3459,10 @@ export interface IServerOptions {
|
|
|
3455
3459
|
* Follows the "Enabled by Default" pattern - tus is automatically enabled
|
|
3456
3460
|
* without any configuration. Set `tus: false` to explicitly disable.
|
|
3457
3461
|
*
|
|
3462
|
+
* Read by `TusModule.forRoot()` when it gets no `config` of its own; an explicit `config` wins.
|
|
3463
|
+
* Before 11.42.9 this key was never read — `tus: false`, `allowedTypes`, `maxSize` and `roles` here
|
|
3464
|
+
* did nothing.
|
|
3465
|
+
*
|
|
3458
3466
|
* Accepts:
|
|
3459
3467
|
* - `true` or `undefined`: Enable with defaults (enabled by default)
|
|
3460
3468
|
* - `false`: Disable TUS uploads
|
|
@@ -3756,28 +3764,29 @@ export interface ITusConfig {
|
|
|
3756
3764
|
|
|
3757
3765
|
/**
|
|
3758
3766
|
* Checksum extension configuration.
|
|
3759
|
-
*
|
|
3767
|
+
* Has no effect with the built-in stores (file system, S3): neither supports the extension.
|
|
3760
3768
|
* @default true
|
|
3761
3769
|
*/
|
|
3762
3770
|
checksum?: boolean;
|
|
3763
3771
|
|
|
3764
3772
|
/**
|
|
3765
3773
|
* Concatenation extension configuration.
|
|
3766
|
-
*
|
|
3774
|
+
* Has no effect with the built-in stores (file system, S3): neither supports the extension.
|
|
3767
3775
|
* @default true
|
|
3768
3776
|
*/
|
|
3769
3777
|
concatenation?: boolean;
|
|
3770
3778
|
|
|
3771
3779
|
/**
|
|
3772
3780
|
* Creation extension configuration.
|
|
3773
|
-
* Allows creating new uploads via POST.
|
|
3781
|
+
* Allows creating new uploads via POST. `false`: POST answers 501 and OPTIONS no longer advertises it
|
|
3782
|
+
* (enforced since 11.42.9 — `@tus/server` itself ignores the flag).
|
|
3774
3783
|
* @default true
|
|
3775
3784
|
*/
|
|
3776
3785
|
creation?: boolean | ITusCreationConfig;
|
|
3777
3786
|
|
|
3778
3787
|
/**
|
|
3779
3788
|
* Creation With Upload extension configuration.
|
|
3780
|
-
* Allows sending data in the initial POST request.
|
|
3789
|
+
* Allows sending data in the initial POST request. `false`: a POST carrying data answers 501 (11.42.9+).
|
|
3781
3790
|
* @default true
|
|
3782
3791
|
*/
|
|
3783
3792
|
creationWithUpload?: boolean;
|
|
@@ -3802,11 +3811,30 @@ export interface ITusConfig {
|
|
|
3802
3811
|
maxSize?: number;
|
|
3803
3812
|
|
|
3804
3813
|
/**
|
|
3805
|
-
* Base path for tus endpoints
|
|
3814
|
+
* Base path for tus endpoints (11.42.9+):
|
|
3815
|
+
* - a new route (`'/uploads'`): the controller is mounted there — a project controller too, as with
|
|
3816
|
+
* `roles`. Like any controller route it sits BELOW a global prefix and a URI version.
|
|
3817
|
+
* - a path ending with the controller's own route (`'/api/tus'` for `tus`): the URL the route is
|
|
3818
|
+
* publicly reachable under — a global prefix, or a proxy that strips a prefix. The controller stays
|
|
3819
|
+
* and upload URLs use this path. That is how `path` had to be set before 11.42.9, and it still works.
|
|
3820
|
+
* - unset: follows the controller's own route.
|
|
3821
|
+
*
|
|
3822
|
+
* Otherwise the URL handed to a client for a new upload is built from the path the upload was created
|
|
3823
|
+
* at, so it is correct below a global prefix or a URI version without any `path`.
|
|
3806
3824
|
* @default '/tus'
|
|
3807
3825
|
*/
|
|
3808
3826
|
path?: string;
|
|
3809
3827
|
|
|
3828
|
+
/**
|
|
3829
|
+
* Hand out upload URLs (`Location`) without scheme and host — `/tus/<id>` instead of
|
|
3830
|
+
* `https://api.example.com/tus/<id>`. For a frontend that reaches TUS through a same-origin proxy:
|
|
3831
|
+
* an absolute URL names the origin the API saw, which the browser cannot reach. A project passing
|
|
3832
|
+
* @tus/server's own `relativeLocation` is honoured too.
|
|
3833
|
+
* @default false
|
|
3834
|
+
* @since 11.42.9
|
|
3835
|
+
*/
|
|
3836
|
+
relativeLocation?: boolean;
|
|
3837
|
+
|
|
3810
3838
|
/**
|
|
3811
3839
|
* Roles allowed to use the tus endpoints (create, write, read offset, terminate).
|
|
3812
3840
|
*
|
|
@@ -3849,7 +3877,8 @@ export interface ITusConfig {
|
|
|
3849
3877
|
|
|
3850
3878
|
/**
|
|
3851
3879
|
* Termination extension configuration.
|
|
3852
|
-
* Allows deleting uploads via DELETE.
|
|
3880
|
+
* Allows deleting uploads via DELETE. `false`: DELETE answers 501 and OPTIONS no longer advertises it
|
|
3881
|
+
* (enforced since 11.42.9 — before, the flag was only logged).
|
|
3853
3882
|
* @default true
|
|
3854
3883
|
*/
|
|
3855
3884
|
termination?: boolean;
|
|
@@ -4071,6 +4100,8 @@ interface IBetterAuthBase {
|
|
|
4071
4100
|
* Disable user registration (sign-up) via BetterAuth.
|
|
4072
4101
|
* Passed through to better-auth's native emailAndPassword.disableSignUp.
|
|
4073
4102
|
* Custom endpoints (GraphQL + REST) also check this flag early.
|
|
4103
|
+
* The Swagger document built by `setupSwagger()` then leaves out `POST /iam/sign-up/email`,
|
|
4104
|
+
* which would answer `SIGNUP_DISABLED` to every caller.
|
|
4074
4105
|
* @default false
|
|
4075
4106
|
*/
|
|
4076
4107
|
disableSignUp?: boolean;
|
|
@@ -4091,6 +4122,8 @@ interface IBetterAuthBase {
|
|
|
4091
4122
|
* Set `false` to withhold the hook, which makes that route answer `RESET_PASSWORD_DISABLED`
|
|
4092
4123
|
* again. For deployments whose reset policy is support-mediated or SSO-primary, and which
|
|
4093
4124
|
* therefore do not want an unauthenticated, token-minting, mail-sending endpoint at all.
|
|
4125
|
+
* The Swagger document built by `setupSwagger()` then leaves out `/iam/request-password-reset`,
|
|
4126
|
+
* `/iam/reset-password` and `/iam/reset-password/{token}`.
|
|
4094
4127
|
*
|
|
4095
4128
|
* @default true
|
|
4096
4129
|
* @since 11.36.1
|
|
@@ -4243,7 +4276,12 @@ interface IBetterAuthBase {
|
|
|
4243
4276
|
|
|
4244
4277
|
/**
|
|
4245
4278
|
* Additional Better-Auth plugins to include.
|
|
4246
|
-
* These will be merged with the built-in plugins (jwt, twoFactor, passkey).
|
|
4279
|
+
* These will be merged with the built-in plugins (jwt, twoFactor, passkey). Their routes appear in the
|
|
4280
|
+
* Swagger document built by `setupSwagger()` automatically.
|
|
4281
|
+
*
|
|
4282
|
+
* Better-Auth's `openAPI()` plugin is registered internally (it feeds `setupSwagger()`), with its routes
|
|
4283
|
+
* kept off the router. Add `openAPI()` here only if the project wants those routes public; it then
|
|
4284
|
+
* replaces the internal registration.
|
|
4247
4285
|
* @see https://www.better-auth.com/docs/plugins
|
|
4248
4286
|
* @example
|
|
4249
4287
|
* ```typescript
|
|
@@ -6,10 +6,13 @@ import type { DocumentBuilder, SwaggerCustomOptions, SwaggerDocumentOptions } fr
|
|
|
6
6
|
* @since 11.42.7
|
|
7
7
|
*/
|
|
8
8
|
export interface ISwaggerOperationInfo {
|
|
9
|
-
/**
|
|
9
|
+
/**
|
|
10
|
+
* Class name of the controller that serves the operation. For a route Better-Auth serves itself, the
|
|
11
|
+
* project's Better-Auth controller (`BetterAuthController` / `CoreBetterAuthController`).
|
|
12
|
+
*/
|
|
10
13
|
controller: string;
|
|
11
14
|
|
|
12
|
-
/** Name of the handler method */
|
|
15
|
+
/** Name of the handler method; for a route Better-Auth serves itself, Better-Auth's operation id */
|
|
13
16
|
handler: string;
|
|
14
17
|
|
|
15
18
|
/** True when neither the role guard nor the tenant guard requires a sign-in */
|
|
@@ -693,6 +693,13 @@ import { mountAiMcpOAuth } from '@lenne.tech/nest-server';
|
|
|
693
693
|
await mountAiMcpOAuth(app);
|
|
694
694
|
```
|
|
695
695
|
|
|
696
|
+
**Swagger (11.42.9):** `setupSwagger()` documents `/ai/mcp` (POST/GET/DELETE, with the bearer
|
|
697
|
+
requirement the endpoint enforces itself) whenever the MCP server is registered, and — because the
|
|
698
|
+
OAuth endpoints are an Express router, invisible to `@nestjs/swagger` — exactly the OAuth endpoints
|
|
699
|
+
`mountAiMcpOAuth()` mounted (`/.well-known/oauth-authorization-server`,
|
|
700
|
+
`/.well-known/oauth-protected-resource/ai/mcp`, `/authorize`, `/token`, `/register`; `/revoke` only for a
|
|
701
|
+
provider with `revokeToken`). Without the call they are not documented, because they do not exist.
|
|
702
|
+
|
|
696
703
|
The issuer — and with it every endpoint URL in the discovery metadata, which MCP clients follow —
|
|
697
704
|
is the server's `baseUrl` (`NSC__BASE_URL` when deployed), resolved the same way BetterAuth and
|
|
698
705
|
CORS resolve it. `local` / `ci` / `e2e` fall back to `http://localhost:3000`; any other environment
|