@astram/guard 0.5.1 → 0.6.0

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.
Files changed (64) hide show
  1. package/dist/auth/account.controller.d.ts.map +1 -1
  2. package/dist/auth/account.controller.js +3 -0
  3. package/dist/auth/auth.controller.d.ts +90 -1
  4. package/dist/auth/auth.controller.d.ts.map +1 -1
  5. package/dist/auth/auth.controller.js +245 -7
  6. package/dist/auth/auth.module.d.ts +21 -0
  7. package/dist/auth/auth.module.d.ts.map +1 -1
  8. package/dist/auth/auth.module.js +28 -0
  9. package/dist/auth/dto.d.ts +32 -0
  10. package/dist/auth/dto.d.ts.map +1 -1
  11. package/dist/auth/ports/membership-checker.d.ts +41 -0
  12. package/dist/auth/ports/membership-checker.d.ts.map +1 -0
  13. package/dist/auth/ports/membership-checker.js +32 -0
  14. package/dist/auth/ports/scope-subtree-resolver.d.ts +48 -0
  15. package/dist/auth/ports/scope-subtree-resolver.d.ts.map +1 -0
  16. package/dist/auth/ports/scope-subtree-resolver.js +38 -0
  17. package/dist/auth/tokens.d.ts +25 -0
  18. package/dist/auth/tokens.d.ts.map +1 -1
  19. package/dist/auth/tokens.js +26 -1
  20. package/dist/authentication/mariadb-profile-writer.d.ts.map +1 -1
  21. package/dist/authentication/mariadb-profile-writer.js +10 -1
  22. package/dist/authz/list-scope-sql.d.ts +125 -0
  23. package/dist/authz/list-scope-sql.d.ts.map +1 -0
  24. package/dist/authz/list-scope-sql.js +477 -0
  25. package/dist/authz/mariadb-cross-tenant-grant-adapter.d.ts +28 -0
  26. package/dist/authz/mariadb-cross-tenant-grant-adapter.d.ts.map +1 -1
  27. package/dist/authz/mariadb-cross-tenant-grant-adapter.js +49 -0
  28. package/dist/authz/mariadb-list-scope-adapter.d.ts +61 -0
  29. package/dist/authz/mariadb-list-scope-adapter.d.ts.map +1 -0
  30. package/dist/authz/mariadb-list-scope-adapter.js +157 -0
  31. package/dist/authz/mariadb-policy-store.d.ts +118 -0
  32. package/dist/authz/mariadb-policy-store.d.ts.map +1 -0
  33. package/dist/authz/mariadb-policy-store.js +225 -0
  34. package/dist/authz/mariadb-record-rule-adapter.d.ts +7 -3
  35. package/dist/authz/mariadb-record-rule-adapter.d.ts.map +1 -1
  36. package/dist/authz/mariadb-record-rule-adapter.js +156 -13
  37. package/dist/authz/record-share-writer.d.ts +22 -0
  38. package/dist/authz/record-share-writer.d.ts.map +1 -1
  39. package/dist/authz/record-share-writer.js +41 -0
  40. package/dist/authz/wire-record-authorization.d.ts +35 -1
  41. package/dist/authz/wire-record-authorization.d.ts.map +1 -1
  42. package/dist/authz/wire-record-authorization.js +75 -0
  43. package/dist/identity/identity-actions.d.ts +7 -0
  44. package/dist/identity/identity-actions.d.ts.map +1 -1
  45. package/dist/identity/identity-actions.js +8 -1
  46. package/dist/index.d.ts +7 -0
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +47 -3
  49. package/dist/module/shield.module.d.ts +33 -0
  50. package/dist/module/shield.module.d.ts.map +1 -1
  51. package/dist/module/shield.module.js +12 -0
  52. package/dist/request-auth/request-principal.d.ts.map +1 -1
  53. package/dist/request-auth/request-principal.js +12 -0
  54. package/dist/request-auth/session-token-binder.d.ts +35 -1
  55. package/dist/request-auth/session-token-binder.d.ts.map +1 -1
  56. package/dist/request-auth/session-token-binder.js +123 -8
  57. package/dist/scope/closure-builder.d.ts +75 -0
  58. package/dist/scope/closure-builder.d.ts.map +1 -0
  59. package/dist/scope/closure-builder.js +134 -0
  60. package/migrations/1710000000009-CreateRecordPermissionRule.cjs +56 -0
  61. package/migrations/1710000000014-CreateRecordShare.cjs +49 -0
  62. package/migrations/1710000000015-CreateScopeClosure.cjs +46 -0
  63. package/migrations/1710000000016-AlterCompanyParentFkRestrict.cjs +71 -0
  64. package/package.json +3 -3
@@ -1 +1 @@
1
- {"version":3,"file":"account.controller.d.ts","sourceRoot":"","sources":["../../src/auth/account.controller.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"account.controller.d.ts","sourceRoot":"","sources":["../../src/auth/account.controller.ts"],"names":[],"mappings":"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"}
@@ -147,6 +147,9 @@ let AccountController = class AccountController {
147
147
  try {
148
148
  const profile = await this.profileResolver.resolve({
149
149
  id: claims.id,
150
+ // Scope (ADR-0027) — INTERIM self-node default (Task 2.1): non-widening, = today's isolation.
151
+ activeScopeNodeId: claims.tenantId,
152
+ scopeSubtreeNodeIds: [claims.tenantId],
150
153
  tenantId: claims.tenantId,
151
154
  roleIds: [...claims.roleIds],
152
155
  kind: 'user',
@@ -51,6 +51,8 @@
51
51
  */
52
52
  import type { Authenticator, PasswordHasher, SessionAuthority } from '@astram/core';
53
53
  import type { SessionFactsProjector } from './ports/session-facts-projector.js';
54
+ import type { ScopeSubtreeResolver } from './ports/scope-subtree-resolver.js';
55
+ import type { MembershipChecker } from './ports/membership-checker.js';
54
56
  import type { AttemptLimiter } from './rate-limit/attempt-limiter.js';
55
57
  import { PendingMfaStore } from './pending-mfa-store.js';
56
58
  import type { SessionTokenBinder } from '../request-auth/session-token-binder.js';
@@ -62,6 +64,7 @@ import type { CredentialWriter } from './ports/credential-writer.js';
62
64
  import type { SessionSweeper } from './ports/session-sweeper.js';
63
65
  import type { RecoveryCodeStore } from './ports/recovery-code-store.js';
64
66
  import type { AuditSink } from './ports/audit-sink.js';
67
+ import type { MariaDbPermissionFactsAdapter } from '../identity/permission-facts.adapter.js';
65
68
  import type { LoginRequest, LoginResponse, LogoutRequest, MfaRequest, RefreshRequest, SessionSuccess } from './dto.js';
66
69
  /**
67
70
  * The framework-owned auth endpoints. Registered into `AuthModule`'s `controllers`
@@ -90,7 +93,10 @@ export declare class AuthController {
90
93
  private readonly resetLimiter?;
91
94
  private readonly auditSink?;
92
95
  private readonly sessionFactsProjector?;
93
- constructor(authenticator: Authenticator, sessionAuthority: SessionAuthority, pendingMfa: PendingMfaStore, tokenBinder?: SessionTokenBinder | undefined, profileResolver?: ProfileResolver | undefined, credentialResolver?: (CredentialResolver | null) | undefined, resetTokenStore?: (ResetTokenStore | null) | undefined, emailSender?: (EmailSender | null) | undefined, credentialWriter?: (CredentialWriter | null) | undefined, sessionSweeper?: (SessionSweeper | null) | undefined, passwordHasher?: PasswordHasher | undefined, resetLinkBaseUrl?: string | undefined, recoveryCodes?: (RecoveryCodeStore | null) | undefined, loginLimiter?: (AttemptLimiter | null) | undefined, recoveryLimiter?: (AttemptLimiter | null) | undefined, forgotLimiter?: (AttemptLimiter | null) | undefined, forgotHintLimiter?: (AttemptLimiter | null) | undefined, resetLimiter?: (AttemptLimiter | null) | undefined, auditSink?: (AuditSink | null) | undefined, sessionFactsProjector?: (SessionFactsProjector | null) | undefined);
96
+ private readonly scopeSubtreeResolver?;
97
+ private readonly membershipChecker?;
98
+ private readonly permissionFacts?;
99
+ constructor(authenticator: Authenticator, sessionAuthority: SessionAuthority, pendingMfa: PendingMfaStore, tokenBinder?: SessionTokenBinder | undefined, profileResolver?: ProfileResolver | undefined, credentialResolver?: (CredentialResolver | null) | undefined, resetTokenStore?: (ResetTokenStore | null) | undefined, emailSender?: (EmailSender | null) | undefined, credentialWriter?: (CredentialWriter | null) | undefined, sessionSweeper?: (SessionSweeper | null) | undefined, passwordHasher?: PasswordHasher | undefined, resetLinkBaseUrl?: string | undefined, recoveryCodes?: (RecoveryCodeStore | null) | undefined, loginLimiter?: (AttemptLimiter | null) | undefined, recoveryLimiter?: (AttemptLimiter | null) | undefined, forgotLimiter?: (AttemptLimiter | null) | undefined, forgotHintLimiter?: (AttemptLimiter | null) | undefined, resetLimiter?: (AttemptLimiter | null) | undefined, auditSink?: (AuditSink | null) | undefined, sessionFactsProjector?: (SessionFactsProjector | null) | undefined, scopeSubtreeResolver?: (ScopeSubtreeResolver | null) | undefined, membershipChecker?: (MembershipChecker | null) | undefined, permissionFacts?: (MariaDbPermissionFactsAdapter | null) | undefined);
94
100
  /**
95
101
  * Bind the established session's access token to the principal's claims (the
96
102
  * request-auth layer). The access-token TTL is `expiresAtMs - issuedAtMs` from the
@@ -119,6 +125,52 @@ export declare class AuthController {
119
125
  * never a fabricated grant.
120
126
  */
121
127
  private projectFactsFor;
128
+ /**
129
+ * Resolve the principal's ACTIVE-SCOPE SUBTREE for the session binding (Task 2.2, ADR-0027 /
130
+ * ADR-0024), via the OPTIONAL {@link ScopeSubtreeResolver} seam. Returns the closure-resolved
131
+ * node-id set (self node + descendants) to cache, or `undefined` when there is no resolver,
132
+ * the resolution yielded an EMPTY set, OR it threw — every "no subtree" path collapses to
133
+ * `undefined` so the caller omits `scopeSubtreeNodeIds` and the per-request path falls back to
134
+ * the self-node `[tenantId]` (today's flat isolation).
135
+ *
136
+ * FAIL-SAFE + NON-WIDENING: a resolution failure NEVER fails the login and NEVER fabricates or
137
+ * widens a subtree — it degrades to today's self-tenant isolation. The active scope node is the
138
+ * principal's tenant (its `activeScopeNodeId` today); resolving the same way for every principal
139
+ * keeps root's extra cross-tree span in the `isRoot` tenant-gate path (authoritative), not here.
140
+ */
141
+ private resolveScopeSubtree;
142
+ /**
143
+ * Re-resolve the active-scope subtree by node id — used at REFRESH (where only the session's
144
+ * principal id + tenant are in hand, not a full Principal). Same fail-safe/non-widening
145
+ * contract as {@link resolveScopeSubtree}: undefined on no-resolver / empty / throw. This is a
146
+ * SINGLE indexed closure query at refresh time (NOT per request — the hot path stays zero-DB),
147
+ * so a session whose node was RE-PARENTED / had a subtree change (its permission version was
148
+ * bumped by moveNode/deleteNode) re-derives its CORRECT subtree here, closing the
149
+ * stale-subtree-after-move window (bounded by the access-token lifetime — ADR-0011).
150
+ */
151
+ private resolveScopeSubtreeFor;
152
+ /**
153
+ * Compute the caller's OWN auth-facts projection to INLINE into a login/mfa success
154
+ * body — the SAME `projectFor()` the `GET /identity/auth-facts` endpoint uses (no
155
+ * duplicated fact logic). Returns the projection, or `undefined` when there is no
156
+ * projector wired OR the projection failed (a store outage etc.). BEST-EFFORT: a
157
+ * failure here NEVER fails the login — the body simply omits `authFacts` and the
158
+ * frontend falls back to its separate `/identity/auth-facts` fetch (graceful
159
+ * degradation, identical to an older server that never inlines facts). The projection
160
+ * carries `permissionsVersion`, so the SDK's version-staleness contract is preserved:
161
+ * a later poke that finds a newer version refetches before honoring a fact.
162
+ */
163
+ private projectAuthFacts;
164
+ /**
165
+ * Wrap {@link toSessionSuccess} and INLINE the caller's auth-facts projection when it
166
+ * can be computed. Every authenticated login/mfa success routes through here so the
167
+ * permission-gated nav is ready the instant the response lands — one round-trip. The
168
+ * `authFacts` key is included ONLY when a projection was produced (an absent projector
169
+ * / a projection failure leaves it off → frontend fallback fetch). Under
170
+ * exactOptionalPropertyTypes an explicit `undefined` is not assignable to the optional
171
+ * field, so a spread keeps it truly absent.
172
+ */
173
+ private sessionSuccessWithFacts;
122
174
  /**
123
175
  * Build a Principal directly from resolved internal claims (recovery/alt-factor
124
176
  * paths that don't run the full authenticate() loop). Equivalent to the identity
@@ -157,6 +209,43 @@ export declare class AuthController {
157
209
  * throws (store outage) → generic 500 (fail-closed, never a partial profile).
158
210
  */
159
211
  me(authorization?: string): Promise<CurrentUserProfile>;
212
+ /**
213
+ * `POST /auth/scope-switch { targetNodeId }` — a principal with bindings at multiple
214
+ * nodes acts under ONE active scope per session (the active-scope model, ADR-0027). This
215
+ * SWITCHES which node it acts under: it re-resolves the subtree for the new active node,
216
+ * re-projects/strips the facts, and RE-BINDS the current session so STALE authority from
217
+ * the previous scope does NOT carry over (the security point).
218
+ *
219
+ * ── GATED (fail-closed) ─────────────────────────────────────────────────────────────
220
+ * A principal may ONLY switch to a node it HOLDS an ACTIVE `company_membership` at,
221
+ * verified via the OPTIONAL {@link MembershipChecker} seam. `targetNodeId` from the
222
+ * client is NEVER trusted without this affirmative check — that would be the fail-open
223
+ * hole (a principal switching to a foreign node it isn't bound at, inheriting that node's
224
+ * subtree + authority). DENIED (403) when:
225
+ * - the checker is UNWIRED (cannot verify → deny), OR
226
+ * - the checker returns `false` (no active membership at the target), OR
227
+ * - the checker THROWS (store outage → cannot verify → deny).
228
+ * On any of these the OLD binding STANDS — no re-bind runs.
229
+ *
230
+ * ── ROOT ────────────────────────────────────────────────────────────────────────────
231
+ * Root does NOT switch scope. Root is already UNBOUNDED — its cross-tree span comes from
232
+ * `isRoot` in the tenant gate (authoritative), independent of the active-node subtree, so
233
+ * "acting under a different node" is meaningless for it (it already reaches everything).
234
+ * A root scope-switch is a no-op-shaped 403 (`root_no_switch`): consistent + tested, and
235
+ * it keeps the membership gate the SINGLE path a NON-root uses to change reach. This
236
+ * avoids a root re-bind that could NARROW its subtree to one node's closure (a footgun).
237
+ *
238
+ * ── AUTH ────────────────────────────────────────────────────────────────────────────
239
+ * `@Public()` to the classification guard (not object-gated), but it self-authenticates:
240
+ * it resolves the bearer token → the caller's FULL claims (id + sessionId), exactly like
241
+ * the `/account/*` surface. No/invalid/expired/revoked token → 401.
242
+ */
243
+ scopeSwitch(body: {
244
+ targetNodeId?: string;
245
+ }, authorization?: string): Promise<{
246
+ activeScopeNodeId: string;
247
+ scopeSubtreeNodeIds: readonly string[];
248
+ }>;
160
249
  /**
161
250
  * `POST /auth/forgot-password` — request a password-reset link. ALWAYS returns 204
162
251
  * with an identical (empty) body whether or not the hint matches a principal
@@ -1 +1 @@
1
- {"version":3,"file":"auth.controller.d.ts","sourceRoot":"","sources":["../../src/auth/auth.controller.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"auth.controller.d.ts","sourceRoot":"","sources":["../../src/auth/auth.controller.ts"],"names":[],"mappings":"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"}
@@ -178,7 +178,10 @@ let AuthController = class AuthController {
178
178
  resetLimiter;
179
179
  auditSink;
180
180
  sessionFactsProjector;
181
- constructor(authenticator, sessionAuthority, pendingMfa, tokenBinder, profileResolver, credentialResolver, resetTokenStore, emailSender, credentialWriter, sessionSweeper, passwordHasher, resetLinkBaseUrl, recoveryCodes, loginLimiter, recoveryLimiter, forgotLimiter, forgotHintLimiter, resetLimiter, auditSink, sessionFactsProjector) {
181
+ scopeSubtreeResolver;
182
+ membershipChecker;
183
+ permissionFacts;
184
+ constructor(authenticator, sessionAuthority, pendingMfa, tokenBinder, profileResolver, credentialResolver, resetTokenStore, emailSender, credentialWriter, sessionSweeper, passwordHasher, resetLinkBaseUrl, recoveryCodes, loginLimiter, recoveryLimiter, forgotLimiter, forgotHintLimiter, resetLimiter, auditSink, sessionFactsProjector, scopeSubtreeResolver, membershipChecker, permissionFacts) {
182
185
  this.authenticator = authenticator;
183
186
  this.sessionAuthority = sessionAuthority;
184
187
  this.pendingMfa = pendingMfa;
@@ -199,6 +202,9 @@ let AuthController = class AuthController {
199
202
  this.resetLimiter = resetLimiter;
200
203
  this.auditSink = auditSink;
201
204
  this.sessionFactsProjector = sessionFactsProjector;
205
+ this.scopeSubtreeResolver = scopeSubtreeResolver;
206
+ this.membershipChecker = membershipChecker;
207
+ this.permissionFacts = permissionFacts;
202
208
  }
203
209
  /**
204
210
  * Bind the established session's access token to the principal's claims (the
@@ -215,6 +221,13 @@ let AuthController = class AuthController {
215
221
  // / a null result → `cachedFacts` stays undefined → gate 5 DB-reloads (today's
216
222
  // behavior). Best-effort inside projectFacts (a failure → undefined, never throws).
217
223
  const cachedFacts = await this.projectFacts(principal);
224
+ // EAGER active-scope-subtree resolution (Task 2.2, ADR-0027 / ADR-0024): resolve the
225
+ // principal's closure subtree (self node + descendants) NOW so the per-request tenant gate
226
+ // reads it from the binding with zero DB (the parity guarantee — the closure query happens
227
+ // ONLY here, at login). Absent resolver / empty result / a thrown resolver → undefined →
228
+ // `scopeSubtreeNodeIds` stays absent → the per-request path falls back to the self-node.
229
+ // Best-effort inside resolveScopeSubtree (a failure → undefined, never throws).
230
+ const scopeSubtreeNodeIds = await this.resolveScopeSubtree(principal);
218
231
  try {
219
232
  await this.tokenBinder.bind(est.tokens.accessToken, {
220
233
  id: principal.id,
@@ -230,6 +243,10 @@ let AuthController = class AuthController {
230
243
  // exactOptionalPropertyTypes an explicit `undefined` is not assignable to the
231
244
  // optional field; a spread keeps it truly absent (→ gate 5 DB-reloads).
232
245
  ...(cachedFacts ? { cachedFacts } : {}),
246
+ // Same spread discipline for the resolved subtree: present only when the resolver
247
+ // produced a non-empty set (→ the per-request tenant gate reads the real subtree);
248
+ // absent otherwise (→ the per-request path falls back to the self-node, non-widening).
249
+ ...(scopeSubtreeNodeIds ? { scopeSubtreeNodeIds } : {}),
233
250
  }, ttlMs);
234
251
  }
235
252
  catch {
@@ -275,6 +292,87 @@ let AuthController = class AuthController {
275
292
  return null;
276
293
  }
277
294
  }
295
+ /**
296
+ * Resolve the principal's ACTIVE-SCOPE SUBTREE for the session binding (Task 2.2, ADR-0027 /
297
+ * ADR-0024), via the OPTIONAL {@link ScopeSubtreeResolver} seam. Returns the closure-resolved
298
+ * node-id set (self node + descendants) to cache, or `undefined` when there is no resolver,
299
+ * the resolution yielded an EMPTY set, OR it threw — every "no subtree" path collapses to
300
+ * `undefined` so the caller omits `scopeSubtreeNodeIds` and the per-request path falls back to
301
+ * the self-node `[tenantId]` (today's flat isolation).
302
+ *
303
+ * FAIL-SAFE + NON-WIDENING: a resolution failure NEVER fails the login and NEVER fabricates or
304
+ * widens a subtree — it degrades to today's self-tenant isolation. The active scope node is the
305
+ * principal's tenant (its `activeScopeNodeId` today); resolving the same way for every principal
306
+ * keeps root's extra cross-tree span in the `isRoot` tenant-gate path (authoritative), not here.
307
+ */
308
+ async resolveScopeSubtree(principal) {
309
+ return this.resolveScopeSubtreeFor(principal.tenantId);
310
+ }
311
+ /**
312
+ * Re-resolve the active-scope subtree by node id — used at REFRESH (where only the session's
313
+ * principal id + tenant are in hand, not a full Principal). Same fail-safe/non-widening
314
+ * contract as {@link resolveScopeSubtree}: undefined on no-resolver / empty / throw. This is a
315
+ * SINGLE indexed closure query at refresh time (NOT per request — the hot path stays zero-DB),
316
+ * so a session whose node was RE-PARENTED / had a subtree change (its permission version was
317
+ * bumped by moveNode/deleteNode) re-derives its CORRECT subtree here, closing the
318
+ * stale-subtree-after-move window (bounded by the access-token lifetime — ADR-0011).
319
+ */
320
+ async resolveScopeSubtreeFor(tenantId) {
321
+ const resolver = this.scopeSubtreeResolver;
322
+ if (resolver === undefined || resolver === null)
323
+ return undefined;
324
+ try {
325
+ const subtree = await resolver.resolveSubtree(tenantId);
326
+ // A non-array / empty result → omit (→ self-node fallback, non-widening).
327
+ return Array.isArray(subtree) && subtree.length > 0 ? [...subtree] : undefined;
328
+ }
329
+ catch {
330
+ // Best-effort: a closure outage NEVER fails the operation → omit the subtree → the
331
+ // per-request path falls back to the self-node (today's isolation, non-widening).
332
+ return undefined;
333
+ }
334
+ }
335
+ /**
336
+ * Compute the caller's OWN auth-facts projection to INLINE into a login/mfa success
337
+ * body — the SAME `projectFor()` the `GET /identity/auth-facts` endpoint uses (no
338
+ * duplicated fact logic). Returns the projection, or `undefined` when there is no
339
+ * projector wired OR the projection failed (a store outage etc.). BEST-EFFORT: a
340
+ * failure here NEVER fails the login — the body simply omits `authFacts` and the
341
+ * frontend falls back to its separate `/identity/auth-facts` fetch (graceful
342
+ * degradation, identical to an older server that never inlines facts). The projection
343
+ * carries `permissionsVersion`, so the SDK's version-staleness contract is preserved:
344
+ * a later poke that finds a newer version refetches before honoring a fact.
345
+ */
346
+ async projectAuthFacts(principal) {
347
+ const adapter = this.permissionFacts;
348
+ if (adapter === undefined || adapter === null)
349
+ return undefined;
350
+ try {
351
+ return await adapter.projectFor(principal.id, principal.tenantId, principal.isRoot === true);
352
+ }
353
+ catch {
354
+ // Fail-closed → omit `authFacts`; the frontend fetches it separately. Never a
355
+ // login failure, never a fabricated grant.
356
+ return undefined;
357
+ }
358
+ }
359
+ /**
360
+ * Wrap {@link toSessionSuccess} and INLINE the caller's auth-facts projection when it
361
+ * can be computed. Every authenticated login/mfa success routes through here so the
362
+ * permission-gated nav is ready the instant the response lands — one round-trip. The
363
+ * `authFacts` key is included ONLY when a projection was produced (an absent projector
364
+ * / a projection failure leaves it off → frontend fallback fetch). Under
365
+ * exactOptionalPropertyTypes an explicit `undefined` is not assignable to the optional
366
+ * field, so a spread keeps it truly absent.
367
+ */
368
+ async sessionSuccessWithFacts(est, principal, extra) {
369
+ const authFacts = await this.projectAuthFacts(principal);
370
+ return {
371
+ ...toSessionSuccess(est),
372
+ ...(extra?.mfaRecoveryUsed ? { mfaRecoveryUsed: true } : {}),
373
+ ...(authFacts ? { authFacts } : {}),
374
+ };
375
+ }
278
376
  /**
279
377
  * Build a Principal directly from resolved internal claims (recovery/alt-factor
280
378
  * paths that don't run the full authenticate() loop). Equivalent to the identity
@@ -294,7 +392,16 @@ let AuthController = class AuthController {
294
392
  const roleIds = Array.isArray(claims.roleIds)
295
393
  ? claims.roleIds.filter((r) => typeof r === 'string')
296
394
  : [];
297
- return { kind: 'user', id, tenantId, roleIds, isRoot: false };
395
+ // Scope (ADR-0027) — INTERIM self-node default (Task 2.1): non-widening, = today's isolation.
396
+ return {
397
+ kind: 'user',
398
+ id,
399
+ activeScopeNodeId: tenantId,
400
+ scopeSubtreeNodeIds: [tenantId],
401
+ tenantId,
402
+ roleIds,
403
+ isRoot: false,
404
+ };
298
405
  }
299
406
  /**
300
407
  * `POST /auth/login` — verify a password credential and either establish a
@@ -325,7 +432,9 @@ let AuthController = class AuthController {
325
432
  const principal = requirePrincipal(result);
326
433
  const est = await this.establishOrReject(principal);
327
434
  await this.bindSession(est, principal);
328
- return toSessionSuccess(est);
435
+ // Inline the caller's auth-facts so the permission-gated nav is ready the instant
436
+ // login returns (one round-trip). Best-effort — omitted on any projection failure.
437
+ return this.sessionSuccessWithFacts(est, principal);
329
438
  }
330
439
  if (result.outcome === 'mfa_required') {
331
440
  // Password verified, a second factor is outstanding. The PASSWORD was correct,
@@ -417,6 +526,101 @@ let AuthController = class AuthController {
417
526
  }
418
527
  return profile;
419
528
  }
529
+ /**
530
+ * `POST /auth/scope-switch { targetNodeId }` — a principal with bindings at multiple
531
+ * nodes acts under ONE active scope per session (the active-scope model, ADR-0027). This
532
+ * SWITCHES which node it acts under: it re-resolves the subtree for the new active node,
533
+ * re-projects/strips the facts, and RE-BINDS the current session so STALE authority from
534
+ * the previous scope does NOT carry over (the security point).
535
+ *
536
+ * ── GATED (fail-closed) ─────────────────────────────────────────────────────────────
537
+ * A principal may ONLY switch to a node it HOLDS an ACTIVE `company_membership` at,
538
+ * verified via the OPTIONAL {@link MembershipChecker} seam. `targetNodeId` from the
539
+ * client is NEVER trusted without this affirmative check — that would be the fail-open
540
+ * hole (a principal switching to a foreign node it isn't bound at, inheriting that node's
541
+ * subtree + authority). DENIED (403) when:
542
+ * - the checker is UNWIRED (cannot verify → deny), OR
543
+ * - the checker returns `false` (no active membership at the target), OR
544
+ * - the checker THROWS (store outage → cannot verify → deny).
545
+ * On any of these the OLD binding STANDS — no re-bind runs.
546
+ *
547
+ * ── ROOT ────────────────────────────────────────────────────────────────────────────
548
+ * Root does NOT switch scope. Root is already UNBOUNDED — its cross-tree span comes from
549
+ * `isRoot` in the tenant gate (authoritative), independent of the active-node subtree, so
550
+ * "acting under a different node" is meaningless for it (it already reaches everything).
551
+ * A root scope-switch is a no-op-shaped 403 (`root_no_switch`): consistent + tested, and
552
+ * it keeps the membership gate the SINGLE path a NON-root uses to change reach. This
553
+ * avoids a root re-bind that could NARROW its subtree to one node's closure (a footgun).
554
+ *
555
+ * ── AUTH ────────────────────────────────────────────────────────────────────────────
556
+ * `@Public()` to the classification guard (not object-gated), but it self-authenticates:
557
+ * it resolves the bearer token → the caller's FULL claims (id + sessionId), exactly like
558
+ * the `/account/*` surface. No/invalid/expired/revoked token → 401.
559
+ */
560
+ async scopeSwitch(body, authorization) {
561
+ // 1) Authenticate: resolve the bearer token → the caller's FULL claims (need the raw
562
+ // access token + sessionId for the in-place re-bind). Fail-closed on any miss.
563
+ if (this.tokenBinder === undefined) {
564
+ throw new common_1.HttpException({ error: 'unauthorized' }, common_1.HttpStatus.UNAUTHORIZED);
565
+ }
566
+ const token = (0, request_principal_js_1.extractBearerToken)(authorization);
567
+ if (token === null) {
568
+ throw new common_1.HttpException({ error: 'unauthorized' }, common_1.HttpStatus.UNAUTHORIZED);
569
+ }
570
+ const claims = await this.tokenBinder.resolve(token);
571
+ if (claims === null) {
572
+ throw new common_1.HttpException({ error: 'unauthorized' }, common_1.HttpStatus.UNAUTHORIZED);
573
+ }
574
+ const targetNodeId = typeof body?.targetNodeId === 'string' ? body.targetNodeId : '';
575
+ if (targetNodeId === '') {
576
+ throw new common_1.HttpException({ error: 'invalid_target' }, common_1.HttpStatus.BAD_REQUEST);
577
+ }
578
+ // 2) ROOT does not switch (already unbounded; a switch could only NARROW its reach).
579
+ if (claims.isRoot === true) {
580
+ throw new common_1.HttpException({ error: 'root_no_switch' }, common_1.HttpStatus.FORBIDDEN);
581
+ }
582
+ // 3) THE GATE (fail-closed): the principal must hold an ACTIVE membership at the target.
583
+ // Unwired checker → deny (cannot verify). A throw → deny (cannot verify). `false` →
584
+ // deny (no binding). Only an affirmative `true` proceeds. NEVER trust targetNodeId
585
+ // without this check.
586
+ const checker = this.membershipChecker;
587
+ if (checker === undefined || checker === null) {
588
+ throw new common_1.HttpException({ error: 'scope_switch_denied' }, common_1.HttpStatus.FORBIDDEN);
589
+ }
590
+ let bound;
591
+ try {
592
+ bound = await checker.hasActiveMembership(claims.id, targetNodeId);
593
+ }
594
+ catch {
595
+ // Cannot verify the binding (store outage) → fail closed. No re-bind.
596
+ throw new common_1.HttpException({ error: 'scope_switch_denied' }, common_1.HttpStatus.FORBIDDEN);
597
+ }
598
+ if (bound !== true) {
599
+ throw new common_1.HttpException({ error: 'scope_switch_denied' }, common_1.HttpStatus.FORBIDDEN);
600
+ }
601
+ // 4) Re-resolve the subtree for the NEW active node (self-node fallback if unwired /
602
+ // empty / thrown — non-widening, mirrors login). The new subtree REPLACES the old.
603
+ const resolved = await this.resolveScopeSubtree({
604
+ kind: 'user',
605
+ id: claims.id,
606
+ activeScopeNodeId: targetNodeId,
607
+ scopeSubtreeNodeIds: [targetNodeId],
608
+ tenantId: targetNodeId,
609
+ roleIds: [...claims.roleIds],
610
+ isRoot: false,
611
+ });
612
+ const subtree = resolved ?? [targetNodeId];
613
+ // 5) RE-BIND the current session in place (same token — no rotate). The bound tenantId
614
+ // becomes targetNodeId (→ activeScopeNodeId) + scopeSubtreeNodeIds becomes the NEW
615
+ // subtree; the OLD node's subtree + cachedFacts are DROPPED (stale authority gone).
616
+ // A failed re-bind (session gone / Redis down) → deny (fail-closed, the old binding
617
+ // already effectively dead).
618
+ const ok = await this.tokenBinder.rebindActiveScope(token, claims.sessionId, targetNodeId, subtree);
619
+ if (!ok) {
620
+ throw new common_1.HttpException({ error: 'scope_switch_denied' }, common_1.HttpStatus.FORBIDDEN);
621
+ }
622
+ return { activeScopeNodeId: targetNodeId, scopeSubtreeNodeIds: subtree };
623
+ }
420
624
  /**
421
625
  * `POST /auth/forgot-password` — request a password-reset link. ALWAYS returns 204
422
626
  * with an identical (empty) body whether or not the hint matches a principal
@@ -551,6 +755,9 @@ let AuthController = class AuthController {
551
755
  return;
552
756
  const profile = await this.profileResolver.resolve({
553
757
  id: principalId,
758
+ // Scope (ADR-0027) — INTERIM self-node default (Task 2.1): non-widening, = today's isolation.
759
+ activeScopeNodeId: tenantId,
760
+ scopeSubtreeNodeIds: [tenantId],
554
761
  tenantId,
555
762
  roleIds: [],
556
763
  kind: 'user',
@@ -685,7 +892,8 @@ let AuthController = class AuthController {
685
892
  const principal = requirePrincipal(result);
686
893
  const est = await this.establishOrReject(principal);
687
894
  await this.bindSession(est, principal);
688
- return toSessionSuccess(est);
895
+ // Same inline-facts enrichment as the password-login success (one round-trip).
896
+ return this.sessionSuccessWithFacts(est, principal);
689
897
  }
690
898
  // Anything else (wrong code → rejected; still-outstanding → mfa_required) is a
691
899
  // failure. A transient blip is retryable; everything else is a generic 401.
@@ -785,7 +993,11 @@ let AuthController = class AuthController {
785
993
  tenantId: principal.tenantId,
786
994
  notify: true,
787
995
  });
788
- return { ...toSessionSuccess(est), mfaRecoveryUsed: true };
996
+ // Inline auth-facts here too (one round-trip), alongside the recovery-used flag. The
997
+ // session is enforced-re-enroll-restricted, but the frontend still seeds its facts
998
+ // from this projection (fail-closed: `principalFromClaims` forced isRoot:false, so no
999
+ // control-plane keys leak into a recovery session).
1000
+ return this.sessionSuccessWithFacts(est, principal, { mfaRecoveryUsed: true });
789
1001
  }
790
1002
  /**
791
1003
  * Fire-and-forget security notice for a recovery-code login (spec §6.1). Resolves the
@@ -805,6 +1017,9 @@ let AuthController = class AuthController {
805
1017
  void (async () => {
806
1018
  const profile = await resolver.resolve({
807
1019
  id: principalId,
1020
+ // Scope (ADR-0027) — INTERIM self-node default (Task 2.1): non-widening, = today's isolation.
1021
+ activeScopeNodeId: tenantId,
1022
+ scopeSubtreeNodeIds: [tenantId],
808
1023
  tenantId,
809
1024
  roleIds: [],
810
1025
  kind: 'user',
@@ -923,7 +1138,14 @@ let AuthController = class AuthController {
923
1138
  // behavior); a fresh CoarseGrants → replaces it; `null` (projection yielded
924
1139
  // nothing) → strips it (→ next request DB-reloads; fail-closed).
925
1140
  const freshFacts = await this.projectFactsFor(est.session.principalId, est.session.tenantId);
926
- await this.tokenBinder.rebindOnRotate(est.tokens.accessToken, est.session.sessionId, ttlMs, freshFacts);
1141
+ // EAGER subtree RE-RESOLUTION at REFRESH (the stale-subtree-after-move fix): re-read the
1142
+ // principal's closure subtree so a session whose node was re-parented (moveNode/deleteNode
1143
+ // bumped its version) gets its CORRECT, possibly-smaller subtree — REPLACING the stale one
1144
+ // carried from login. One indexed closure query at refresh (NOT per request — parity
1145
+ // preserved). `undefined` (no resolver / empty / threw) → rebindOnRotate carries the
1146
+ // existing subtree forward (fail-safe, non-widening — never fails the refresh).
1147
+ const freshSubtree = await this.resolveScopeSubtreeFor(est.session.tenantId);
1148
+ await this.tokenBinder.rebindOnRotate(est.tokens.accessToken, est.session.sessionId, ttlMs, freshFacts, freshSubtree);
927
1149
  }
928
1150
  return toSessionSuccess(est);
929
1151
  }
@@ -989,6 +1211,16 @@ __decorate([
989
1211
  __metadata("design:paramtypes", [String]),
990
1212
  __metadata("design:returntype", Promise)
991
1213
  ], AuthController.prototype, "me", null);
1214
+ __decorate([
1215
+ (0, classification_decorator_js_1.Public)(),
1216
+ (0, common_1.Post)('scope-switch'),
1217
+ (0, common_1.HttpCode)(common_1.HttpStatus.OK),
1218
+ __param(0, (0, common_1.Body)()),
1219
+ __param(1, (0, common_1.Headers)('authorization')),
1220
+ __metadata("design:type", Function),
1221
+ __metadata("design:paramtypes", [Object, String]),
1222
+ __metadata("design:returntype", Promise)
1223
+ ], AuthController.prototype, "scopeSwitch", null);
992
1224
  __decorate([
993
1225
  (0, classification_decorator_js_1.Public)(),
994
1226
  (0, common_1.Post)('forgot-password'),
@@ -1074,7 +1306,13 @@ exports.AuthController = AuthController = __decorate([
1074
1306
  __param(18, (0, common_1.Inject)(tokens_js_1.SHIELD_AUDIT_SINK)),
1075
1307
  __param(19, (0, common_2.Optional)()),
1076
1308
  __param(19, (0, common_1.Inject)(tokens_js_1.SHIELD_SESSION_FACTS_PROJECTOR)),
1077
- __metadata("design:paramtypes", [Object, Object, pending_mfa_store_js_1.PendingMfaStore, Function, Object, Object, Object, Object, Object, Object, Object, String, Object, Object, Object, Object, Object, Object, Object, Object])
1309
+ __param(20, (0, common_2.Optional)()),
1310
+ __param(20, (0, common_1.Inject)(tokens_js_1.SHIELD_SCOPE_SUBTREE_RESOLVER)),
1311
+ __param(21, (0, common_2.Optional)()),
1312
+ __param(21, (0, common_1.Inject)(tokens_js_1.SHIELD_MEMBERSHIP_CHECKER)),
1313
+ __param(22, (0, common_2.Optional)()),
1314
+ __param(22, (0, common_1.Inject)(tokens_js_1.SHIELD_PERMISSION_FACTS)),
1315
+ __metadata("design:paramtypes", [Object, Object, pending_mfa_store_js_1.PendingMfaStore, Function, Object, Object, Object, Object, Object, Object, Object, String, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object])
1078
1316
  ], AuthController);
1079
1317
  /**
1080
1318
  * Narrow an `authenticated` result to its guaranteed principal. The core engine
@@ -65,6 +65,8 @@
65
65
  import { type DynamicModule } from '@nestjs/common';
66
66
  import type { CredentialResolver, HmacSha1, FactorVerifier } from '@astram/core';
67
67
  import type { SessionFactsProjector } from './ports/session-facts-projector.js';
68
+ import type { ScopeSubtreeResolver } from './ports/scope-subtree-resolver.js';
69
+ import type { MembershipChecker } from './ports/membership-checker.js';
68
70
  import type { ProfileResolver } from './profile-resolver.js';
69
71
  import type { CredentialWriter } from './ports/credential-writer.js';
70
72
  import type { ResetTokenStore } from './ports/reset-token-store.js';
@@ -188,6 +190,25 @@ export interface AuthModuleOptions {
188
190
  * gate 5 serves them from the request's `cachedFacts`. Only meaningful under `auth: true`.
189
191
  */
190
192
  readonly sessionFactsProjector?: SessionFactsProjector;
193
+ /**
194
+ * OPTIONAL active-scope-subtree resolver (Task 2.2, ADR-0027 / ADR-0024, the
195
+ * {@link ScopeSubtreeResolver} port). Threaded exactly like the facts projector:
196
+ * supplied → bound app-wide under {@link SHIELD_SCOPE_SUBTREE_RESOLVER}; omitted → bound
197
+ * `null`, so the auth controller omits `scopeSubtreeNodeIds` and the per-request path
198
+ * falls back to the self-node `[tenantId]` (today's flat isolation). When supplied, login
199
+ * resolves the principal's closure subtree ONCE and caches it on the session binding so the
200
+ * per-request tenant gate reads it with zero DB. Only meaningful under `auth: true`.
201
+ */
202
+ readonly scopeSubtreeResolver?: ScopeSubtreeResolver;
203
+ /**
204
+ * OPTIONAL scope-switch membership checker (Task 5.2, ADR-0027 / ADR-0024, the
205
+ * {@link MembershipChecker} port). Threaded exactly like the subtree resolver: supplied →
206
+ * bound app-wide under {@link SHIELD_MEMBERSHIP_CHECKER}; omitted → bound `null`, so
207
+ * `POST /auth/scope-switch` fails CLOSED (cannot verify the target binding → deny). When
208
+ * supplied, the scope-switch gate confirms the principal holds an active membership at the
209
+ * target node before re-binding the session. Only meaningful under `auth: true`.
210
+ */
211
+ readonly membershipChecker?: MembershipChecker;
191
212
  }
192
213
  /**
193
214
  * The auth composition module. Imports {@link PersistenceModule} (the live
@@ -1 +1 @@
1
- {"version":3,"file":"auth.module.d.ts","sourceRoot":"","sources":["../../src/auth/auth.module.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+DG;AAEH,OAAO,EAAU,KAAK,aAAa,EAAiB,MAAM,gBAAgB,CAAC;AAa3E,OAAO,KAAK,EAGV,kBAAkB,EAOlB,QAAQ,EACR,cAAc,EACf,MAAM,cAAc,CAAC;AA8DtB,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,oCAAoC,CAAC;AAEhF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AACrE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AACpE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AACxE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAClE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAIvD;;;;;GAKG;AACH,eAAO,MAAM,QAAQ,EAAE,QACqE,CAAC;AAE7F;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,cAAc,GAAG,IAAI,GAAG,SAAS,EAC3C,eAAe,EAAE,cAAc,GAC9B,cAAc,CAEhB;AAED;;;;;;;;GAQG;AACH,wBAAsB,uBAAuB,IAAI,OAAO,CAAC,cAAc,CAAC,CAGvE;AAiVD;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC;;;;;;OAMG;IACH,QAAQ,CAAC,kBAAkB,CAAC,EAAE,kBAAkB,CAAC;IACjD;;;;;;OAMG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,eAAe,CAAC;IAC3C;;;;;;OAMG;IACH,QAAQ,CAAC,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;IAC7C,QAAQ,CAAC,eAAe,CAAC,EAAE,eAAe,CAAC;IAC3C,QAAQ,CAAC,WAAW,CAAC,EAAE,WAAW,CAAC;IACnC,QAAQ,CAAC,iBAAiB,CAAC,EAAE,iBAAiB,CAAC;IAC/C,QAAQ,CAAC,aAAa,CAAC,EAAE,aAAa,CAAC;IACvC,QAAQ,CAAC,cAAc,CAAC,EAAE,cAAc,CAAC;IACzC;;;;;;OAMG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC;IAC/B;;;;;OAKG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,cAAc,CAAC;IACzC;;;;;;;;OAQG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,cAAc,CAAC;IACvC;;;;;;;;OAQG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC;;oGAEgG;IAChG,QAAQ,CAAC,kBAAkB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAChD,uGAAuG;IACvG,QAAQ,CAAC,wBAAwB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACtD;;;;;;;OAOG;IACH,QAAQ,CAAC,qBAAqB,CAAC,EAAE,qBAAqB,CAAC;CACxD;AAuHD;;;;;;GAMG;AACH,qBACa,UAAU;IACrB;;;;;OAKG;IACH,MAAM,CAAC,OAAO,CAAC,OAAO,GAAE,iBAAsB,GAAG,aAAa;CAkH/D"}
1
+ {"version":3,"file":"auth.module.d.ts","sourceRoot":"","sources":["../../src/auth/auth.module.ts"],"names":[],"mappings":"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"}
@@ -556,6 +556,26 @@ function makeAuditSinkProvider(override) {
556
556
  function makeSessionFactsProjectorProvider(override) {
557
557
  return { provide: tokens_js_1.SHIELD_SESSION_FACTS_PROJECTOR, useValue: override ?? null };
558
558
  }
559
+ /**
560
+ * Bind the OPTIONAL active-scope-subtree resolver (Task 2.2). Same value-provider pattern
561
+ * as {@link makeSessionFactsProjectorProvider}: the supplied impl when present, else `null`
562
+ * so the auth controller's `@Optional()` injection resolves cleanly to "no resolver" (→ it
563
+ * omits `scopeSubtreeNodeIds` → the per-request path falls back to the self-node). Always
564
+ * provided so the DI token exists.
565
+ */
566
+ function makeScopeSubtreeResolverProvider(override) {
567
+ return { provide: tokens_js_1.SHIELD_SCOPE_SUBTREE_RESOLVER, useValue: override ?? null };
568
+ }
569
+ /**
570
+ * Bind the OPTIONAL scope-switch membership checker (Task 5.2). Same value-provider pattern
571
+ * as {@link makeScopeSubtreeResolverProvider}: the supplied impl when present, else `null` so
572
+ * the auth controller's `@Optional()` injection resolves to "no checker" (→ `POST
573
+ * /auth/scope-switch` fails closed, denying every switch — cannot verify the binding). Always
574
+ * provided so the DI token exists.
575
+ */
576
+ function makeMembershipCheckerProvider(override) {
577
+ return { provide: tokens_js_1.SHIELD_MEMBERSHIP_CHECKER, useValue: override ?? null };
578
+ }
559
579
  /**
560
580
  * The reset-link base URL the forgot-password handler builds the emailed link from
561
581
  * (`${base}?token=<rawToken>`). A plain string VALUE (not a seam) resolved from the
@@ -625,6 +645,14 @@ let AuthModule = AuthModule_1 = class AuthModule {
625
645
  // The eager session-facts projector (ADR-0011; null when absent → the auth
626
646
  // controller omits cachedFacts → gate 5 DB-reloads). Consumed by AuthController.
627
647
  makeSessionFactsProjectorProvider(options.sessionFactsProjector),
648
+ // The active-scope-subtree resolver (Task 2.2; null when absent → the auth controller
649
+ // omits scopeSubtreeNodeIds → the per-request path falls back to the self-node).
650
+ // Consumed by AuthController at login.
651
+ makeScopeSubtreeResolverProvider(options.scopeSubtreeResolver),
652
+ // The scope-switch membership gate (Task 5.2; null when absent → POST /auth/scope-switch
653
+ // fails closed, denying every switch — cannot verify the target binding). Consumed by
654
+ // AuthController's scopeSwitch handler.
655
+ makeMembershipCheckerProvider(options.membershipChecker),
628
656
  // The framework-DEFAULT permission-facts adapter backing AuthFactsController
629
657
  // (`/identity/auth-facts` + `/identity/permissions-version`). Reads role grants +
630
658
  // per-tenant version from the pool; expands `identity:*` wildcards against the