mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-23 07:26:30 +00:00
06dfdc6943
Rate-limit counters, OAuth pending codes, OIDC login states and codes, the permissions cache and the WebSocket rooms were Maps on module or instance scope. That is right while TREK runs as one process, but nothing could be swapped without editing every consumer. Each now sits behind a port with an in-memory implementation that keeps the old behaviour exactly: RateLimitStore, PendingCodeStore, OidcFlowStore, PermissionsCacheStore and RoomRegistry. The owning module provides the implementation with the same lifetime the state had (process-wide for the pending codes, the permissions cache and the rooms, per instance for the rate limits and the OIDC flows), the consumers take it through their constructor, and a hand-built consumer falls back to the same in-memory store, so tests and the free-function paths (the backup restore's cache flush, the broadcasts) see what they saw before. A store shared between processes can replace a provider later without touching the services.
92 lines
4.6 KiB
TypeScript
92 lines
4.6 KiB
TypeScript
import { RateLimitModule } from '../common/rate-limit.module';
|
|
import { Module } from '@nestjs/common';
|
|
import type { MiddlewareConsumer, NestModule } from '@nestjs/common';
|
|
import { MikroOrmModule } from '@mikro-orm/nestjs';
|
|
import { authorizationHandler } from '@modelcontextprotocol/sdk/server/auth/handlers/authorize';
|
|
import { clientRegistrationHandler } from '@modelcontextprotocol/sdk/server/auth/handlers/register';
|
|
import { OauthPublicController } from './oauth-public.controller';
|
|
import { OauthApiController } from './oauth-api.controller';
|
|
import { OauthService } from './oauth.service';
|
|
import { OauthTokenRetentionJob } from './oauth-token-retention.job';
|
|
import { SchedulingModule } from '../scheduling/scheduling.module';
|
|
import { TrekClientsStore, TrekOAuthProvider } from './oauth-sdk.provider';
|
|
import { AuditModule } from '../audit/audit.module';
|
|
import { AddonsModule } from '../addons/addons.module';
|
|
import { AddonsService } from '../addons/addons.service';
|
|
import { createMcpAddonGate } from '../addons/mcp-addon-gate';
|
|
import { OauthClients } from '../../db/entities/OauthClients.entity';
|
|
import { OauthTokens } from '../../db/entities/OauthTokens.entity';
|
|
import { OauthConsents } from '../../db/entities/OauthConsents.entity';
|
|
import { Users } from '../../db/entities/Users.entity';
|
|
import { PendingCodeStore, processPendingCodes } from './oauth.pending-codes';
|
|
|
|
/**
|
|
* OAuth 2.1 server (MCP). Public token/userinfo/revoke endpoints + the SPA's
|
|
* authenticated consent/client/session management, plus the SDK's own
|
|
* /oauth/authorize and /oauth/register Express routers, mounted here through
|
|
* MiddlewareConsumer over the injected TrekOAuthProvider/TrekClientsStore.
|
|
* forRoutes('<path>') is an Express prefix mount — the same app.use('/path')
|
|
* shape (and the same req.url stripping) the pre-init platform mount used, and
|
|
* exactly what the SDK routers are built for. The handlers are constructed once
|
|
* per app instance in configure(), preserving the per-app lifetime of the DCR
|
|
* handler's built-in rate limiter.
|
|
*
|
|
* The handlers bring their own body parsers; the global 100kb parsers run
|
|
* first and theirs no-op on the already-consumed stream. The one observable
|
|
* difference vs the pre-init mount: bracketed keys in a POST /oauth/authorize
|
|
* body now parse with qs (extended:true) rather than querystring — the SDK
|
|
* zod-rejects such bodies either way.
|
|
*
|
|
* Pending authorization codes live behind the PendingCodeStore port
|
|
* (oauth.pending-codes.ts), provided here as the process-wide in-memory
|
|
* instance: the consent controller (container singleton) writes them, the
|
|
* SDK exchange path reads them back through the same injected singleton.
|
|
*
|
|
* Exports OauthService for AdminController (admin OAuth-session panel) and the
|
|
* MCP transport's token verification.
|
|
*
|
|
* `MikroOrmModule.forFeature([OauthClients, OauthTokens, OauthConsents,
|
|
* Users])` (Plan 3b Task 4 deliverable): `Users` is registered for cross-
|
|
* module availability (inventory §6 — `OauthTokensRepository` needs to be
|
|
* importable from `AuthModule` too, Task 5's `revokeAllForUser`, the same
|
|
* shape `McpTokensRepository`/`InviteTokensRepository` already use across
|
|
* `nest/tokens`/`nest/auth`/`nest/oidc`) even though `OauthService` itself
|
|
* never injects `UsersRepository` directly — OA16/OA31 read `users` columns
|
|
* THROUGH `OauthTokensRepository`'s own join methods, not a separate
|
|
* `Users` call (Task 1's ruling: "do not add users methods" for this join).
|
|
*/
|
|
@Module({
|
|
imports: [RateLimitModule, AuditModule, AddonsModule, SchedulingModule, MikroOrmModule.forFeature([OauthClients, OauthTokens, OauthConsents, Users])],
|
|
controllers: [OauthPublicController, OauthApiController],
|
|
providers: [
|
|
OauthService,
|
|
TrekClientsStore,
|
|
TrekOAuthProvider,
|
|
OauthTokenRetentionJob,
|
|
{ provide: PendingCodeStore, useValue: processPendingCodes },
|
|
],
|
|
exports: [OauthService],
|
|
})
|
|
export class OauthModule implements NestModule {
|
|
constructor(
|
|
private readonly addons: AddonsService,
|
|
private readonly provider: TrekOAuthProvider,
|
|
private readonly clients: TrekClientsStore,
|
|
) {}
|
|
|
|
configure(consumer: MiddlewareConsumer): void {
|
|
const mcpAddonGate = createMcpAddonGate(this.addons);
|
|
|
|
// SDK authorize handler: validates OAuth params, calls provider.authorize()
|
|
// which redirects to the SPA consent page at /oauth/consent
|
|
consumer
|
|
.apply(mcpAddonGate, authorizationHandler({ provider: this.provider }))
|
|
.forRoutes('oauth/authorize');
|
|
|
|
// SDK DCR handler: accepts registrations without scope (fixes issue #959 bug 2)
|
|
consumer
|
|
.apply(mcpAddonGate, clientRegistrationHandler({ clientsStore: this.clients }))
|
|
.forRoutes('oauth/register');
|
|
}
|
|
}
|