mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 13:36:48 +00:00
docs: close the bridge chapter
This commit is contained in:
+20
-18
@@ -21,26 +21,28 @@ import { TrekWsAdapter } from './nest/realtime/trek-ws.adapter';
|
||||
* former Express app is gone. One builder is shared by the production bootstrap
|
||||
* (index.ts) and the integration-test harness so the two can never drift.
|
||||
*
|
||||
* Composition order is load-bearing. Everything except the SPA index.html fallback
|
||||
* is registered on the underlying Express instance BEFORE `app.init()`, because
|
||||
* Nest's router terminates an unmatched request by throwing NotFoundException — it
|
||||
* does NOT fall through to a route registered after init, so a post-init Express
|
||||
* route is unreachable. The platform routes are all specific paths (/uploads/*,
|
||||
* /api/health, /mcp, /.well-known/*, /oauth/{authorize,register,consent}) so they
|
||||
* match their own requests and `next()` everything else through to the Nest
|
||||
* controllers registered during init.
|
||||
* Composition order is load-bearing. The few remaining pre-`app.init()` pieces
|
||||
* are registered on the underlying Express instance first, because Nest's
|
||||
* router terminates an unmatched request by throwing NotFoundException — it
|
||||
* does NOT fall through to a route registered after init, so a post-init
|
||||
* Express route is unreachable. Each pre-init piece matches its own requests
|
||||
* and `next()`s everything else through to the Nest controllers:
|
||||
*
|
||||
* 1. applyGlobalMiddleware — helmet/CSP, CORS, HSTS, forced-HTTPS, the global MFA
|
||||
* policy, request logging + cookie-parser. `bodyParser: false` so Nest does its
|
||||
* own parsing and the raw /mcp body reaches the MCP handler unparsed.
|
||||
* 1. applyGlobalMiddleware — helmet/CSP, CORS, HSTS, forced-HTTPS, request
|
||||
* logging + cookie-parser.
|
||||
* 2. applyPlatformUploads — the static + guarded /uploads/* routes.
|
||||
* 3. applyPlatformTransport — /api/health, the OAuth/MCP SDK + /.well-known
|
||||
* metadata, the /mcp routes, the /oauth/consent COOP header.
|
||||
* 4. applyPlatformStatic — the production built-client static assets (so a real
|
||||
* asset request returns the file before the Nest router 404s it).
|
||||
* 4b. setupApiDocs — Swagger UI/spec at /api/docs* when TREK_API_DOCS_ENABLED;
|
||||
* also Express-level, so it must precede init for the same reason.
|
||||
* 5. app.init() — registers every migrated /api domain (the Nest controllers).
|
||||
* 3. MCP_METADATA_MIDDLEWARE — the SDK's OAuth discovery router + addon
|
||||
* gate, container-built (DI) but mounted pathless here (see below).
|
||||
* 4. applyPlatformStatic — the production built-client static assets (so a
|
||||
* real asset request returns the file before the Nest router 404s it).
|
||||
* 4b. setupApiDocs — Swagger UI/spec at /api/docs* when TREK_API_DOCS_ENABLED.
|
||||
* 4c. the named body-parser wrappers — the global 100kb JSON/urlencoded
|
||||
* parsers, exempting /mcp so the MCP SDK reads the raw stream.
|
||||
* 5. app.init() — every domain controller, including the transport surface
|
||||
* that used to live pre-init: /api/health (FeaturesController), OAuth
|
||||
* discovery documents (DiscoveryController), the /oauth/consent COOP
|
||||
* override (ConsentCoopMiddleware), /oauth/authorize + /oauth/register
|
||||
* (SDK routers via OauthModule.configure) and /mcp (McpTransportModule).
|
||||
*
|
||||
* The SPA index.html fallback (unmatched GET → index.html in production) is the
|
||||
* SpaFallbackFilter (APP_FILTER in AppModule); the global error envelope is the
|
||||
|
||||
+29
-13
@@ -152,10 +152,11 @@ tools, which never pass through an HTTP guard. In the five domains piloted for
|
||||
It replaced `setupWebSocket(server)`, which index.ts kicked off with a
|
||||
dynamic import after listen().
|
||||
- `ws-state.ts` holds the socket registry, and that is **module state on
|
||||
purpose**. The out-of-container bridges and `notifications.instance.ts`
|
||||
build `new RealtimeService()` outside the container; rooms on a provider
|
||||
instance would leave their broadcasts going to an empty map, with no error
|
||||
and no log. Same reasoning as the geo throttle cursor.
|
||||
purpose**. The no-Nest test harnesses build `new RealtimeService()`
|
||||
outside the container and 115 test files `vi.mock` the `src/websocket`
|
||||
stub; rooms on a provider instance would leave their broadcasts going to
|
||||
an empty map, with no error and no log. Same reasoning as the geo
|
||||
throttle cursor.
|
||||
- `trek-ws.adapter.ts` exists because the stock `WsAdapter` dispatches on
|
||||
`{ event, data }` and every deployed client sends `{ type, tripId }`. It
|
||||
also builds the ws server itself rather than delegating: the base's port-0
|
||||
@@ -190,9 +191,9 @@ tools, which never pass through an HTTP guard. In the five domains piloted for
|
||||
politely waited and OSM saw a single instance ignoring its rate limit. Both
|
||||
domains now go through `nominatimFetch`.
|
||||
- The throttle cursor and the cache are **module state, not provider state**,
|
||||
for the permissions-cache reason: the bridges build their collaborators with
|
||||
`new`, outside the container, and a second copy of the cursor is the very bug
|
||||
the fold removes. `GeoModule` is registered straight in AppModule and neither
|
||||
for the permissions-cache reason: the no-Nest test harnesses build their
|
||||
collaborators with `new`, outside the container, and a second copy of the
|
||||
cursor is the very bug the fold removes. `GeoModule` is registered straight in AppModule and neither
|
||||
`MapsModule` nor `AtlasModule` imports it — an import would claim an
|
||||
injection edge that does not exist.
|
||||
- `GeocodingService` owns exactly one thing: the cache-cleanup timer's
|
||||
@@ -226,12 +227,27 @@ was the fullest example — a structural `SchedulerDeps` interface filled by
|
||||
`index.ts` — until every cron became an in-container `*.job.ts` provider and
|
||||
the seam disappeared entirely, which is the real endgame for this shape.)
|
||||
|
||||
**`<domain>.bridge.ts` is the older shape** and still correct for the MCP
|
||||
registrars that reach a domain at import time. Its cost is real, though: a
|
||||
bridge rebuilds its services with `new`, so adding one constructor parameter to
|
||||
a service means hand-editing every bridge that constructs it — five of them in
|
||||
one PR, most recently. `app.get()` costs nothing there and returns the container
|
||||
singleton rather than a second instance.
|
||||
**`<domain>.bridge.ts` is a retired shape** — zero bridge files remain since
|
||||
the MCP/OAuth mount moved behind the container (2026-08-11). It served code
|
||||
that ran before `app.init()` and so could not inject; its cost was real: a
|
||||
bridge rebuilt its services with `new`, so adding one constructor parameter to
|
||||
a service meant hand-editing every bridge that constructed it — five of them in
|
||||
one PR, at the worst. If a pre-init consumer ever reappears, prefer the
|
||||
`bootstrap.ts` pattern instead: resolve the container singleton with
|
||||
`app.get(...)` before init (the `httpConfig.KEY` / discovery-metadata
|
||||
precedent) rather than minting a second instance.
|
||||
|
||||
**Express middleware with DI has two honest mounts** (first used by the mount
|
||||
migration): `consumer.apply(...).forRoutes('<concrete path>')` in a module's
|
||||
`configure()` — an Express prefix mount, exactly the legacy `app.use('/path')`
|
||||
semantics, which is how `OauthModule` mounts the MCP SDK's authorize/register
|
||||
routers and `PlatformModule` the consent-COOP override — and, for middleware
|
||||
that must see the ORIGINAL `req.url` (a wildcard `forRoutes()` is a pattern
|
||||
mount that strips the matched prefix), a pathless `app.use` in `bootstrap.ts`
|
||||
over a container-resolved factory provider (`MCP_METADATA_MIDDLEWARE`).
|
||||
`nest/mcp-transport/` (the /mcp controller + service over injected services)
|
||||
and `nest/mcp-shared/` (`McpToolGuardsService` for the `*.mcp.ts` classes) are
|
||||
the module shapes that replaced the last bridges.
|
||||
|
||||
**A bridge imported from INSIDE the container hides an edge from the module
|
||||
graph**, which is a different problem from serving a genuine outside consumer.
|
||||
|
||||
@@ -14,8 +14,7 @@ import { getPhotoProviderConfig } from '../memories/memories.helpers';
|
||||
* bag-tracking and collab-features flags. The boolean polarities differ on
|
||||
* purpose — bag tracking is opt-in (`=== 'true'`, default OFF), the collab
|
||||
* sub-features are opt-out (`!== 'false'`, default ON). Reads are uncached
|
||||
* per-call queries so admin toggles stay immediately visible; out-of-container
|
||||
* consumers go through addons.bridge.ts.
|
||||
* per-call queries so admin toggles stay immediately visible.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonsService {
|
||||
|
||||
@@ -6,11 +6,11 @@ import path from 'path';
|
||||
import { ADDON_IDS } from '../../addons';
|
||||
import { readEnv } from '../../app-config';
|
||||
import { updateJwtSecret } from '../../config';
|
||||
// Import from sessionManager directly, NOT the ../../mcp barrel: the barrel pulls
|
||||
// the whole tools fan-out (and via the domain bridges, the Nest services) into
|
||||
// every consumer of this module — a nest→mcp→nest module cycle. The
|
||||
// invalidateMcpSessions barrel import below predates the fold and is deliberately
|
||||
// separate: it is only reached from the controller, never from the cron path.
|
||||
// Import from sessionManager directly, NOT the ../../mcp barrel — the direct
|
||||
// path keeps this module's graph minimal, and the split predates the barrel's
|
||||
// shrink to process-wide state. The invalidateMcpSessions barrel import below
|
||||
// is deliberately separate: it is only reached from the controller, never from
|
||||
// the cron path.
|
||||
import { revokeUserSessions, revokeUserSessionsForClient } from '../../mcp/sessionManager';
|
||||
import { invalidateMcpSessions } from '../../mcp';
|
||||
import { emitUserDeleted } from '../../plugin-user-lifecycle';
|
||||
|
||||
@@ -34,8 +34,9 @@ const HEARTBEAT_INTERVAL = 30_000;
|
||||
* than an interval nobody stops.
|
||||
*
|
||||
* What did NOT move is the socket registry. It stays module-scoped in
|
||||
* ws-state.ts because out-of-container code (the MCP seam's safeBroadcast,
|
||||
* auth.bridge's graph) must see the same rooms; see the note there.
|
||||
* ws-state.ts because out-of-container code (the no-Nest test harnesses,
|
||||
* the vi.mock'd src/websocket seam) must see the same rooms; see the note
|
||||
* there.
|
||||
*
|
||||
* The wire protocol is unchanged, down to the frame names. TrekWsAdapter maps
|
||||
* `{ type }` onto @SubscribeMessage, because the stock adapter dispatches on
|
||||
|
||||
@@ -4,7 +4,7 @@ import { RealtimeService } from './realtime.service';
|
||||
/**
|
||||
* Global so every migrated module can inject RealtimeService without
|
||||
* re-importing (same rationale as DatabaseModule). The service stays
|
||||
* stateless and dependency-free so the out-of-container bridges can build it
|
||||
* stateless and dependency-free so the no-Nest test harnesses can build it
|
||||
* with a bare `new`.
|
||||
*
|
||||
* The transport itself lives in RealtimeGatewayModule, which AppModule imports
|
||||
|
||||
@@ -108,8 +108,8 @@ export class TrekWsAdapter extends WsAdapter {
|
||||
// failures; this one keeps the transport's own errors visible.
|
||||
server.on('error', (err) => logError(`ws server error: ${err instanceof Error ? err.message : String(err)}`));
|
||||
|
||||
// The fan-out primitives reach the live server through here, and so do the
|
||||
// out-of-container bridges.
|
||||
// The fan-out primitives reach the live server through here, and so does
|
||||
// every out-of-container RealtimeService the test harnesses build.
|
||||
setServer(server);
|
||||
return server;
|
||||
}
|
||||
|
||||
@@ -6,13 +6,13 @@ import { User } from '../../types';
|
||||
* The socket registry: rooms, per-socket identity, and the three fan-out
|
||||
* primitives.
|
||||
*
|
||||
* MODULE state, not provider state, and that is load-bearing. Out-of-container
|
||||
* code still reaches the rooms: the MCP seam's safeBroadcast imports
|
||||
* src/websocket directly, auth.bridge hand-builds a RealtimeService, and the
|
||||
* no-Nest test harnesses construct more. If the rooms lived on a provider
|
||||
* instance, those broadcasts would go to an empty map: no error, no log, just
|
||||
* a client that stops updating. Same reasoning as the geo throttle cursor and
|
||||
* the permissions cache.
|
||||
* MODULE state, not provider state, and that is load-bearing. The no-Nest test
|
||||
* harnesses hand-build RealtimeService instances (mcp-test-controllers.ts and
|
||||
* ~60 unit suites), and the 115 vi.mock('src/websocket') seams assert on these
|
||||
* exact exports. If the rooms lived on a provider instance, an out-of-container
|
||||
* broadcast would go to an empty map: no error, no log, just a client that
|
||||
* stops updating. Same reasoning as the geo throttle cursor and the
|
||||
* permissions cache.
|
||||
*
|
||||
* The gateway next door owns the connection lifecycle and the handshake and
|
||||
* writes into here. This file deliberately knows nothing about auth.
|
||||
|
||||
Reference in New Issue
Block a user