@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.
- package/dist/auth/account.controller.d.ts.map +1 -1
- package/dist/auth/account.controller.js +3 -0
- package/dist/auth/auth.controller.d.ts +90 -1
- package/dist/auth/auth.controller.d.ts.map +1 -1
- package/dist/auth/auth.controller.js +245 -7
- package/dist/auth/auth.module.d.ts +21 -0
- package/dist/auth/auth.module.d.ts.map +1 -1
- package/dist/auth/auth.module.js +28 -0
- package/dist/auth/dto.d.ts +32 -0
- package/dist/auth/dto.d.ts.map +1 -1
- package/dist/auth/ports/membership-checker.d.ts +41 -0
- package/dist/auth/ports/membership-checker.d.ts.map +1 -0
- package/dist/auth/ports/membership-checker.js +32 -0
- package/dist/auth/ports/scope-subtree-resolver.d.ts +48 -0
- package/dist/auth/ports/scope-subtree-resolver.d.ts.map +1 -0
- package/dist/auth/ports/scope-subtree-resolver.js +38 -0
- package/dist/auth/tokens.d.ts +25 -0
- package/dist/auth/tokens.d.ts.map +1 -1
- package/dist/auth/tokens.js +26 -1
- package/dist/authentication/mariadb-profile-writer.d.ts.map +1 -1
- package/dist/authentication/mariadb-profile-writer.js +10 -1
- package/dist/authz/list-scope-sql.d.ts +125 -0
- package/dist/authz/list-scope-sql.d.ts.map +1 -0
- package/dist/authz/list-scope-sql.js +477 -0
- package/dist/authz/mariadb-cross-tenant-grant-adapter.d.ts +28 -0
- package/dist/authz/mariadb-cross-tenant-grant-adapter.d.ts.map +1 -1
- package/dist/authz/mariadb-cross-tenant-grant-adapter.js +49 -0
- package/dist/authz/mariadb-list-scope-adapter.d.ts +61 -0
- package/dist/authz/mariadb-list-scope-adapter.d.ts.map +1 -0
- package/dist/authz/mariadb-list-scope-adapter.js +157 -0
- package/dist/authz/mariadb-policy-store.d.ts +118 -0
- package/dist/authz/mariadb-policy-store.d.ts.map +1 -0
- package/dist/authz/mariadb-policy-store.js +225 -0
- package/dist/authz/mariadb-record-rule-adapter.d.ts +7 -3
- package/dist/authz/mariadb-record-rule-adapter.d.ts.map +1 -1
- package/dist/authz/mariadb-record-rule-adapter.js +156 -13
- package/dist/authz/record-share-writer.d.ts +22 -0
- package/dist/authz/record-share-writer.d.ts.map +1 -1
- package/dist/authz/record-share-writer.js +41 -0
- package/dist/authz/wire-record-authorization.d.ts +35 -1
- package/dist/authz/wire-record-authorization.d.ts.map +1 -1
- package/dist/authz/wire-record-authorization.js +75 -0
- package/dist/identity/identity-actions.d.ts +7 -0
- package/dist/identity/identity-actions.d.ts.map +1 -1
- package/dist/identity/identity-actions.js +8 -1
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +47 -3
- package/dist/module/shield.module.d.ts +33 -0
- package/dist/module/shield.module.d.ts.map +1 -1
- package/dist/module/shield.module.js +12 -0
- package/dist/request-auth/request-principal.d.ts.map +1 -1
- package/dist/request-auth/request-principal.js +12 -0
- package/dist/request-auth/session-token-binder.d.ts +35 -1
- package/dist/request-auth/session-token-binder.d.ts.map +1 -1
- package/dist/request-auth/session-token-binder.js +123 -8
- package/dist/scope/closure-builder.d.ts +75 -0
- package/dist/scope/closure-builder.d.ts.map +1 -0
- package/dist/scope/closure-builder.js +134 -0
- package/migrations/1710000000009-CreateRecordPermissionRule.cjs +56 -0
- package/migrations/1710000000014-CreateRecordShare.cjs +49 -0
- package/migrations/1710000000015-CreateScopeClosure.cjs +46 -0
- package/migrations/1710000000016-AlterCompanyParentFkRestrict.cjs +71 -0
- package/package.json +3 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"account.controller.d.ts","sourceRoot":"","sources":["../../src/auth/account.controller.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AAiBH,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAa,MAAM,cAAc,CAAC;AAI9E,OAAO,KAAK,EAEV,kBAAkB,EACnB,MAAM,yCAAyC,CAAC;AAWjD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AACrE,OAAO,KAAK,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC7E,OAAO,KAAK,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACjF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AACxE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AA4DjE,qBACa,iBAAiB;IAO1B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC;IAIxB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC;IAIxB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC;IAMxB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC;IAKzB,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAC;IAM/B,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAC;IAO9B,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAC;IAKjC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAC;IAK/B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC;gBA1CX,MAAM,CAAC,EAAE,kBAAkB,YAAA,EAI3B,MAAM,CAAC,EAAE,cAAc,YAAA,EAIvB,MAAM,CAAC,GAAE,gBAAgB,GAAG,IAAI,aAAA,EAMhC,OAAO,CAAC,GAAE,cAAc,GAAG,IAAI,aAAA,EAK/B,aAAa,CAAC,GAAE,iBAAiB,GAAG,IAAI,aAAA,EAMxC,YAAY,CAAC,GAAE,cAAc,GAAG,IAAI,aAAA,EAOpC,eAAe,CAAC,GAAE,eAAe,GAAG,IAAI,aAAA,EAKxC,aAAa,CAAC,GAAE,aAAa,GAAG,IAAI,aAAA,EAKpC,UAAU,CAAC,GAAE,cAAc,GAAG,IAAI,aAAA;IAGrD;;;;;;OAMG;YACW,mBAAmB;
|
|
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
|
-
|
|
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":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkDG;AAeH,OAAO,KAAK,EACV,aAAa,EAIb,cAAc,EAGd,gBAAgB,EAEjB,MAAM,cAAc,CAAC;
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
|
1
|
+
{"version":3,"file":"auth.module.d.ts","sourceRoot":"","sources":["../../src/auth/auth.module.ts"],"names":[],"mappings":"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"}
|
package/dist/auth/auth.module.js
CHANGED
|
@@ -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
|