@astram/guard 0.5.0 → 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
package/dist/index.js CHANGED
@@ -16,9 +16,9 @@
16
16
  * `@nestjs/core`, `reflect-metadata`, `rxjs` so the host owns one copy of each.
17
17
  */
18
18
  Object.defineProperty(exports, "__esModule", { value: true });
19
- exports.MariaDbCrossTenantGrantAdapter = exports.makeShareDeleteCascade = exports.ShareStoreUnavailableError = exports.ShareNotPermittedError = exports.SHIELD_RECORD_SHARE_WRITER = exports.RecordShareWriter = exports.SHIELD_RECORD_SHARE_PORT = exports.MariaDbRecordShareAdapter = exports.SHIELD_RECORD_RULE_PORT = exports.MariaDbRecordPermissionRuleAdapter = exports.SHIELD_CLOCK = exports.SHIELD_REFRESH_HASHER = exports.SHIELD_TOKEN_GENERATOR = exports.systemClockProvider = exports.refreshHasherProvider = exports.cryptoTokenGeneratorProvider = exports.SystemClock = exports.Sha256RefreshTokenHasher = exports.CryptoTokenGenerator = exports.SHIELD_HOT_SESSION_STORE = exports.RedisHotSessionStore = exports.SHIELD_SESSION_RECORD_STORE = exports.MariaDbSessionRecordStore = exports.SHIELD_PASSWORD_HASHER = exports.argon2idHasherProvider = exports.Argon2idPasswordHasher = exports.stubDecisionAuthority = exports.GlobalEnforcementGuard = exports.NO_SESSION_FACTS = exports.SHIELD_SESSION_FACTS_ALS = exports.extractBearerToken = exports.principalFromClaims = exports.resolveRequestPrincipal = exports.SessionTokenBinder = exports.SHIELD_SESSION_TOKEN_BINDER = exports.CatalogGatabilityError = exports.CatalogReconciler = exports.OperationInventoryScanner = exports.normalizeClassification = exports.DEFAULT_CLASSIFIED_ACTION = exports.CLASSIFICATION_METADATA = exports.Classified = exports.Public = exports.SHIELD_DECISION_AUTHORITY = exports.ShieldModule = exports.actionsFor = exports.IDENTITY_ACTIONS = exports.MODULE_KEY = exports.buildIdentityModuleRegistry = exports.guardMigrationsDir = void 0;
20
- exports.buildOtpauthUri = exports.generateTotpSecret = exports.PendingMfaStore = exports.AuthController = exports.SHIELD_SESSION_FACTS_PROJECTOR = exports.hmacSha1 = exports.resolveTotpVerifier = exports.makeDefaultTotpVerifier = exports.AuthModule = exports.PersistenceModule = exports.MariaDbCredentialResolver = exports.SHIELD_TOTP_VERIFIER_OVERRIDE = exports.SHIELD_MFA_DEVICE_STORE = exports.SHIELD_AUDIT_SINK = exports.SHIELD_SESSION_SWEEPER = exports.SHIELD_PROFILE_WRITER = exports.SHIELD_RECOVERY_CODE_STORE = exports.SHIELD_EMAIL_SENDER = exports.SHIELD_RESET_TOKEN_STORE = exports.SHIELD_CREDENTIAL_WRITER = exports.SHIELD_SESSION_AUTHORITY = exports.SHIELD_AUTHENTICATOR = exports.SHIELD_PROFILE_RESOLVER = exports.SHIELD_CREDENTIAL_RESOLVER = exports.SHIELD_REDIS = exports.SHIELD_DB_POOL = exports.DEFAULT_REDIS_URL = exports.DEFAULT_DB_CONFIG = exports.assertRefreshHmacKey = exports.loadRefreshHmacKey = exports.loadSessionLifetimes = exports.loadRedisConfig = exports.loadDbConfig = exports.SCHEMA_SVC_POOL_HARD_CAP = exports.DEFAULT_POOL_BUDGET = exports.evaluatePoolBudget = exports.loadPoolBudget = exports.loadAppConfig = exports.APP_CONFIG = exports.assertPoolBudget = exports.BootSequence = exports.buildRecordAuthorization = exports.SHIELD_OPERATION_AUTHORITY_PORT = exports.ManifestOperationAuthorityAdapter = exports.GrantStoreUnavailableError = exports.InvalidGrantError = exports.GrantNotPermittedError = exports.SHIELD_CROSS_TENANT_GRANT_WRITER = exports.CrossTenantGrantWriter = exports.SHIELD_CROSS_TENANT_GRANT_PORT = void 0;
21
- exports.AccountController = exports.generateRecoveryCodes = void 0;
19
+ exports.SHIELD_RECORD_SHARE_PORT = exports.MariaDbRecordShareAdapter = exports.UnknownScopeFieldError = exports.translateListScope = exports.SHIELD_LIST_SCOPE_PORT = exports.MariaDbListScopeAdapter = exports.SHIELD_POLICY_STORE = exports.MariaDbPolicyStore = exports.SHIELD_RECORD_RULE_PORT = exports.MariaDbRecordPermissionRuleAdapter = exports.SHIELD_CLOCK = exports.SHIELD_REFRESH_HASHER = exports.SHIELD_TOKEN_GENERATOR = exports.systemClockProvider = exports.refreshHasherProvider = exports.cryptoTokenGeneratorProvider = exports.SystemClock = exports.Sha256RefreshTokenHasher = exports.CryptoTokenGenerator = exports.SHIELD_HOT_SESSION_STORE = exports.RedisHotSessionStore = exports.SHIELD_SESSION_RECORD_STORE = exports.MariaDbSessionRecordStore = exports.SHIELD_PASSWORD_HASHER = exports.argon2idHasherProvider = exports.Argon2idPasswordHasher = exports.stubDecisionAuthority = exports.GlobalEnforcementGuard = exports.NO_SESSION_FACTS = exports.SHIELD_SESSION_FACTS_ALS = exports.extractBearerToken = exports.principalFromClaims = exports.resolveRequestPrincipal = exports.SessionTokenBinder = exports.SHIELD_SESSION_TOKEN_BINDER = exports.CatalogGatabilityError = exports.CatalogReconciler = exports.OperationInventoryScanner = exports.normalizeClassification = exports.DEFAULT_CLASSIFIED_ACTION = exports.CLASSIFICATION_METADATA = exports.Classified = exports.Public = exports.SHIELD_DECISION_AUTHORITY = exports.ShieldModule = exports.actionsFor = exports.IDENTITY_ACTIONS = exports.MODULE_KEY = exports.buildIdentityModuleRegistry = exports.guardMigrationsDir = void 0;
20
+ exports.makeDefaultTotpVerifier = exports.AuthModule = exports.PersistenceModule = exports.MariaDbCredentialResolver = exports.SHIELD_TOTP_VERIFIER_OVERRIDE = exports.SHIELD_MFA_DEVICE_STORE = exports.SHIELD_AUDIT_SINK = exports.SHIELD_SESSION_SWEEPER = exports.SHIELD_PROFILE_WRITER = exports.SHIELD_RECOVERY_CODE_STORE = exports.SHIELD_EMAIL_SENDER = exports.SHIELD_RESET_TOKEN_STORE = exports.SHIELD_CREDENTIAL_WRITER = exports.SHIELD_SESSION_AUTHORITY = exports.SHIELD_AUTHENTICATOR = exports.SHIELD_PROFILE_RESOLVER = exports.SHIELD_CREDENTIAL_RESOLVER = exports.SHIELD_REDIS = exports.SHIELD_DB_POOL = exports.DEFAULT_REDIS_URL = exports.DEFAULT_DB_CONFIG = exports.assertRefreshHmacKey = exports.loadRefreshHmacKey = exports.loadSessionLifetimes = exports.loadRedisConfig = exports.loadDbConfig = exports.SCHEMA_SVC_POOL_HARD_CAP = exports.DEFAULT_POOL_BUDGET = exports.evaluatePoolBudget = exports.loadPoolBudget = exports.loadAppConfig = exports.APP_CONFIG = exports.assertPoolBudget = exports.BootSequence = exports.buildListScopeFn = exports.buildRecordAuthorization = exports.SHIELD_OPERATION_AUTHORITY_PORT = exports.ManifestOperationAuthorityAdapter = exports.GrantStoreUnavailableError = exports.InvalidGrantError = exports.GrantNotPermittedError = exports.SHIELD_CROSS_TENANT_GRANT_WRITER = exports.CrossTenantGrantWriter = exports.SHIELD_CROSS_TENANT_GRANT_PORT = exports.MariaDbCrossTenantGrantAdapter = exports.makeShareDeleteCascade = exports.ShareStoreUnavailableError = exports.ShareNotPermittedError = exports.SHIELD_RECORD_SHARE_WRITER = exports.RecordShareWriter = void 0;
21
+ exports.ScopeUnknownParentError = exports.ScopeDepthError = exports.ScopeCycleError = exports.buildClosureRows = exports.AccountController = exports.generateRecoveryCodes = exports.buildOtpauthUri = exports.generateTotpSecret = exports.PendingMfaStore = exports.AuthController = exports.SHIELD_SCOPE_SUBTREE_RESOLVER = exports.SHIELD_SESSION_FACTS_PROJECTOR = exports.hmacSha1 = exports.resolveTotpVerifier = void 0;
22
22
  // Framework authz migrations (shield_authz DDL — permission / role_permission /
23
23
  // authz_permission_version). Absolute path a consumer's `@astram/migrate` setup merges in.
24
24
  var migrations_dir_js_1 = require("./migrations-dir.js");
@@ -127,6 +127,30 @@ Object.defineProperty(exports, "SHIELD_CLOCK", { enumerable: true, get: function
127
127
  var mariadb_record_rule_adapter_js_1 = require("./authz/mariadb-record-rule-adapter.js");
128
128
  Object.defineProperty(exports, "MariaDbRecordPermissionRuleAdapter", { enumerable: true, get: function () { return mariadb_record_rule_adapter_js_1.MariaDbRecordPermissionRuleAdapter; } });
129
129
  Object.defineProperty(exports, "SHIELD_RECORD_RULE_PORT", { enumerable: true, get: function () { return mariadb_record_rule_adapter_js_1.SHIELD_RECORD_RULE_PORT; } });
130
+ // The guided-policy AUTHOR store (ADR-0003 / design Task 0.3): the concrete
131
+ // `PolicyStore` (core admin port) over the SAME `record_permission_rule` table the
132
+ // rule adapter enforces — author-what-enforces. Persists a `GuidedPolicy`'s N
133
+ // AND-composed conditions as N rows sharing one `rpr_policy_uuid` group id (the read
134
+ // adapter AND-groups by it). `everyone` subject (v1 mapping); canonical operator
135
+ // names; parameterized SQL; no hard-coded UUIDs. NOT wired into buildRecordAuthorization
136
+ // here — the admin authoring surface (Phase 5) consumes it.
137
+ var mariadb_policy_store_js_1 = require("./authz/mariadb-policy-store.js");
138
+ Object.defineProperty(exports, "MariaDbPolicyStore", { enumerable: true, get: function () { return mariadb_policy_store_js_1.MariaDbPolicyStore; } });
139
+ Object.defineProperty(exports, "SHIELD_POLICY_STORE", { enumerable: true, get: function () { return mariadb_policy_store_js_1.SHIELD_POLICY_STORE; } });
140
+ // The LIST-SCOPE port adapter + the ScopeSpec→SQL translator (ADR-0003 / ADR-0005 list
141
+ // form / Task 2.1). The adapter ({@link MariaDbListScopeAdapter}, `ListScopePort`) projects
142
+ // the SAME `record_permission_rule` rows the rule adapter enforces into abstract allow/deny
143
+ // {@link ScopeCondition} AND-groups; the PURE translator ({@link translateListScope}) turns
144
+ // core's {@link ListScopeSpec} into a SAFE, parameterized WHERE fragment (every VALUE a `?`
145
+ // bound param; ONLY registry-derived columns interpolated; unknown field ⇒ throw; negative
146
+ // operators fail closed on NULL; deny-wins as `AND NOT (...)`). Phase 3 injects the fragment
147
+ // into the engine's `list()`. Store outage ⇒ AuthorizationUnavailableError (never unbounded).
148
+ var mariadb_list_scope_adapter_js_1 = require("./authz/mariadb-list-scope-adapter.js");
149
+ Object.defineProperty(exports, "MariaDbListScopeAdapter", { enumerable: true, get: function () { return mariadb_list_scope_adapter_js_1.MariaDbListScopeAdapter; } });
150
+ Object.defineProperty(exports, "SHIELD_LIST_SCOPE_PORT", { enumerable: true, get: function () { return mariadb_list_scope_adapter_js_1.SHIELD_LIST_SCOPE_PORT; } });
151
+ var list_scope_sql_js_1 = require("./authz/list-scope-sql.js");
152
+ Object.defineProperty(exports, "translateListScope", { enumerable: true, get: function () { return list_scope_sql_js_1.translateListScope; } });
153
+ Object.defineProperty(exports, "UnknownScopeFieldError", { enumerable: true, get: function () { return list_scope_sql_js_1.UnknownScopeFieldError; } });
130
154
  // The per-record SHARE adapter (ADR-0003 / design §6): the concrete `RecordSharePort` over
131
155
  // the `record_share` table. Answers `hasShare` (user- OR role-granted shares, exact-or-`*`
132
156
  // action). No share ⇒ false; store outage ⇒ AuthorizationUnavailableError. Injected into
@@ -196,6 +220,13 @@ Object.defineProperty(exports, "SHIELD_OPERATION_AUTHORITY_PORT", { enumerable:
196
220
  // enforcement-only path core-free.
197
221
  var wire_record_authorization_js_1 = require("./authz/wire-record-authorization.js");
198
222
  Object.defineProperty(exports, "buildRecordAuthorization", { enumerable: true, get: function () { return wire_record_authorization_js_1.buildRecordAuthorization; } });
223
+ // The LIST-FORM sibling (Task 2.2): the engine-facing `ListScopeFn` the module wiring hands
224
+ // to the engine's `list()` (Task 3.1). `buildListScopeFn` is a SEPARATE factory (Option A)
225
+ // that takes the SAME `RecordAuthorizationInputs` and constructs its `MariaDbListScopeAdapter`
226
+ // over the SAME un-overridable `inputs.pool` — the adapter is NOT a field of the inputs, so a
227
+ // consumer cannot inject a fail-open scope port. Fail-closed: a transient throw PROPAGATES.
228
+ var wire_record_authorization_js_2 = require("./authz/wire-record-authorization.js");
229
+ Object.defineProperty(exports, "buildListScopeFn", { enumerable: true, get: function () { return wire_record_authorization_js_2.buildListScopeFn; } });
199
230
  // Boot infrastructure (ADR-0018 connection-pool budget assertion). `BootModule`
200
231
  // itself is NOT exported — it is composed internally by `ShieldModule.forRoot()`.
201
232
  var boot_sequence_js_1 = require("./boot/boot-sequence.js");
@@ -279,6 +310,8 @@ Object.defineProperty(exports, "resolveTotpVerifier", { enumerable: true, get: f
279
310
  Object.defineProperty(exports, "hmacSha1", { enumerable: true, get: function () { return auth_module_js_2.hmacSha1; } });
280
311
  var tokens_js_3 = require("./auth/tokens.js");
281
312
  Object.defineProperty(exports, "SHIELD_SESSION_FACTS_PROJECTOR", { enumerable: true, get: function () { return tokens_js_3.SHIELD_SESSION_FACTS_PROJECTOR; } });
313
+ var tokens_js_4 = require("./auth/tokens.js");
314
+ Object.defineProperty(exports, "SHIELD_SCOPE_SUBTREE_RESOLVER", { enumerable: true, get: function () { return tokens_js_4.SHIELD_SCOPE_SUBTREE_RESOLVER; } });
282
315
  // The framework-owned `/auth/*` HTTP controller (Team C) + its pending-MFA store.
283
316
  // The controller CONSUMES SHIELD_AUTHENTICATOR / SHIELD_SESSION_AUTHORITY and the
284
317
  // PendingMfaStore. It is EXPORTED so AuthModule (Team B) / the integration owner
@@ -301,3 +334,14 @@ Object.defineProperty(exports, "generateRecoveryCodes", { enumerable: true, get:
301
334
  // later). Registered by AuthModule alongside AuthController under `auth: true`.
302
335
  var account_controller_js_1 = require("./auth/account.controller.js");
303
336
  Object.defineProperty(exports, "AccountController", { enumerable: true, get: function () { return account_controller_js_1.AccountController; } });
337
+ // The PURE scope-closure builder (ADR-0027 / Phase 1 Task 1.1 of the scoped-hierarchy
338
+ // tenancy program): turns a node forest's parent edges into the flat ancestor/descendant/
339
+ // depth rows the `shield_authz.scope_closure` table stores. No I/O — the Task 1.2
340
+ // reconciler is the one that calls this against real edges and writes the rows back.
341
+ // `ScopeCycleError` / `ScopeDepthError` / `ScopeUnknownParentError` are the fail-loud
342
+ // exceptions a caller (the reconciler) must handle as data-corruption signals, never swallow.
343
+ var closure_builder_js_1 = require("./scope/closure-builder.js");
344
+ Object.defineProperty(exports, "buildClosureRows", { enumerable: true, get: function () { return closure_builder_js_1.buildClosureRows; } });
345
+ Object.defineProperty(exports, "ScopeCycleError", { enumerable: true, get: function () { return closure_builder_js_1.ScopeCycleError; } });
346
+ Object.defineProperty(exports, "ScopeDepthError", { enumerable: true, get: function () { return closure_builder_js_1.ScopeDepthError; } });
347
+ Object.defineProperty(exports, "ScopeUnknownParentError", { enumerable: true, get: function () { return closure_builder_js_1.ScopeUnknownParentError; } });
@@ -11,6 +11,8 @@ import type { SessionSweeper } from '../auth/ports/session-sweeper.js';
11
11
  import type { MfaDeviceStore } from '../auth/ports/mfa-device-store.js';
12
12
  import type { AuditSink } from '../auth/ports/audit-sink.js';
13
13
  import type { SessionFactsProjector } from '../auth/ports/session-facts-projector.js';
14
+ import type { ScopeSubtreeResolver } from '../auth/ports/scope-subtree-resolver.js';
15
+ import type { MembershipChecker } from '../auth/ports/membership-checker.js';
14
16
  import type { CoreDecisionAuthority } from '../enforcement/core-seam.js';
15
17
  import { type AppConfig } from '../config/app-config.js';
16
18
  import { SHIELD_DECISION_AUTHORITY } from './shield-tokens.js';
@@ -156,6 +158,25 @@ export interface ShieldModuleOptions {
156
158
  * `cachedFacts`. Only consulted when `auth: true`.
157
159
  */
158
160
  readonly sessionFactsProjector?: SessionFactsProjector;
161
+ /**
162
+ * OPTIONAL active-scope-subtree resolver (Task 2.2, ADR-0027 / ADR-0024) — see
163
+ * {@link AuthModuleOptions.scopeSubtreeResolver}. Threaded straight through to
164
+ * {@link AuthModule.forRoot} (bound under {@link SHIELD_SCOPE_SUBTREE_RESOLVER}, or `null` when
165
+ * absent → the auth controller omits `scopeSubtreeNodeIds` and the per-request path falls back to
166
+ * the self-node `[tenantId]`, today's flat isolation). When supplied, login resolves the
167
+ * principal's closure subtree ONCE and caches it on the session binding so the per-request tenant
168
+ * gate reads the real subtree with zero DB. Only consulted when `auth: true`.
169
+ */
170
+ readonly scopeSubtreeResolver?: ScopeSubtreeResolver;
171
+ /**
172
+ * OPTIONAL scope-switch membership checker (Task 5.2, ADR-0027 / ADR-0024) — see
173
+ * {@link AuthModuleOptions.membershipChecker}. Threaded straight through to
174
+ * {@link AuthModule.forRoot} (bound under {@link SHIELD_MEMBERSHIP_CHECKER}, or `null` when
175
+ * absent → `POST /auth/scope-switch` fails closed, denying every switch — cannot verify the
176
+ * target binding). When supplied, the scope-switch gate confirms the principal holds an active
177
+ * membership at the target node before re-binding the session. Only consulted when `auth: true`.
178
+ */
179
+ readonly membershipChecker?: MembershipChecker;
159
180
  }
160
181
  /** Async variant of {@link ShieldModuleOptions} (DI-computed config). */
161
182
  export interface ShieldModuleAsyncOptions {
@@ -248,6 +269,18 @@ export interface ShieldModuleAsyncOptions {
248
269
  * options. Only consulted when `auth: true`.
249
270
  */
250
271
  readonly sessionFactsProjector?: SessionFactsProjector;
272
+ /**
273
+ * OPTIONAL active-scope-subtree resolver — see {@link ShieldModuleOptions.scopeSubtreeResolver}.
274
+ * STATIC here (like the other seams): a registration-time collaborator threaded into the
275
+ * statically-composed {@link AuthModule} import. Only consulted when `auth: true`.
276
+ */
277
+ readonly scopeSubtreeResolver?: ScopeSubtreeResolver;
278
+ /**
279
+ * OPTIONAL scope-switch membership checker — see {@link ShieldModuleOptions.membershipChecker}.
280
+ * STATIC here (like the other seams): a registration-time collaborator threaded into the
281
+ * statically-composed {@link AuthModule} import. Only consulted when `auth: true`.
282
+ */
283
+ readonly membershipChecker?: MembershipChecker;
251
284
  }
252
285
  /**
253
286
  * `ShieldModule` — the SINGLE registration seam (ADR-0023 §4).
@@ -1 +1 @@
1
- {"version":3,"file":"shield.module.d.ts","sourceRoot":"","sources":["../../src/module/shield.module.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"shield.module.d.ts","sourceRoot":"","sources":["../../src/module/shield.module.ts"],"names":[],"mappings":"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"}
@@ -197,6 +197,12 @@ let ShieldModule = ShieldModule_1 = class ShieldModule {
197
197
  ...(options.sessionFactsProjector
198
198
  ? { sessionFactsProjector: options.sessionFactsProjector }
199
199
  : {}),
200
+ ...(options.scopeSubtreeResolver
201
+ ? { scopeSubtreeResolver: options.scopeSubtreeResolver }
202
+ : {}),
203
+ ...(options.membershipChecker
204
+ ? { membershipChecker: options.membershipChecker }
205
+ : {}),
200
206
  })
201
207
  : undefined;
202
208
  return {
@@ -308,6 +314,12 @@ let ShieldModule = ShieldModule_1 = class ShieldModule {
308
314
  ...(options.sessionFactsProjector
309
315
  ? { sessionFactsProjector: options.sessionFactsProjector }
310
316
  : {}),
317
+ ...(options.scopeSubtreeResolver
318
+ ? { scopeSubtreeResolver: options.scopeSubtreeResolver }
319
+ : {}),
320
+ ...(options.membershipChecker
321
+ ? { membershipChecker: options.membershipChecker }
322
+ : {}),
311
323
  })
312
324
  : undefined;
313
325
  return {
@@ -1 +1 @@
1
- {"version":3,"file":"request-principal.d.ts","sourceRoot":"","sources":["../../src/request-auth/request-principal.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,KAAK,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AAK1F,6EAA6E;AAC7E,wBAAgB,kBAAkB,CAAC,mBAAmB,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAK9E;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,oBAAoB,GAAG,SAAS,GAAG,IAAI,CAoBlF;AAED;;;GAGG;AACH,wBAAsB,uBAAuB,CAC3C,mBAAmB,EAAE,OAAO,EAC5B,MAAM,EAAE,kBAAkB,GACzB,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAM3B;AAED;;;;;;;GAOG;AACH,wBAAsB,kBAAkB,CACtC,mBAAmB,EAAE,OAAO,EAC5B,MAAM,EAAE,kBAAkB,GACzB,OAAO,CAAC;IAAE,SAAS,EAAE,SAAS,CAAC;IAAC,MAAM,EAAE,oBAAoB,CAAA;CAAE,GAAG,IAAI,CAAC,CAQxE"}
1
+ {"version":3,"file":"request-principal.d.ts","sourceRoot":"","sources":["../../src/request-auth/request-principal.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,KAAK,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AAK1F,6EAA6E;AAC7E,wBAAgB,kBAAkB,CAAC,mBAAmB,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAK9E;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,oBAAoB,GAAG,SAAS,GAAG,IAAI,CAiClF;AAED;;;GAGG;AACH,wBAAsB,uBAAuB,CAC3C,mBAAmB,EAAE,OAAO,EAC5B,MAAM,EAAE,kBAAkB,GACzB,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAM3B;AAED;;;;;;;GAOG;AACH,wBAAsB,kBAAkB,CACtC,mBAAmB,EAAE,OAAO,EAC5B,MAAM,EAAE,kBAAkB,GACzB,OAAO,CAAC;IAAE,SAAS,EAAE,SAAS,CAAC;IAAC,MAAM,EAAE,oBAAoB,CAAA;CAAE,GAAG,IAAI,CAAC,CAQxE"}
@@ -44,9 +44,21 @@ function principalFromClaims(claims) {
44
44
  const roleIds = Array.isArray(claims.roleIds)
45
45
  ? claims.roleIds.filter((r) => typeof r === 'string')
46
46
  : [];
47
+ // Scope (ADR-0027 / ADR-0024, Task 2.2) — the active scope node IS the bound tenant. The
48
+ // reachable subtree is the CLOSURE-RESOLVED set (self node + descendants) cached on the binding
49
+ // at LOGIN (`claims.scopeSubtreeNodeIds`), read here with ZERO DB — the parity guarantee: the
50
+ // closure query happened once at login, never per request. FAIL-SAFE + NON-WIDENING: when the
51
+ // cached subtree is ABSENT or EMPTY (a legacy binding, an unwired resolver, or a resolver that
52
+ // threw at login), fall back to just the self-node `[tenantId]` — today's flat self-tenant
53
+ // isolation, EXACTLY. A missing subtree can never widen a principal's reach.
54
+ const subtree = Array.isArray(claims.scopeSubtreeNodeIds) && claims.scopeSubtreeNodeIds.length > 0
55
+ ? [...claims.scopeSubtreeNodeIds]
56
+ : [claims.tenantId];
47
57
  return Object.freeze({
48
58
  kind,
49
59
  id: claims.id,
60
+ activeScopeNodeId: claims.tenantId,
61
+ scopeSubtreeNodeIds: Object.freeze(subtree),
50
62
  tenantId: claims.tenantId,
51
63
  roleIds: Object.freeze([...roleIds]),
52
64
  isRoot: claims.isRoot === true,
@@ -65,6 +65,20 @@ export interface BoundPrincipalClaims {
65
65
  * `cachedFacts` slot is treated as absent (see `resolve()`), never fabricated.
66
66
  */
67
67
  readonly cachedFacts?: CoarseGrants;
68
+ /**
69
+ * OPTIONAL cached active-scope subtree (ADR-0027 / ADR-0024) — the active scope node + all its
70
+ * descendants, resolved ONCE at session establish from `shield_authz.scope_closure` and stored
71
+ * HERE so the per-request tenant gate reads it from the binding (zero per-request DB — the parity
72
+ * guarantee). The subtree query happens ONLY at login; `resolve()` returns this cached set and
73
+ * `principalFromClaims` reads it with NO DB call.
74
+ *
75
+ * ABSENT (undefined) → `principalFromClaims` falls back to `[tenantId]` (self-node, today's flat
76
+ * isolation) — the fail-safe, NON-WIDENING default for a legacy/old binding, an unwired resolver,
77
+ * or a resolver that threw at login. It is NEVER inferred; only a genuine login-time resolution
78
+ * sets it, and an EMPTY resolution is treated as absent (→ self-node) so a missing closure can
79
+ * never silently widen or empty a principal's reach.
80
+ */
81
+ readonly scopeSubtreeNodeIds?: readonly string[];
68
82
  }
69
83
  export declare class SessionTokenBinder {
70
84
  private readonly redis;
@@ -96,7 +110,27 @@ export declare class SessionTokenBinder {
96
110
  * - `null` → STRIP `cachedFacts` (a projection that yielded nothing → the next
97
111
  * request DB-reloads; fail-closed).
98
112
  */
99
- rebindOnRotate(newAccessToken: string, sessionId: string, ttlMs: number, freshFacts?: CoarseGrants | null): Promise<boolean>;
113
+ rebindOnRotate(newAccessToken: string, sessionId: string, ttlMs: number, freshFacts?: CoarseGrants | null, freshSubtree?: readonly string[]): Promise<boolean>;
114
+ /**
115
+ * SCOPE-SWITCH re-bind (Task 5.2, ADR-0027 / ADR-0024). A principal is switching which
116
+ * active node it acts under; re-bind its CURRENT session (the same access token — no
117
+ * rotate, no new tokens) so the bound `tenantId` becomes `targetNodeId` (the new active
118
+ * scope node → `principalFromClaims` reads `activeScopeNodeId = tenantId`) and
119
+ * `scopeSubtreeNodeIds` becomes the NEW node's closure-resolved subtree. The old node's
120
+ * subtree/tenant is REPLACED — stale authority from the previous scope does NOT carry
121
+ * over (the security point of the switch).
122
+ *
123
+ * Rewrites BOTH the per-access-token binding AND the session-claims cushion (so a later
124
+ * refresh/rotate carries the NEW active scope forward), each preserving its remaining TTL
125
+ * (a scope-switch NEVER extends the session life). The `cachedFacts` slot is STRIPPED —
126
+ * the old node's coarse grants are stale under the new scope; the next request re-projects
127
+ * / DB-reloads (fail-closed, never a stale-grant carry-over). The caller resolves the
128
+ * subtree for the NEW node and passes it in.
129
+ *
130
+ * Returns `true` on a successful re-bind, `false` when the binding is gone (logged out /
131
+ * expired) or Redis is unavailable (the caller then denies the switch — fail-closed).
132
+ */
133
+ rebindActiveScope(accessToken: string, sessionId: string, targetNodeId: string, subtree: readonly string[]): Promise<boolean>;
100
134
  /**
101
135
  * Resolve an access token to its bound claims, or `null` if the binding is
102
136
  * absent/expired. Fail-closed: any Redis error → `null` (the caller denies), never
@@ -1 +1 @@
1
- {"version":3,"file":"session-token-binder.d.ts","sourceRoot":"","sources":["../../src/request-auth/session-token-binder.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,KAAK,MAAM,SAAS,CAAC;AAKjC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAEjD,oFAAoF;AACpF,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;;;;OAKG;IACH,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,iEAAiE;IACjE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B;;;;;;;;;;;;OAYG;IACH,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B;;;;;;;;;;;;;OAaG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,YAAY,CAAC;CACrC;AA0CD,qBAAa,kBAAkB;IACjB,OAAO,CAAC,QAAQ,CAAC,KAAK;gBAAL,KAAK,EAAE,KAAK;IAEzC;;;;;;;OAOG;IACG,IAAI,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,oBAAoB,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQ3F;;;;;;;;;;;;;;;;;OAiBG;IACG,cAAc,CAClB,cAAc,EAAE,MAAM,EACtB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,EACb,UAAU,CAAC,EAAE,YAAY,GAAG,IAAI,GAC/B,OAAO,CAAC,OAAO,CAAC;IAgCnB;;;;;OAKG;IACG,OAAO,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC;IA8DxE;;;;;;;;;;;;OAYG;IACG,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA+BrD,gDAAgD;IAC1C,MAAM,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQhD;;;;OAIG;IACG,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAOtD"}
1
+ {"version":3,"file":"session-token-binder.d.ts","sourceRoot":"","sources":["../../src/request-auth/session-token-binder.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,KAAK,MAAM,SAAS,CAAC;AAKjC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAEjD,oFAAoF;AACpF,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;;;;OAKG;IACH,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,iEAAiE;IACjE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B;;;;;;;;;;;;OAYG;IACH,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B;;;;;;;;;;;;;OAaG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,YAAY,CAAC;IACpC;;;;;;;;;;;;OAYG;IACH,QAAQ,CAAC,mBAAmB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAClD;AAmED,qBAAa,kBAAkB;IACjB,OAAO,CAAC,QAAQ,CAAC,KAAK;gBAAL,KAAK,EAAE,KAAK;IAEzC;;;;;;;OAOG;IACG,IAAI,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,oBAAoB,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQ3F;;;;;;;;;;;;;;;;;OAiBG;IACG,cAAc,CAClB,cAAc,EAAE,MAAM,EACtB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,EACb,UAAU,CAAC,EAAE,YAAY,GAAG,IAAI,EAChC,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,GAC/B,OAAO,CAAC,OAAO,CAAC;IA2CnB;;;;;;;;;;;;;;;;;;OAkBG;IACG,iBAAiB,CACrB,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,SAAS,MAAM,EAAE,GACzB,OAAO,CAAC,OAAO,CAAC;IAmDnB;;;;;OAKG;IACG,OAAO,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC;IAwExE;;;;;;;;;;;;OAYG;IACG,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA+BrD,gDAAgD;IAC1C,MAAM,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQhD;;;;OAIG;IACG,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAOtD"}
@@ -45,6 +45,30 @@ function parseCachedFacts(slot) {
45
45
  grantedPermissionKeys: [...c.grantedPermissionKeys],
46
46
  };
47
47
  }
48
+ /**
49
+ * Structurally validate the OPTIONAL `scopeSubtreeNodeIds` slot from the (untrusted) Redis
50
+ * payload. Returns a fresh string[] of the non-empty node ids, or `undefined` when the slot is
51
+ * absent, not an array, empty, or carries any non-string element (fail-safe → the per-request
52
+ * path falls back to the self-node; a junk slot can never widen or empty a principal's reach).
53
+ */
54
+ function parseScopeSubtree(slot) {
55
+ if (!Array.isArray(slot) || slot.length === 0)
56
+ return undefined;
57
+ const out = [];
58
+ for (const n of slot) {
59
+ if (typeof n !== 'string' || n.length === 0)
60
+ return undefined;
61
+ out.push(n);
62
+ }
63
+ return out;
64
+ }
65
+ /** Return the claims with any `scopeSubtreeNodeIds` slot removed (absent → self-node fallback). */
66
+ function stripScopeSubtree(claims) {
67
+ if (!('scopeSubtreeNodeIds' in claims))
68
+ return claims;
69
+ const { scopeSubtreeNodeIds: _drop, ...rest } = claims;
70
+ return rest;
71
+ }
48
72
  /** Return the claims with any `cachedFacts` slot removed (absent → gate 5 DB-reloads). */
49
73
  function stripCachedFacts(claims) {
50
74
  if (!('cachedFacts' in claims))
@@ -100,7 +124,7 @@ class SessionTokenBinder {
100
124
  * - `null` → STRIP `cachedFacts` (a projection that yielded nothing → the next
101
125
  * request DB-reloads; fail-closed).
102
126
  */
103
- async rebindOnRotate(newAccessToken, sessionId, ttlMs, freshFacts) {
127
+ async rebindOnRotate(newAccessToken, sessionId, ttlMs, freshFacts, freshSubtree) {
104
128
  let raw;
105
129
  try {
106
130
  raw = await this.redis.get(sessClaimsKey(sessionId));
@@ -110,16 +134,28 @@ class SessionTokenBinder {
110
134
  }
111
135
  if (raw === null)
112
136
  return false;
113
- // Default: carry the existing payload forward byte-for-byte (facts unchanged).
137
+ // Default: carry the existing payload forward byte-for-byte (facts + subtree unchanged).
114
138
  let payload = raw;
115
- if (freshFacts !== undefined) {
116
- // A refresh-time projection was supplied → rewrite the `cachedFacts` slot on the
117
- // carried-forward claims. A parse failure here is treated as "nothing to update"
118
- // and we carry the original payload forward (fail-safe: never drop the whole
119
- // binding on a transient JSON hiccup — a later request re-projects/DB-reloads).
139
+ if (freshFacts !== undefined || freshSubtree !== undefined) {
140
+ // A refresh-time projection and/or subtree was supplied → rewrite those slots on the
141
+ // carried-forward claims. A parse failure here is treated as "nothing to update" and we
142
+ // carry the original payload forward (fail-safe: never drop the whole binding on a
143
+ // transient JSON hiccup — a later request re-projects/DB-reloads).
120
144
  try {
121
145
  const parsed = JSON.parse(raw);
122
- const next = freshFacts === null ? stripCachedFacts(parsed) : { ...parsed, cachedFacts: freshFacts };
146
+ let next = parsed;
147
+ if (freshFacts !== undefined) {
148
+ next =
149
+ freshFacts === null ? stripCachedFacts(next) : { ...next, cachedFacts: freshFacts };
150
+ }
151
+ // The subtree (the stale-subtree-after-move fix): a non-empty fresh set REPLACES the
152
+ // carried-forward one (a re-parented session gets its correct, possibly-smaller subtree).
153
+ // `undefined` (no resolver / empty / threw at refresh) → carry the existing subtree
154
+ // forward (fail-safe, non-widening — never narrow to self-node on a transient resolver
155
+ // hiccup, never fail the refresh).
156
+ if (freshSubtree !== undefined && freshSubtree.length > 0) {
157
+ next = { ...next, scopeSubtreeNodeIds: [...freshSubtree] };
158
+ }
123
159
  payload = JSON.stringify(next);
124
160
  }
125
161
  catch {
@@ -131,6 +167,77 @@ class SessionTokenBinder {
131
167
  await this.redis.set(sessClaimsKey(sessionId), payload, 'PX', Math.max(ttlMs * 4, ttlMs));
132
168
  return true;
133
169
  }
170
+ /**
171
+ * SCOPE-SWITCH re-bind (Task 5.2, ADR-0027 / ADR-0024). A principal is switching which
172
+ * active node it acts under; re-bind its CURRENT session (the same access token — no
173
+ * rotate, no new tokens) so the bound `tenantId` becomes `targetNodeId` (the new active
174
+ * scope node → `principalFromClaims` reads `activeScopeNodeId = tenantId`) and
175
+ * `scopeSubtreeNodeIds` becomes the NEW node's closure-resolved subtree. The old node's
176
+ * subtree/tenant is REPLACED — stale authority from the previous scope does NOT carry
177
+ * over (the security point of the switch).
178
+ *
179
+ * Rewrites BOTH the per-access-token binding AND the session-claims cushion (so a later
180
+ * refresh/rotate carries the NEW active scope forward), each preserving its remaining TTL
181
+ * (a scope-switch NEVER extends the session life). The `cachedFacts` slot is STRIPPED —
182
+ * the old node's coarse grants are stale under the new scope; the next request re-projects
183
+ * / DB-reloads (fail-closed, never a stale-grant carry-over). The caller resolves the
184
+ * subtree for the NEW node and passes it in.
185
+ *
186
+ * Returns `true` on a successful re-bind, `false` when the binding is gone (logged out /
187
+ * expired) or Redis is unavailable (the caller then denies the switch — fail-closed).
188
+ */
189
+ async rebindActiveScope(accessToken, sessionId, targetNodeId, subtree) {
190
+ // Read the current claims (from the per-access-token entry) + confirm the session is
191
+ // still live (the session-claims entry exists). Any miss / Redis error → fail closed.
192
+ let rawAt;
193
+ let rawSess;
194
+ try {
195
+ rawAt = await this.redis.get(atKey(accessToken));
196
+ rawSess = await this.redis.get(sessClaimsKey(sessionId));
197
+ }
198
+ catch {
199
+ return false; // Redis unavailable → fail closed (no switch).
200
+ }
201
+ if (rawAt === null || rawSess === null)
202
+ return false; // token/session gone → fail closed.
203
+ let current;
204
+ try {
205
+ current = JSON.parse(rawAt);
206
+ }
207
+ catch {
208
+ return false; // malformed → fail closed (a later request re-denies anyway).
209
+ }
210
+ // Build the re-scoped claims: new active node (tenantId) + new subtree, facts stripped.
211
+ // An empty subtree is stored as absent (→ per-request self-node fallback, non-widening) —
212
+ // but the switch caller always passes at least `[targetNodeId]`, so this is defensive.
213
+ const next = stripCachedFacts({
214
+ ...stripScopeSubtree(current),
215
+ tenantId: targetNodeId,
216
+ sessionId,
217
+ ...(subtree.length > 0 ? { scopeSubtreeNodeIds: [...subtree] } : {}),
218
+ });
219
+ const payload = JSON.stringify(next);
220
+ // Preserve each key's remaining TTL — a scope-switch must never extend the session.
221
+ let atTtl;
222
+ let sessTtl;
223
+ try {
224
+ atTtl = await this.redis.pttl(atKey(accessToken));
225
+ sessTtl = await this.redis.pttl(sessClaimsKey(sessionId));
226
+ }
227
+ catch {
228
+ return false;
229
+ }
230
+ if (atTtl <= 0 || sessTtl <= 0)
231
+ return false; // expired between reads → fail closed.
232
+ try {
233
+ await this.redis.set(atKey(accessToken), payload, 'PX', atTtl);
234
+ await this.redis.set(sessClaimsKey(sessionId), payload, 'PX', sessTtl);
235
+ }
236
+ catch {
237
+ return false;
238
+ }
239
+ return true;
240
+ }
134
241
  /**
135
242
  * Resolve an access token to its bound claims, or `null` if the binding is
136
243
  * absent/expired. Fail-closed: any Redis error → `null` (the caller denies), never
@@ -171,6 +278,14 @@ class SessionTokenBinder {
171
278
  parsed = cachedFacts === undefined
172
279
  ? stripCachedFacts(parsed)
173
280
  : { ...parsed, cachedFacts };
281
+ // Normalize the OPTIONAL scope-subtree slot (ADR-0027 / Task 2.2): an absent, empty, or
282
+ // malformed slot is scrubbed to ABSENT (→ `principalFromClaims` falls back to the self-node,
283
+ // non-widening); a well-formed non-empty array is the validated cached subtree. Never
284
+ // fabricated — rebuild the object so a junk slot cannot ride through into a Principal.
285
+ const scopeSubtreeNodeIds = parseScopeSubtree(parsed.scopeSubtreeNodeIds);
286
+ parsed = scopeSubtreeNodeIds === undefined
287
+ ? stripScopeSubtree(parsed)
288
+ : { ...parsed, scopeSubtreeNodeIds };
174
289
  // SESSION-LIVENESS GATE: the per-token binding alone is not enough — a logout
175
290
  // revokes the session but cannot reach every outstanding access-token key. So we
176
291
  // ALSO require the session-claims entry (which `unbindSession` deletes on logout)
@@ -0,0 +1,75 @@
1
+ /**
2
+ * `buildClosureRows` — the PURE, data-foundation half of Task 1.1 (Phase 1 of the
3
+ * scoped-hierarchy tenancy program). Turns a node forest's parent edges into the flat
4
+ * ancestor/descendant/depth rows the `shield_authz.scope_closure` table stores, so any
5
+ * subtree query (`WHERE ancestor_uuid = N`) or ancestry query (`WHERE descendant_uuid = N`)
6
+ * is a flat, indexed lookup — NEVER a recursive walk on any request-serving query path.
7
+ *
8
+ * This module does NO I/O. The Task 1.2 reconciler is the one that calls this against real
9
+ * node edges (read from the DB) and writes the resulting rows back to `scope_closure`; this
10
+ * file only computes the correct row set from an in-memory edge list.
11
+ *
12
+ * ── SEMANTICS ───────────────────────────────────────────────────────────────────────────
13
+ * • EVERY node gets a self-row `{ancestor: n, descendant: n, depth: 0}` — including a
14
+ * root (parent === null) and including a node with NO children.
15
+ * • For each node, walking UP its parent chain, every ancestor `a` at chain-distance `d`
16
+ * (1-based: the immediate parent is depth 1, the grandparent depth 2, ...) emits
17
+ * `{ancestor: a, descendant: n, depth: d}`.
18
+ * • A FLAT set (every node's parent is `null`) therefore degenerates to SELF-ROWS ONLY —
19
+ * this is the non-breaking case: a flat deployment (no hierarchy configured yet) is
20
+ * just a depth-1 forest of roots, and every existing single-tenant scope check
21
+ * (`ancestor_uuid = descendant_uuid = tenant`) continues to hold.
22
+ * • A FOREST (multiple independent root trees) computes each branch independently — no
23
+ * cross-tree rows are ever emitted.
24
+ *
25
+ * ── FAILURE MODES (all fail LOUD — a closure computed over bad data must never silently
26
+ * produce a wrong, over- or under-scoped row set; that would be a tenant-isolation bug) ─
27
+ * • CYCLE (a node reachable from itself via parent edges, direct `A→A` or indirect
28
+ * `A→B→A`): throws {@link ScopeCycleError}. Detected by tracking the in-progress walk
29
+ * set per node — if the walk revisits a node already on the CURRENT path, it's a cycle.
30
+ * • DEPTH EXCEEDED (a parent chain longer than `opts.maxDepth`, default 32 — a sane bound
31
+ * for any real org hierarchy): throws {@link ScopeDepthError}. Guards against a
32
+ * pathological or malformed edge set turning this into an unbounded walk.
33
+ * • UNKNOWN PARENT (a node's `parent` id doesn't reference any node in the SAME edge set):
34
+ * throws a plain `Error` — an orphaned edge is a data error (e.g. the parent was deleted
35
+ * without re-parenting children), never a silent skip that would misplace a subtree.
36
+ * • Empty input → empty output (no throw) — the vacuous case.
37
+ *
38
+ * ── DETERMINISM: output order is not semantically meaningful (the caller writes rows to a
39
+ * PK-indexed table), but rows are sorted (ancestor, then descendant, then depth) so tests
40
+ * and diffs are stable.
41
+ *
42
+ * ── COMPLEXITY: O(nodes × depth) — for each node, one iterative (non-recursive) walk up its
43
+ * parent chain, bounded by `maxDepth`. No recursion, so no call-stack depth risk either.
44
+ */
45
+ /** One parent edge in the node forest: `parent === null` marks a root. */
46
+ export interface ScopeEdge {
47
+ readonly node: string;
48
+ readonly parent: string | null;
49
+ }
50
+ /** One row of the computed transitive closure. */
51
+ export interface ClosureRow {
52
+ readonly ancestor: string;
53
+ readonly descendant: string;
54
+ readonly depth: number;
55
+ }
56
+ /** A node's parent chain revisits a node already on the walk — the edge set has a cycle. */
57
+ export declare class ScopeCycleError extends Error {
58
+ constructor(node: string);
59
+ }
60
+ /** A node's parent chain exceeds the configured (or default) maximum depth. */
61
+ export declare class ScopeDepthError extends Error {
62
+ constructor(node: string, maxDepth: number);
63
+ }
64
+ /** A node's `parent` id does not reference any node present in the same edge set. */
65
+ export declare class ScopeUnknownParentError extends Error {
66
+ constructor(node: string, parent: string);
67
+ }
68
+ /**
69
+ * Compute the transitive closure of a node forest from its parent edges.
70
+ * Pure — no I/O. See the file header for full semantics and failure modes.
71
+ */
72
+ export declare function buildClosureRows(edges: readonly ScopeEdge[], opts?: {
73
+ maxDepth?: number;
74
+ }): ClosureRow[];
75
+ //# sourceMappingURL=closure-builder.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"closure-builder.d.ts","sourceRoot":"","sources":["../../src/scope/closure-builder.ts"],"names":[],"mappings":"AACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AAEH,0EAA0E;AAC1E,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC;AAED,kDAAkD;AAClD,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,4FAA4F;AAC5F,qBAAa,eAAgB,SAAQ,KAAK;gBAC5B,IAAI,EAAE,MAAM;CAQzB;AAED,+EAA+E;AAC/E,qBAAa,eAAgB,SAAQ,KAAK;gBAC5B,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM;CAS3C;AAED,qFAAqF;AACrF,qBAAa,uBAAwB,SAAQ,KAAK;gBACpC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;CASzC;AAKD;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,SAAS,SAAS,EAAE,EAC3B,IAAI,CAAC,EAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GAC3B,UAAU,EAAE,CA2Dd"}