@lenne.tech/nest-server 11.42.2 → 11.42.4
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/configurable-features.md +3 -1
- package/.claude/rules/package-management.md +1 -1
- package/.claude/rules/role-system.md +31 -3
- package/.claude/rules/testing.md +4 -4
- package/CLAUDE.md +7 -1
- package/FRAMEWORK-API.md +2 -1
- package/dist/core/common/helpers/graphql-ws-connection.helper.d.ts +43 -0
- package/dist/core/common/helpers/graphql-ws-connection.helper.js +219 -0
- package/dist/core/common/helpers/graphql-ws-connection.helper.js.map +1 -0
- package/dist/core/common/helpers/graphql-ws-context.helper.d.ts +2 -1
- package/dist/core/common/helpers/graphql-ws-context.helper.js +54 -18
- package/dist/core/common/helpers/graphql-ws-context.helper.js.map +1 -1
- package/dist/core/common/interfaces/server-options.interface.d.ts +9 -0
- package/dist/core/common/services/core-body-parser.initializer.d.ts +26 -0
- package/dist/core/common/services/core-body-parser.initializer.js +136 -0
- package/dist/core/common/services/core-body-parser.initializer.js.map +1 -0
- package/dist/core/modules/auth/services/core-auth.service.d.ts +1 -0
- package/dist/core/modules/auth/services/core-auth.service.js +19 -0
- package/dist/core/modules/auth/services/core-auth.service.js.map +1 -1
- package/dist/core/modules/better-auth/better-auth.config.d.ts +2 -1
- package/dist/core/modules/better-auth/better-auth.config.js +27 -1
- package/dist/core/modules/better-auth/better-auth.config.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.module.js +7 -3
- package/dist/core/modules/better-auth/core-better-auth.module.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.service.js +4 -0
- package/dist/core/modules/better-auth/core-better-auth.service.js.map +1 -1
- package/dist/core/modules/error-code/error-codes.d.ts +9 -0
- package/dist/core/modules/error-code/error-codes.js +8 -0
- package/dist/core/modules/error-code/error-codes.js.map +1 -1
- package/dist/core/modules/tenant/core-tenant.guard.d.ts +2 -0
- package/dist/core/modules/tenant/core-tenant.guard.js +20 -8
- package/dist/core/modules/tenant/core-tenant.guard.js.map +1 -1
- package/dist/core/modules/user/core-user.service.js +11 -6
- package/dist/core/modules/user/core-user.service.js.map +1 -1
- package/dist/core.module.d.ts +3 -0
- package/dist/core.module.js +102 -42
- 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/server/modules/error-code/error-codes.d.ts +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/docs/REQUEST-LIFECYCLE.md +29 -0
- package/docs/error-codes.md +16 -0
- package/docs/security-overrides.md +18 -2
- package/migration-guides/11.42.2-to-11.42.3.md +145 -0
- package/migration-guides/11.42.3-to-11.42.4.md +185 -0
- package/package.json +3 -3
- package/src/core/common/helpers/graphql-ws-connection.helper.ts +382 -0
- package/src/core/common/helpers/graphql-ws-context.helper.ts +91 -18
- package/src/core/common/interfaces/server-options.interface.ts +121 -2
- package/src/core/common/services/core-body-parser.initializer.ts +232 -0
- package/src/core/modules/auth/services/core-auth.service.ts +37 -0
- package/src/core/modules/better-auth/better-auth.config.ts +46 -1
- package/src/core/modules/better-auth/core-better-auth.module.ts +24 -4
- package/src/core/modules/better-auth/core-better-auth.service.ts +9 -1
- package/src/core/modules/error-code/error-codes.ts +9 -0
- package/src/core/modules/tenant/README.md +8 -8
- package/src/core/modules/tenant/core-tenant.guard.ts +41 -12
- package/src/core/modules/user/core-user.service.ts +19 -6
- package/src/core.module.ts +170 -55
- package/src/index.ts +1 -0
|
@@ -320,7 +320,8 @@ This pattern is currently applied to:
|
|
|
320
320
|
| Response Model Interceptor | `security.responseModelInterceptor` | Boolean Shorthand | `true` (enabled), `debug: false` |
|
|
321
321
|
| Translate Response Interceptor | `security.translateResponseInterceptor` | Boolean Shorthand | `true` (enabled) |
|
|
322
322
|
| Secret Fields Removal | `security.secretFields` | Array | `['password', 'verificationToken', ...]` |
|
|
323
|
-
| Multi-Tenancy | `multiTenancy` | Presence Implies Enabled | **`excludeSchemas` is an OFF SWITCH for isolation, per model** — a listed schema gets no tenant filter at all; since 11.35.0 the plugin WARNS once per model when the excluded schema declares `tenantId` (i.e. was built for isolation). The framework's own docs recommended `['User', 'Session']` until then; that is withdrawn — it is only right for the global-user model. Since 11.35.0 GraphQL operations over the WEBSOCKET also run inside a `RequestContext` (`CoreModule` installs a context-aware `execute`/`subscribe` pair inside `subscriptions`, where `ApolloDriver` forwards it): the tenant comes from the handshake header validated against an active membership, else from the subscriber's memberships, and an unresolvable tenant leaves the safety net to refuse the read rather than returning everything. `headerName: 'x-tenant-id'`, `membershipModel: 'TenantMember'`, `adminBypass: true`, `excludeSchemas: []`, `roleHierarchy: { member: 1, manager: 2, owner: 3 }`, `cacheTtlMs: 30000` (0 disables, process-local). System roles (`S_EVERYONE`, `S_USER`, `S_VERIFIED`) are checked as OR alternatives before real roles; method-level system roles take precedence; membership validated for context when system role grants access + header present. Hierarchy roles use level comparison, normal roles use exact match. Use `DefaultHR` or `createHierarchyRoles()` for type-safe role constants. Bypass: `RequestContext.runWithBypassTenantGuard()`. Cache invalidation: `invalidateUser(userId)` / `invalidateAll()` are **instance** methods on the singleton `CoreTenantGuard` — inject it and call `this.tenantGuard?.invalidateUser(userId)`, never `CoreTenantGuard.invalidateUser(...)` (there is no static). With `redis` configured, both additionally BROADCAST the invalidation to every replica (`<keyPrefix>:tenant-cache:invalidate` pub/sub); without Redis they clear the local process only, so other replicas stay stale until `cacheTtlMs`. A received broadcast clears locally without re-publishing. **`isTenantActive` (11.42.0)**: optional hook `(tenantId, { connection }) => boolean \| Promise<boolean>`, absent = every tenant active (no change). Asked AFTER the membership check on every path that establishes a tenant: 403 on header paths and for tenant API tokens, no enrichment on `S_EVERYONE`, dropped from the header-less tenant list, no tenant for a NEW WebSocket subscription
|
|
323
|
+
| Multi-Tenancy | `multiTenancy` | Presence Implies Enabled | **`excludeSchemas` is an OFF SWITCH for isolation, per model** — a listed schema gets no tenant filter at all; since 11.35.0 the plugin WARNS once per model when the excluded schema declares `tenantId` (i.e. was built for isolation). The framework's own docs recommended `['User', 'Session']` until then; that is withdrawn — it is only right for the global-user model. Since 11.35.0 GraphQL operations over the WEBSOCKET also run inside a `RequestContext` (`CoreModule` installs a context-aware `execute`/`subscribe` pair inside `subscriptions`, where `ApolloDriver` forwards it): the tenant comes from the handshake header validated against an active membership, else from the subscriber's memberships, and an unresolvable tenant leaves the safety net to refuse the read rather than returning everything. `headerName: 'x-tenant-id'`, `membershipModel: 'TenantMember'`, `adminBypass: true`, `excludeSchemas: []`, `roleHierarchy: { member: 1, manager: 2, owner: 3 }`, `cacheTtlMs: 30000` (0 disables, process-local). System roles (`S_EVERYONE`, `S_USER`, `S_VERIFIED`) are checked as OR alternatives before real roles; method-level system roles take precedence; membership validated for context when system role grants access + header present. Hierarchy roles use level comparison, normal roles use exact match. Use `DefaultHR` or `createHierarchyRoles()` for type-safe role constants. Bypass: `RequestContext.runWithBypassTenantGuard()`. Cache invalidation: `invalidateUser(userId)` / `invalidateAll()` are **instance** methods on the singleton `CoreTenantGuard` — inject it and call `this.tenantGuard?.invalidateUser(userId)`, never `CoreTenantGuard.invalidateUser(...)` (there is no static). With `redis` configured, both additionally BROADCAST the invalidation to every replica (`<keyPrefix>:tenant-cache:invalidate` pub/sub); without Redis they clear the local process only, so other replicas stay stale until `cacheTtlMs`. A received broadcast clears locally without re-publishing. **`isTenantActive` (11.42.0)**: optional hook `(tenantId, { connection }) => boolean \| Promise<boolean>`, absent = every tenant active (no change). Asked AFTER the membership check on every path that establishes a tenant: 403 on header paths and for tenant API tokens, no enrichment on `S_EVERYONE`, dropped from the header-less tenant list, no tenant for a NEW WebSocket subscription, and since 11.42.3 an OPEN one is closed with 4403 on `invalidateTenant()` (or at its next event once `graphQl.subscriptionRevalidationMs` has passed — see the GraphQL Subscription Re-check row); an admin under `adminBypass` is exempt (someone must be able to switch it back on). Only `false` deactivates. Cached for `cacheTtlMs`; `invalidateTenant(tenantId)` clears and broadcasts (`{ scope: 'tenant' }`). A function, so `config.env.ts` only — not reachable through `NSC__*`. **`adminBypass: false` (11.42.0)** additionally withholds two things that went around it: `CoreUserService.update()` refuses changing another account's e-mail or password (`assertCredentialChangeAllowed()`, administrator targets included — promote-then-change — system work without a current user exempt), and the AI tool registry grants no ADMIN bypass |
|
|
324
|
+
| GraphQL Subscription Re-check | `graphQl.subscriptionRevalidationMs` | Numeric — `0` = STRICTEST, `false` = off, invalid → default (a fifth shape: Family C's "invalid never means unbounded", but `0` asks for MORE checking, not less) | **30000 since 11.42.3.** Before, a WebSocket was authorized once, at the handshake, and every event and operation on it ran on that snapshot until the client reconnected. Every connection a `CoreModule` `onConnect` accepts is now registered (`registerWsConnection()`) and re-checked — at once when an invalidation names it (`CoreTenantGuard.invalidateUser()` / `invalidateTenant()` / `invalidateAll()`, `CoreUserService.setRoles()` and role/verification `update()`s, the legacy password reset, and Better-Auth deleting a session through `wsSessionRevocationPlugin()`), and otherwise before the next event or operation once the last check is older than this value. The check re-runs the handshake's own authentication (session store, Better-Auth JWT signature/expiry, or for legacy tokens `CoreAuthService.verifyJwt()` — signature/expiry, verified on the WebSocket since 11.42.3 — plus the device binding) and the tenant resolution; any difference in user id, roles, verification state or tenant scope closes the socket with **4403**, which the graphql-ws client retries — the user continues with current rights or is refused at the handshake. `0` re-checks on every event and operation (one session lookup each); `false` switches only the TIME-based check off, invalidations still apply. Process-local registry: an invalidation reaches other replicas only where the tenant guard's Redis broadcast carries it; everything else is bounded by the interval. A project that replaces `onConnect` through `graphQl.driver.subscriptions` must call `registerWsConnection()` itself to keep this. Implementation: `common/helpers/graphql-ws-connection.helper.ts` |
|
|
324
325
|
| API Tokens | `apiTokens` | Boolean Shorthand + Presence Implies Enabled — absent = off | `prefix: 'ltt'`, `scopes: []` (vocabulary; empty = no token creatable, boot warning), `manageRole`: highest hierarchy role, `maxAssertionLifetimeSeconds: 900` (invalid → default, never unbounded — Family C), `rateLimit: { max: 600, windowSeconds: 60 }` per token (**on by default**, `false` disables; Redis-shared when configured), `userTokens: true`, `tenantTokens: true` (effective only with `multiTenancy`), `encryptionKey` → `SECRETS_ENCRYPTION_KEY` → dev default (boot FAILS in production/staging). Top-level, NOT under `multiTenancy`: user tokens must work without it. **Deny by default**: every guard refuses a token on a route without `@ApiTokenScopes()`, public routes included, via ONE shared function (`enforceApiTokenRoute`) called BEFORE each guard's public-route shortcut. USER tokens act as their user with global roles REMOVED (no admin bypass); TENANT tokens act with the lowest hierarchy role in their own tenant, and a route guarded ONLY by `S_SELF` / `S_CREATOR` refuses them (a tenant token is nobody's self — skipping those roles would grant for any target). A tenant token depends on no member, so it outlives an off-boarded or deleted tenant until the project calls `deleteAllForTenant()`. The boot refuses a hierarchy in which that lowest role reaches `manageRole`. A prefixed credential that fails answers 401 on every route (not anonymous); `CoreBetterAuthMiddleware` skips prefixed credentials entirely, cookie fallback included. A password reset that ends the user's sessions ends their USER tokens too (legacy reset always; IAM reset while `betterAuth.emailAndPassword.revokeSessionsOnPasswordReset` is on), wired through `core-api-token.registry.ts` so neither reset path imports the optional module. Implementation: `src/core/modules/api-token/` |
|
|
325
326
|
| BetterAuth Tenant Skip | `betterAuth.skipTenantCheck` | Explicit Boolean | `true` (default). When `true` and no `X-Tenant-Id` header is sent, IAM endpoints (controller + resolver) skip `CoreTenantGuard` tenant validation. When header IS present, normal membership validation runs regardless. Set `false` for tenant-aware auth scenarios (subdomain-based, invite links, SSO per tenant) |
|
|
326
327
|
| Debug Process Input | `debugProcessInput` | Explicit Boolean | `false` (default). When `true`, logs a debug message when `prepareInput()` changes the input type during `process()`. Has performance cost due to `JSON.stringify` on every `process()` call — enable only for debugging |
|
|
@@ -334,6 +335,7 @@ This pattern is currently applied to:
|
|
|
334
335
|
| TUS S3 Staging | `tus.s3Staging` | Explicit Boolean (default ON when S3 configured) | `true` when `s3` is configured, otherwise inert. Stages in-progress uploads in `s3.stagingBucket` via `@tus/s3-store` instead of `tus.uploadDir` on local disk, so resumable uploads survive replica restarts and need no sticky sessions. Set `false` to force local disk. Missing OPTIONAL peer `@tus/s3-store` → warning + fall back to local disk (NOT a boot failure). **Give the staging bucket a lifecycle rule expiring incomplete multipart uploads** — the framework's own expiration cleanup is skipped in S3 mode (S3 is the right place for that policy), so aborted uploads otherwise accumulate parts nothing removes |
|
|
335
336
|
| Cron Job Deduplication | per job: `distributed` in `CronJobConfig` | Explicit Boolean | **`true` when `redis` is configured, otherwise `false`** — a single-replica project that upgrades must not silently gain a `cron-locks` collection, a lease write per tick, and a new way for a tick to be skipped. A Redis-less multi-replica fleet opts in per job with `distributed: true` (MongoDB lease). Mechanism: BullMQ job scheduler when Redis + the OPTIONAL peer `bullmq` are present AND `cronTime` is a string without `utcOffset`; otherwise local timer + lease (Redis `SET NX`, else a TTL-indexed `cron-locks` document). Tick lease TTL 3600 s. **Leases fail open** — an unreachable lease store runs the tick everywhere rather than stopping all scheduled work fleet-wide. **`runOnInit` (default `true`) deduplicates over a FIXED per-job key with a 300 s TTL**, because replicas do not share a boot instant: replicas booting within 5 min run the startup tick once between them, and a replica restarting inside that window SKIPS its startup tick — set `distributed: false` on jobs whose `runOnInit` work is per-process (warming a process-local cache). No constructor change needed: `CoreModule` fills `core-cron-jobs.registry.ts` via `CoreCronJobsInitializer` and `CoreCronJobs` reads it lazily; explicit `{ connection, redisService }` in `CoreCronJobsOptions` wins. With neither source it warns once and every replica runs every tick |
|
|
336
337
|
| Shutdown Delay | `shutdownDelayMs` | Numeric Sentinel — Family B (`0` = off) | `0` (default, no delay, no log). Waits N ms **in the SIGTERM/SIGINT handler, before `close()` is entered**, so a load balancer can finish deregistering while the instance is still fully healthy. NOT a lifecycle hook: `close()` runs `onModuleDestroy` → `beforeApplicationShutdown` → dispose → `onApplicationShutdown`, so a delay in `beforeApplicationShutdown` would wait with every module already torn down while the socket still accepts — worse than no delay. **Requires `installGracefulShutdown(app)` in main.ts, which REPLACES `server.enableShutdownHooks()`** — keeping both makes Nest close the app in parallel with the wait, so the delay silently never happens. At delay `0` the helper IS `enableShutdownHooks()`. **Keep the value well below the orchestrator grace period AND leave room for the drain that follows**: Compose `stop_grace_period` 10s, Kubernetes `terminationGracePeriodSeconds` 30s, and `installProcessDiagnostics()` force-exits after 30s — exceed any and the process is SIGKILLed mid-wait with no hook running. Warns above `10000`, capped at `60000`. Non-numeric / negative values behave like `0` |
|
|
338
|
+
| Body Parser Limits | `bodyParser.json.limit`, `bodyParser.urlencoded.limit` | Explicit Value (Express/body-parser default) — invalid → default, never unbounded | **Unset = 100 kB each, exactly as NestJS registers the parsers** (body-parser's own default — a limit nobody chose, and before 11.42.4 the only one in force: over it a request got a bare `request entity too large`, before auth and routing, GraphQL included). A number is bytes, a string a 1024-based size (`'500kb'`, `'2mb'`, units `b`/`kb`/`mb`/`gb`). The two limits are separate on purpose — raising JSON must not raise form posts. **Not raised by default because the parser runs before authentication** and `JSON.parse` blocks the event loop for the whole body: ~0.5 ms for 100 kB, ~16 ms for 1 MB, ~55 ms for 2 MB per request on a realistic document shape. Applied by `CoreBodyParserInitializer` (CoreModule provider) in `onModuleInit` by **swapping the handle of the global `jsonParser`/`urlencodedParser` layers NestJS registered, in place** — NestJS registers them inside `app.init()` before any init hook, and a parser appended later would sit behind the routes. Same position, `extended: true` for URL-encoded, `req.rawBody` kept when the app came from `NestFactory.create(…, { rawBody: true })` (read from the container's context options; under `Test.createTestingModule()` those options never reach the container, so a test app with `rawBody: true` gets no `req.rawBody` from a CONFIGURED parser). Because it runs during module init it **holds under `Test.createTestingModule()`**, which never runs `main.ts` — the reason a `useBodyParser()` in `main.ts` was not enough. A configured value also replaces a global parser registered in `main.ts`; a **path-scoped** one (`server.use('/upload', json({ limit }))`) is left alone and stays the way to give one path its own limit (the parser runs before routing; GraphQL shares one path). An invalid value (`0`, negative, `NaN`, a word) is refused with an error log and the parser keeps its limit. **Independent of configuration**, every 413 from a global parser is answered as `PayloadTooLargeException` with `#LTNS_0304: Request body too large [<n> bytes, limit <m> bytes]` (size only when a `Content-Length` was sent) and logged as a WARN naming method, path without query string, and the config key — no boot warning, because the framework cannot know which routes expect large bodies; the first oversized request is the earliest point anything can be said. Other parser errors pass through unchanged. Pinned by `tests/unit/body-parser.spec.ts` and `tests/body-parser.e2e-spec.ts` (mutations `body-parser-*`). Implementation: `common/services/core-body-parser.initializer.ts` |
|
|
337
339
|
| Trust Proxy | `trustProxy` | Explicit Value (pass-through, Express default) | `false` (Express's own default — the forwarded chain is not trusted). Passed verbatim to `app.set('trust proxy', …)` by `CoreTrustProxyInitializer`, a `CoreModule` provider, so a consumer inherits it by upgrading and needs no `main.ts` edit. Accepts `false` / a hop count (`1`, `2`) / `'loopback'` / a subnet list — **not** Express's predicate function: the value must survive `NEST_SERVER_CONFIG` / `NSC__*` (JSON) and the ConfigService deep clone. **This is what makes `request.ip` correct, and every IP-keyed rate limit depends on it**: unset behind Caddy/nginx/an ingress, `req.ip` is the PROXY address for every request, so all clients share ONE bucket and `auth.rateLimit.max` throttles everybody at once — exactly fleet-wide once `redis` is configured. Trusting MORE hops than exist is the opposite failure: a client prepends its own header entry and picks a fresh bucket per request. Applied at module init (inside `app.init()`/`listen()`, i.e. AFTER `main.ts`), so a configured value wins over a hand-written `app.set()`; an UNSET value is never applied, which keeps `app.set('trust proxy', fn)` in `main.ts` available as the escape hatch for the predicate form. **Unset + an IP-keyed limiter enabled (`auth.rateLimit` / `betterAuth.rateLimit`) logs a boot warning naming the shared-bucket consequence**; `trustProxy: false` is the explicit "nothing proxies me" answer that silences it. The AI limiter keys on the user id and is unaffected |
|
|
338
340
|
| Outbound URL Guard (SSRF) | `security.allowPrivateOutboundTargets` | Explicit Boolean with DERIVED default | **Opt-in helper for URLs a USER entered** (webhooks, export targets, ERP addresses) — `outboundFetch()`, `assertOutboundUrlAllowed()`, `createOutboundDispatcher()` in `common/helpers/outbound-url.helper.ts` (11.42.0). Nothing is applied globally: configured targets (AI providers, SMTP, S3, Redis, Brevo) never go through it. Refuses loopback, RFC 1918, link-local incl. `169.254.169.254`, CGNAT, `0/8`, benchmarking, multicast/reserved, IPv6 unspecified/loopback/IPv4-compatible (`::/96`), NAT64, ULA, link-local and multicast, and IPv4-mapped IPv6 in every notation (canonicalised through the URL parser). Checked TWICE: on the URL before sending (readable `OutboundUrlBlockedError`) and on every connection through an undici `Agent` with a guarded connector — the only place that sees a redirect's next hop and a DNS answer at connect time (rebinding). **Unset → derived from the environment**: allowed in `local`, `e2e`, `ci`, refused everywhere else — deliberately including `test`, a DEPLOYED stage in starter-based projects; `true`/`false` (also the strings, from `NSC__SECURITY__ALLOW_PRIVATE_OUTBOUND_TARGETS`) decide explicitly. Per call, `allowedHosts` frees single internal hosts (`'erp.local:8443'` = that port, `'erp.local'` = any port) without opening the rest — re-checked per connection, so a redirect does not inherit it. `outboundFetch()` uses undici's OWN `fetch` (runtime dependency `undici` since 11.42.0), because Node 22's built-in fetch bundles a different undici major than the agent |
|
|
339
341
|
| AI Egress Allowlist (SSRF) | `ai.allowedBaseUrlHosts` | Explicit Value — **unset = permissive**, malformed = INTERPRETED or reported, never silently off | `undefined` → no restriction, so a local provider (Ollama on `localhost:11434`) works out of the box. Set → only those hosts are reachable, matched against `URL.host` (incl. port) and `URL.hostname`. **Accepts an array OR a comma-separated string, and the string form is not a convenience** — it is what the framework's own env mapping produces: `NSC__AI__ALLOWED_BASE_URL_HOSTS` becomes `{ ai: { allowedBaseUrlHosts: '<string>' } }` and lodash `merge` assigns that scalar straight over a configured array. Until 11.39.x a bare `!Array.isArray(...) -> return` read that as "not configured" and skipped the check entirely, so the canonical `NSC__` spelling silently disabled SSRF egress control with no log line at all. Entries are trimmed, lowercased and stripped of a fully-qualifying trailing dot, and the SAME normalisation is applied to the URL, so neither side wins by spelling one DNS name differently; a bare hostname entry matches any port, and an entry naming the scheme's default port (`example.com:443` on https) also matches the portless URL. A value that is neither array nor string carries no hostnames — the allowlist is then inactive and that is LOGGED as an error, because from the outside it looks exactly like "correctly unset". Enforced on **all three** outbound paths (`chat()`, the capability `probe()`, and `probeContextWindow()` — the last was unguarded before 11.39.x and is reachable from an ordinary user prompt via `detectAndPersistCapabilities()`, before the rate limit). Implementation: `openai-compatible.provider.ts` → `assertBaseUrlAllowed()` / `resolveAllowedBaseUrlHosts()` |
|
|
@@ -169,7 +169,7 @@ Two ordering rules, both enforced by the contract test:
|
|
|
169
169
|
Package overrides live in the `overrides:` section of **`pnpm-workspace.yaml`** (they moved out of `package.json`'s `pnpm.overrides` in the pnpm 11 upgrade). They force transitive dependencies to a security-patched version.
|
|
170
170
|
|
|
171
171
|
**Keep the set minimal.** On the pnpm 11 upgrade the list was pruned from 36 to the 9 then still
|
|
172
|
-
load-bearing (it has since grown back to
|
|
172
|
+
load-bearing (it has since grown back to 19 as new advisories landed) — an override is only necessary if removing it lets the package resolve back INTO its vulnerable range (verify with a with/without lockfile diff; `pnpm audit` is the arbiter). Each surviving entry carries its CVE rationale as a comment. Remove an entry once its parent dependency ships a fixed version.
|
|
173
173
|
|
|
174
174
|
### Rule: Override Targets MUST Be Fixed Versions
|
|
175
175
|
|
|
@@ -444,9 +444,37 @@ account over with an address change and a reset, which is precisely the access `
|
|
|
444
444
|
withholds.
|
|
445
445
|
|
|
446
446
|
**Deactivated tenants.** `multiTenancy.isTenantActive` is asked after the membership on every path that
|
|
447
|
-
establishes a tenant (see `.claude/rules/configurable-features.md`, Multi-Tenancy row).
|
|
448
|
-
|
|
449
|
-
|
|
447
|
+
establishes a tenant (see `.claude/rules/configurable-features.md`, Multi-Tenancy row).
|
|
448
|
+
|
|
449
|
+
**Open WebSockets (11.42.3).** A WebSocket is authorized at the handshake, and until 11.42.3 that was
|
|
450
|
+
the only check: a removed membership, a deactivated tenant, a withdrawn role or a revoked session took
|
|
451
|
+
effect on every new request and on none of the sockets already open. Now every connection
|
|
452
|
+
`CoreModule`'s `onConnect` accepts is registered (`registerWsConnection()`,
|
|
453
|
+
`common/helpers/graphql-ws-connection.helper.ts`) with the rights it was authorized with, and is
|
|
454
|
+
checked again
|
|
455
|
+
|
|
456
|
+
| Trigger | When |
|
|
457
|
+
|---------|------|
|
|
458
|
+
| `CoreTenantGuard.invalidateUser()` / `invalidateTenant()` / `invalidateAll()` (also when received from another replica), `CoreUserService.setRoles()`, an `update()` touching `roles` / `verified` / `emailVerified`, the legacy password reset | at once |
|
|
459
|
+
| Better-Auth deleting a session (sign-out, revoked session, reset with `revokeSessionsOnPasswordReset`) — through `wsSessionRevocationPlugin()`, a plugin so a project's own `options.databaseHooks` cannot displace it | at once |
|
|
460
|
+
| anything nobody invalidated (a membership edited in the database, an expired session) | before the next event or operation once the last check is older than `graphQl.subscriptionRevalidationMs` (30 s) |
|
|
461
|
+
|
|
462
|
+
A connection whose rights CHANGED — in either direction — is closed with **4403**, which the
|
|
463
|
+
graphql-ws client retries: it reconnects and resubscribes with its current rights, or is refused at the
|
|
464
|
+
handshake. It is closed rather than patched because a subscription may have picked its topic or filter
|
|
465
|
+
from the tenant at subscribe time. An event is checked after graphql-js executed it and before it is
|
|
466
|
+
sent, so a dropped event's reads happened but none of it reaches the client. Pinned by
|
|
467
|
+
`tests/subscription-revalidation.e2e-spec.ts` (mechanism), `tests/subscription-session-iam.e2e-spec.ts`
|
|
468
|
+
and `tests/subscription-session-legacy.e2e-spec.ts` (assembled stack).
|
|
469
|
+
|
|
470
|
+
The check re-runs the HANDSHAKE'S authentication, which on the legacy builder means
|
|
471
|
+
`CoreAuthService.verifyJwt()` — signature and expiry, in passport-jwt's key order — before
|
|
472
|
+
`validateUser()`'s device binding. The handshake used `decodeJwt()` until 11.42.3, so an expired or
|
|
473
|
+
re-signed legacy token opened sockets that HTTP refused (mutation `legacy-ws-jwt-unverified`). A legacy
|
|
474
|
+
socket therefore ends with its token: at expiry or at the next token refresh.
|
|
475
|
+
|
|
476
|
+
**A guard that asks "is this request allowed?" once is not enough for a connection that outlives the
|
|
477
|
+
answer.** When you add a long-lived channel (SSE, a WebSocket of your own), register it the same way.
|
|
450
478
|
|
|
451
479
|
## Role Check Implementation
|
|
452
480
|
|
package/.claude/rules/testing.md
CHANGED
|
@@ -266,7 +266,7 @@ pnpm run check:mutations -- --id=<id> # one mutation
|
|
|
266
266
|
pnpm run check:mutations -- --list # the registry, without running anything
|
|
267
267
|
pnpm run check:mutations -- --allow-dirty # when the fix and its evidence share a working tree
|
|
268
268
|
pnpm run check:mutations -- --jobs=4 # N mutations at a time (default: 2, or 4 on >=12 cores)
|
|
269
|
-
pnpm run check:mutations -- --no-infra # only the
|
|
269
|
+
pnpm run check:mutations -- --no-infra # only the 97 that need no MongoDB
|
|
270
270
|
pnpm run check:mutations -- --since=<ref> # only mutations touching files changed since <ref>
|
|
271
271
|
```
|
|
272
272
|
|
|
@@ -285,7 +285,7 @@ when the registry, a vitest config or a setup file changed, since those can move
|
|
|
285
285
|
commit, and in CI alongside the publish without blocking it. The price of caching is a cache that
|
|
286
286
|
has to model each spec's full dependency closure correctly, and getting that wrong produces a stale
|
|
287
287
|
PASS for a test that has since gone vacuous — exactly what the gate is there to prevent. Bad trade
|
|
288
|
-
at
|
|
288
|
+
at 160 mutations.
|
|
289
289
|
|
|
290
290
|
**Decided 2026-10-03 (repo owner): keep the full run.** Measured at 128 mutations, the CI job for
|
|
291
291
|
11.41.6 took **14 minutes**. The trigger to revisit is that wall time, not the mutation count —
|
|
@@ -330,9 +330,9 @@ exactly the environment where the answer matters.
|
|
|
330
330
|
|
|
331
331
|
### The cost is vitest's cold start, not the tests
|
|
332
332
|
|
|
333
|
-
Worth knowing before optimising the wrong thing: the specs behind all
|
|
333
|
+
Worth knowing before optimising the wrong thing: the specs behind all 63 e2e mutations add up to
|
|
334
334
|
**~40 seconds**. The step takes ~740s. The remaining ~700s is paying vitest's startup — process
|
|
335
|
-
spawn, transform, module graph, mongod connect, DB create and drop — once per mutation,
|
|
335
|
+
spawn, transform, module graph, mongod connect, DB create and drop — once per mutation, 160 times.
|
|
336
336
|
That work is largely single-threaded I/O and barely scales with cores: the full registry measures
|
|
337
337
|
**744s on a 12-core laptop and 777s on a 4-vCPU CI runner**.
|
|
338
338
|
|
package/CLAUDE.md
CHANGED
|
@@ -233,6 +233,11 @@ in services (MCP/AI tools call services directly), tenant-aware tool roles in th
|
|
|
233
233
|
boundary under `adminBypass: false`. User-entered URLs (webhooks, export targets) go through
|
|
234
234
|
`outboundFetch()` (SSRF guard) — configured targets do not.
|
|
235
235
|
|
|
236
|
+
Since 11.42.3 an **open GraphQL WebSocket** is re-checked, not trusted for its lifetime: at once on an
|
|
237
|
+
invalidation (tenant guard, role change, password reset, sign-out), otherwise every
|
|
238
|
+
`graphQl.subscriptionRevalidationMs` (30 s); changed rights close it with 4403 and the client
|
|
239
|
+
reconnects. A custom `onConnect` must call `registerWsConnection()` to keep that.
|
|
240
|
+
|
|
236
241
|
See `.claude/rules/role-system.md` for complete documentation.
|
|
237
242
|
|
|
238
243
|
### Versioning
|
|
@@ -500,8 +505,9 @@ lt dev doctor # Diagnose Caddy/CA/DNS/port issues
|
|
|
500
505
|
**Active URLs for THIS project:**
|
|
501
506
|
|
|
502
507
|
- API: `https://api.nest-server.localhost`
|
|
508
|
+
- The `*.localhost` names are resolved by BROWSERS. Node, `curl` and other tools may not resolve them (they do not on Windows), so a script uses the loopback address: `LT_DEV_API_INTERNAL_URL` (set in the processes and in `.lt-dev/.env`), or the `→ 127.0.0.1:<port>` that `lt dev status` prints. The port differs per machine, which is why it is not written here.
|
|
503
509
|
- DB: `mongodb://127.0.0.1/nest-server-local`
|
|
504
510
|
|
|
505
|
-
Env vars set automatically by `lt dev up`: `BASE_URL`, `APP_URL`, `NUXT_API_URL`, `NUXT_PUBLIC_API_URL`, `NUXT_PUBLIC_SITE_URL`, `NUXT_PUBLIC_STORAGE_PREFIX`, `NSC__MONGOOSE__URI`, `DATABASE_URL`. **Never assume `localhost:3000` / `localhost:3001` for this project** — those are the framework defaults, not the active URLs.
|
|
511
|
+
Env vars set automatically by `lt dev up`: `BASE_URL`, `APP_URL`, `NUXT_API_URL`, `NUXT_PUBLIC_API_URL`, `NUXT_PUBLIC_SITE_URL`, `NUXT_PUBLIC_STORAGE_PREFIX`, `NUXT_SESSION_PASSWORD`, `NSC__MONGOOSE__URI`, `DATABASE_URL`. **Never assume `localhost:3000` / `localhost:3001` for this project** — those are the framework defaults, not the active URLs.
|
|
506
512
|
|
|
507
513
|
<!-- lt-dev:url-block:end -->
|
package/FRAMEWORK-API.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @lenne.tech/nest-server — Framework API Reference
|
|
2
2
|
|
|
3
|
-
> Auto-generated from source code as of 2026-10-
|
|
3
|
+
> Auto-generated from source code as of 2026-10-06 (v11.42.4)
|
|
4
4
|
> File: `FRAMEWORK-API.md` — compact, machine-readable API surface for Claude Code
|
|
5
5
|
|
|
6
6
|
## CoreModule.forRoot()
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
- `automaticObjectIdFiltering?`: `boolean | undefined` — Automatically detect ObjectIds in string values in FilterQueries
|
|
20
20
|
- `baseUrl?`: `string | undefined` — Base URL of the API server.
|
|
21
21
|
- `betterAuth?`: `boolean | IBetterAuth | undefined` (default: `undefined (enabled with defaults)`) — Configuration for better-auth authentication framework.
|
|
22
|
+
- `bodyParser?`: `IBodyParserConfig | undefined` — Size limits for request bodies.
|
|
22
23
|
- `brevo?`: `{ apiKey: string; exclude?: RegExp; maxRetries?: number; sender: { email: str...` — Configuration for Brevo
|
|
23
24
|
- `compression?`: `boolean | compression.CompressionOptions | undefined` — Whether to use the compression middleware package to enable gzip compression.
|
|
24
25
|
- `cookies?`: `boolean | ICookiesConfig | undefined` (default: `true`) — Cookie configuration for authentication handling.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { ResolvedTenantContext } from '../services/core-tenant-context.registry';
|
|
2
|
+
export declare const WS_AUTHORIZATION_CHANGED_CLOSE_CODE = 4403;
|
|
3
|
+
export declare const DEFAULT_SUBSCRIPTION_REVALIDATION_MS = 30000;
|
|
4
|
+
export interface IWsConnectionSocket {
|
|
5
|
+
close(code: number, reason?: string): void;
|
|
6
|
+
once?(event: 'close', listener: (...args: any[]) => void): unknown;
|
|
7
|
+
}
|
|
8
|
+
export interface IWsConnectionRegistration {
|
|
9
|
+
carrier: object;
|
|
10
|
+
reauthenticate?: () => any;
|
|
11
|
+
socket?: IWsConnectionSocket;
|
|
12
|
+
user: any;
|
|
13
|
+
}
|
|
14
|
+
export interface IWsConnectionIdentity {
|
|
15
|
+
email?: string;
|
|
16
|
+
iamId?: string;
|
|
17
|
+
userId?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface WsConnection {
|
|
20
|
+
readonly email?: string;
|
|
21
|
+
readonly iamId?: string;
|
|
22
|
+
readonly reauthenticate?: () => any;
|
|
23
|
+
readonly socket?: IWsConnectionSocket;
|
|
24
|
+
tenant?: ResolvedTenantContext;
|
|
25
|
+
readonly tenantHeader?: string;
|
|
26
|
+
terminated: boolean;
|
|
27
|
+
readonly user: any;
|
|
28
|
+
readonly userFingerprint: string;
|
|
29
|
+
readonly userId?: string;
|
|
30
|
+
inflight?: Promise<boolean>;
|
|
31
|
+
stale: boolean;
|
|
32
|
+
tenantFingerprint?: string;
|
|
33
|
+
validatedAt: number;
|
|
34
|
+
}
|
|
35
|
+
export declare function resolveSubscriptionRevalidationMs(value: unknown): false | number;
|
|
36
|
+
export declare function readWsTenantHeader(contextValue: any): string | undefined;
|
|
37
|
+
export declare function registerWsConnection(registration: IWsConnectionRegistration): void;
|
|
38
|
+
export declare function getWsConnection(contextValue: any): undefined | WsConnection;
|
|
39
|
+
export declare function ensureWsConnectionCurrent(connection: WsConnection): Promise<boolean>;
|
|
40
|
+
export declare function noteWsConnectionTenant(connection: WsConnection, tenant: ResolvedTenantContext | undefined): boolean;
|
|
41
|
+
export declare function revalidateWsConnectionsOf(identity: IWsConnectionIdentity): number;
|
|
42
|
+
export declare function revalidateWsConnectionsForTenant(tenantId: string): number;
|
|
43
|
+
export declare function revalidateAllWsConnections(): number;
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_SUBSCRIPTION_REVALIDATION_MS = exports.WS_AUTHORIZATION_CHANGED_CLOSE_CODE = void 0;
|
|
4
|
+
exports.resolveSubscriptionRevalidationMs = resolveSubscriptionRevalidationMs;
|
|
5
|
+
exports.readWsTenantHeader = readWsTenantHeader;
|
|
6
|
+
exports.registerWsConnection = registerWsConnection;
|
|
7
|
+
exports.getWsConnection = getWsConnection;
|
|
8
|
+
exports.ensureWsConnectionCurrent = ensureWsConnectionCurrent;
|
|
9
|
+
exports.noteWsConnectionTenant = noteWsConnectionTenant;
|
|
10
|
+
exports.revalidateWsConnectionsOf = revalidateWsConnectionsOf;
|
|
11
|
+
exports.revalidateWsConnectionsForTenant = revalidateWsConnectionsForTenant;
|
|
12
|
+
exports.revalidateAllWsConnections = revalidateAllWsConnections;
|
|
13
|
+
const common_1 = require("@nestjs/common");
|
|
14
|
+
const config_service_1 = require("../services/config.service");
|
|
15
|
+
const core_tenant_context_registry_1 = require("../services/core-tenant-context.registry");
|
|
16
|
+
exports.WS_AUTHORIZATION_CHANGED_CLOSE_CODE = 4403;
|
|
17
|
+
exports.DEFAULT_SUBSCRIPTION_REVALIDATION_MS = 30_000;
|
|
18
|
+
const WS_CONNECTION = Symbol('ltWsConnection');
|
|
19
|
+
const liveConnections = new Set();
|
|
20
|
+
const logger = new common_1.Logger('GraphQlWsConnection');
|
|
21
|
+
function resolveSubscriptionRevalidationMs(value) {
|
|
22
|
+
if (value === false || value === 'false') {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
const numeric = typeof value === 'string' && value.trim() !== '' ? Number(value) : value;
|
|
26
|
+
if (typeof numeric === 'number' && Number.isFinite(numeric) && numeric >= 0) {
|
|
27
|
+
return numeric;
|
|
28
|
+
}
|
|
29
|
+
return exports.DEFAULT_SUBSCRIPTION_REVALIDATION_MS;
|
|
30
|
+
}
|
|
31
|
+
function configuredRevalidationMs() {
|
|
32
|
+
const graphQl = config_service_1.ConfigService.configFastButReadOnly?.graphQl;
|
|
33
|
+
return resolveSubscriptionRevalidationMs(graphQl ? graphQl.subscriptionRevalidationMs : undefined);
|
|
34
|
+
}
|
|
35
|
+
function fingerprintUser(user) {
|
|
36
|
+
if (!user) {
|
|
37
|
+
return '';
|
|
38
|
+
}
|
|
39
|
+
const roles = Array.isArray(user.roles) ? user.roles.map((role) => String(role)).sort() : [];
|
|
40
|
+
const verified = !!(user.verified || user.verifiedAt || user.emailVerified);
|
|
41
|
+
return JSON.stringify([String(user.id ?? user._id ?? ''), roles, verified]);
|
|
42
|
+
}
|
|
43
|
+
function fingerprintTenant(tenant) {
|
|
44
|
+
const tenantIds = Array.isArray(tenant?.tenantIds) ? tenant.tenantIds.map((id) => String(id)).sort() : null;
|
|
45
|
+
return JSON.stringify([tenant?.tenantId ?? null, tenantIds, tenant?.tenantRole ?? null, !!tenant?.isAdminBypass]);
|
|
46
|
+
}
|
|
47
|
+
function readWsTenantHeader(contextValue) {
|
|
48
|
+
const headerName = (config_service_1.ConfigService.configFastButReadOnly?.multiTenancy?.headerName ?? 'x-tenant-id').toLowerCase();
|
|
49
|
+
const sources = [contextValue?.headers, contextValue?.connectionParams, contextValue?.request?.headers];
|
|
50
|
+
for (const source of sources) {
|
|
51
|
+
if (!source || typeof source !== 'object') {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
for (const [key, value] of Object.entries(source)) {
|
|
55
|
+
if (key.toLowerCase() === headerName && typeof value === 'string' && value) {
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
function registerWsConnection(registration) {
|
|
63
|
+
const { carrier, reauthenticate, socket, user } = registration;
|
|
64
|
+
if (!carrier || typeof carrier !== 'object') {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const userId = user?.id ?? user?._id;
|
|
68
|
+
const connection = {
|
|
69
|
+
email: typeof user?.email === 'string' ? user.email.toLowerCase() : undefined,
|
|
70
|
+
iamId: typeof user?.iamId === 'string' ? user.iamId : undefined,
|
|
71
|
+
reauthenticate,
|
|
72
|
+
socket,
|
|
73
|
+
stale: false,
|
|
74
|
+
tenantHeader: readWsTenantHeader(carrier),
|
|
75
|
+
terminated: false,
|
|
76
|
+
user,
|
|
77
|
+
userFingerprint: fingerprintUser(user),
|
|
78
|
+
userId: userId !== undefined && userId !== null ? String(userId) : undefined,
|
|
79
|
+
validatedAt: Date.now(),
|
|
80
|
+
};
|
|
81
|
+
Object.defineProperty(carrier, WS_CONNECTION, { configurable: true, enumerable: true, value: connection });
|
|
82
|
+
if (socket && typeof socket.once === 'function') {
|
|
83
|
+
liveConnections.add(connection);
|
|
84
|
+
socket.once('close', () => {
|
|
85
|
+
connection.terminated = true;
|
|
86
|
+
liveConnections.delete(connection);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function getWsConnection(contextValue) {
|
|
91
|
+
if (!contextValue || typeof contextValue !== 'object') {
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
const connection = contextValue[WS_CONNECTION];
|
|
95
|
+
return connection && typeof connection === 'object' ? connection : undefined;
|
|
96
|
+
}
|
|
97
|
+
function terminate(connection, reason) {
|
|
98
|
+
if (connection.terminated) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
connection.terminated = true;
|
|
102
|
+
liveConnections.delete(connection);
|
|
103
|
+
logger.log(`Closing GraphQL WebSocket of user ${connection.userId ?? '(unknown)'}: ${reason}`);
|
|
104
|
+
try {
|
|
105
|
+
connection.socket?.close(exports.WS_AUTHORIZATION_CHANGED_CLOSE_CODE, reason);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
}
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
async function revalidate(connection) {
|
|
112
|
+
connection.stale = false;
|
|
113
|
+
try {
|
|
114
|
+
let user = connection.user;
|
|
115
|
+
if (connection.reauthenticate) {
|
|
116
|
+
user = await connection.reauthenticate();
|
|
117
|
+
if (!user) {
|
|
118
|
+
return terminate(connection, 'Session no longer valid');
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (fingerprintUser(user) !== connection.userFingerprint) {
|
|
122
|
+
return terminate(connection, 'User authorization changed');
|
|
123
|
+
}
|
|
124
|
+
if (connection.tenantFingerprint !== undefined) {
|
|
125
|
+
const resolver = (0, core_tenant_context_registry_1.getTenantContextResolver)();
|
|
126
|
+
const tenant = resolver ? await resolver.resolve(user, connection.tenantHeader) : {};
|
|
127
|
+
if (fingerprintTenant(tenant) !== connection.tenantFingerprint) {
|
|
128
|
+
return terminate(connection, 'Tenant access changed');
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (connection.terminated) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
connection.validatedAt = Date.now();
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return terminate(connection, 'Authorization check failed');
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function ensureWsConnectionCurrent(connection) {
|
|
142
|
+
if (connection.terminated) {
|
|
143
|
+
return Promise.resolve(false);
|
|
144
|
+
}
|
|
145
|
+
const interval = configuredRevalidationMs();
|
|
146
|
+
if (!connection.stale && (interval === false || Date.now() - connection.validatedAt < interval)) {
|
|
147
|
+
return Promise.resolve(true);
|
|
148
|
+
}
|
|
149
|
+
if (!connection.inflight) {
|
|
150
|
+
connection.inflight = revalidate(connection).finally(() => {
|
|
151
|
+
connection.inflight = undefined;
|
|
152
|
+
if (connection.stale && !connection.terminated) {
|
|
153
|
+
void ensureWsConnectionCurrent(connection);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
return connection.inflight;
|
|
158
|
+
}
|
|
159
|
+
function noteWsConnectionTenant(connection, tenant) {
|
|
160
|
+
if (connection.terminated) {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
const fingerprint = fingerprintTenant(tenant);
|
|
164
|
+
if (connection.tenantFingerprint === undefined) {
|
|
165
|
+
connection.tenant = tenant ? { ...tenant } : {};
|
|
166
|
+
connection.tenantFingerprint = fingerprint;
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
if (fingerprint !== connection.tenantFingerprint) {
|
|
170
|
+
return terminate(connection, 'Tenant access changed');
|
|
171
|
+
}
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
function markStale(connection) {
|
|
175
|
+
connection.stale = true;
|
|
176
|
+
void ensureWsConnectionCurrent(connection);
|
|
177
|
+
}
|
|
178
|
+
function revalidateWsConnectionsOf(identity) {
|
|
179
|
+
const userId = identity?.userId !== undefined && identity?.userId !== null ? String(identity.userId) : undefined;
|
|
180
|
+
const iamId = identity?.iamId || undefined;
|
|
181
|
+
const email = typeof identity?.email === 'string' ? identity.email.toLowerCase() : undefined;
|
|
182
|
+
if (!userId && !iamId && !email) {
|
|
183
|
+
return 0;
|
|
184
|
+
}
|
|
185
|
+
let count = 0;
|
|
186
|
+
for (const connection of liveConnections) {
|
|
187
|
+
if ((userId && connection.userId === userId) ||
|
|
188
|
+
(iamId && connection.iamId === iamId) ||
|
|
189
|
+
(email && connection.email === email)) {
|
|
190
|
+
markStale(connection);
|
|
191
|
+
count++;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return count;
|
|
195
|
+
}
|
|
196
|
+
function revalidateWsConnectionsForTenant(tenantId) {
|
|
197
|
+
if (!tenantId) {
|
|
198
|
+
return 0;
|
|
199
|
+
}
|
|
200
|
+
const id = String(tenantId);
|
|
201
|
+
let count = 0;
|
|
202
|
+
for (const connection of liveConnections) {
|
|
203
|
+
const tenant = connection.tenant;
|
|
204
|
+
if (tenant && (tenant.tenantId === id || (Array.isArray(tenant.tenantIds) && tenant.tenantIds.includes(id)))) {
|
|
205
|
+
markStale(connection);
|
|
206
|
+
count++;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return count;
|
|
210
|
+
}
|
|
211
|
+
function revalidateAllWsConnections() {
|
|
212
|
+
let count = 0;
|
|
213
|
+
for (const connection of liveConnections) {
|
|
214
|
+
markStale(connection);
|
|
215
|
+
count++;
|
|
216
|
+
}
|
|
217
|
+
return count;
|
|
218
|
+
}
|
|
219
|
+
//# sourceMappingURL=graphql-ws-connection.helper.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"graphql-ws-connection.helper.js","sourceRoot":"","sources":["../../../../src/core/common/helpers/graphql-ws-connection.helper.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { IRequestContext } from '../services/request-context.service';
|
|
2
|
+
import { WsConnection } from './graphql-ws-connection.helper';
|
|
2
3
|
interface GraphQlArgs {
|
|
3
4
|
contextValue?: any;
|
|
4
5
|
[key: string]: any;
|
|
@@ -7,7 +8,7 @@ type ExecuteFn = (args: GraphQlArgs) => any;
|
|
|
7
8
|
export declare function buildWsRequestContext(contextValue: any): Promise<IRequestContext | undefined>;
|
|
8
9
|
export declare function withRequestContextAsyncIterator<T>(iterator: AsyncIterator<T> & {
|
|
9
10
|
[Symbol.asyncIterator]?: () => AsyncIterator<T>;
|
|
10
|
-
}, context: IRequestContext): AsyncIterableIterator<T>;
|
|
11
|
+
}, context: IRequestContext, connection?: WsConnection): AsyncIterableIterator<T>;
|
|
11
12
|
export declare function createRequestContextAwareExecute(execute: ExecuteFn): ExecuteFn;
|
|
12
13
|
export declare function createRequestContextAwareSubscribe(subscribe: ExecuteFn): ExecuteFn;
|
|
13
14
|
export declare function buildRequestContextAwareExecute(): (args: GraphQlArgs) => any;
|
|
@@ -8,22 +8,10 @@ exports.buildRequestContextAwareExecute = buildRequestContextAwareExecute;
|
|
|
8
8
|
exports.buildRequestContextAwareSubscribe = buildRequestContextAwareSubscribe;
|
|
9
9
|
const graphql = require("graphql");
|
|
10
10
|
const core_tenant_context_registry_1 = require("../services/core-tenant-context.registry");
|
|
11
|
-
const config_service_1 = require("../services/config.service");
|
|
12
11
|
const request_context_service_1 = require("../services/request-context.service");
|
|
12
|
+
const graphql_ws_connection_helper_1 = require("./graphql-ws-connection.helper");
|
|
13
13
|
function readTenantHeader(contextValue) {
|
|
14
|
-
|
|
15
|
-
const sources = [contextValue?.headers, contextValue?.connectionParams, contextValue?.request?.headers];
|
|
16
|
-
for (const source of sources) {
|
|
17
|
-
if (!source || typeof source !== 'object') {
|
|
18
|
-
continue;
|
|
19
|
-
}
|
|
20
|
-
for (const [key, value] of Object.entries(source)) {
|
|
21
|
-
if (key.toLowerCase() === headerName && typeof value === 'string' && value) {
|
|
22
|
-
return value;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
return undefined;
|
|
14
|
+
return (0, graphql_ws_connection_helper_1.readWsTenantHeader)(contextValue);
|
|
27
15
|
}
|
|
28
16
|
function readUser(contextValue) {
|
|
29
17
|
return contextValue?.user ?? contextValue?.extra?.user ?? undefined;
|
|
@@ -42,17 +30,50 @@ async function buildWsRequestContext(contextValue) {
|
|
|
42
30
|
}
|
|
43
31
|
const resolver = (0, core_tenant_context_registry_1.getTenantContextResolver)();
|
|
44
32
|
if (resolver) {
|
|
33
|
+
let resolved = {};
|
|
45
34
|
try {
|
|
46
|
-
|
|
35
|
+
resolved = (await resolver.resolve(user, readTenantHeader(contextValue))) ?? {};
|
|
36
|
+
Object.assign(context, resolved);
|
|
47
37
|
}
|
|
48
38
|
catch {
|
|
49
39
|
}
|
|
40
|
+
const connection = (0, graphql_ws_connection_helper_1.getWsConnection)(contextValue);
|
|
41
|
+
if (connection) {
|
|
42
|
+
(0, graphql_ws_connection_helper_1.noteWsConnectionTenant)(connection, resolved);
|
|
43
|
+
}
|
|
50
44
|
}
|
|
51
45
|
return context;
|
|
52
46
|
}
|
|
53
|
-
function
|
|
47
|
+
function refusedResult() {
|
|
48
|
+
return {
|
|
49
|
+
errors: [
|
|
50
|
+
new graphql.GraphQLError('The authorization of this connection changed — reconnect.', {
|
|
51
|
+
extensions: { code: 'FORBIDDEN' },
|
|
52
|
+
}),
|
|
53
|
+
],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
async function connectionStillAuthorized(contextValue) {
|
|
57
|
+
const connection = (0, graphql_ws_connection_helper_1.getWsConnection)(contextValue);
|
|
58
|
+
return connection ? (0, graphql_ws_connection_helper_1.ensureWsConnectionCurrent)(connection) : true;
|
|
59
|
+
}
|
|
60
|
+
function withRequestContextAsyncIterator(iterator, context, connection) {
|
|
61
|
+
const next = (...args) => request_context_service_1.RequestContext.run(context, () => iterator.next(...args));
|
|
54
62
|
const wrapped = {
|
|
55
|
-
next:
|
|
63
|
+
next: connection
|
|
64
|
+
? async (...args) => {
|
|
65
|
+
const result = await next(...args);
|
|
66
|
+
if (result.done || (await (0, graphql_ws_connection_helper_1.ensureWsConnectionCurrent)(connection))) {
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
await request_context_service_1.RequestContext.run(context, () => iterator.return?.());
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
}
|
|
74
|
+
return { done: true, value: undefined };
|
|
75
|
+
}
|
|
76
|
+
: next,
|
|
56
77
|
[Symbol.asyncIterator]() {
|
|
57
78
|
return wrapped;
|
|
58
79
|
},
|
|
@@ -70,21 +91,36 @@ function isAsyncIterable(value) {
|
|
|
70
91
|
}
|
|
71
92
|
function createRequestContextAwareExecute(execute) {
|
|
72
93
|
return async (args) => {
|
|
94
|
+
if (!(await connectionStillAuthorized(args?.contextValue))) {
|
|
95
|
+
return refusedResult();
|
|
96
|
+
}
|
|
73
97
|
const context = await buildWsRequestContext(args?.contextValue);
|
|
74
98
|
if (!context) {
|
|
75
99
|
return execute(args);
|
|
76
100
|
}
|
|
101
|
+
if ((0, graphql_ws_connection_helper_1.getWsConnection)(args?.contextValue)?.terminated) {
|
|
102
|
+
return refusedResult();
|
|
103
|
+
}
|
|
77
104
|
return request_context_service_1.RequestContext.run(context, () => execute(args));
|
|
78
105
|
};
|
|
79
106
|
}
|
|
80
107
|
function createRequestContextAwareSubscribe(subscribe) {
|
|
81
108
|
return async (args) => {
|
|
109
|
+
if (!(await connectionStillAuthorized(args?.contextValue))) {
|
|
110
|
+
return refusedResult();
|
|
111
|
+
}
|
|
82
112
|
const context = await buildWsRequestContext(args?.contextValue);
|
|
83
113
|
if (!context) {
|
|
84
114
|
return subscribe(args);
|
|
85
115
|
}
|
|
116
|
+
const connection = (0, graphql_ws_connection_helper_1.getWsConnection)(args?.contextValue);
|
|
117
|
+
if (connection?.terminated) {
|
|
118
|
+
return refusedResult();
|
|
119
|
+
}
|
|
86
120
|
const result = await request_context_service_1.RequestContext.run(context, () => subscribe(args));
|
|
87
|
-
return isAsyncIterable(result)
|
|
121
|
+
return isAsyncIterable(result)
|
|
122
|
+
? withRequestContextAsyncIterator(result, context, connection)
|
|
123
|
+
: result;
|
|
88
124
|
};
|
|
89
125
|
}
|
|
90
126
|
function buildRequestContextAwareExecute() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graphql-ws-context.helper.js","sourceRoot":"","sources":["../../../../src/core/common/helpers/graphql-ws-context.helper.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"graphql-ws-context.helper.js","sourceRoot":"","sources":["../../../../src/core/common/helpers/graphql-ws-context.helper.ts"],"names":[],"mappings":"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"}
|