@astram/guard 0.7.0 → 0.9.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 (187) hide show
  1. package/dist/auth/account.controller.d.ts +27 -5
  2. package/dist/auth/account.controller.d.ts.map +1 -1
  3. package/dist/auth/account.controller.js +99 -26
  4. package/dist/auth/auth.controller.d.ts +130 -21
  5. package/dist/auth/auth.controller.d.ts.map +1 -1
  6. package/dist/auth/auth.controller.js +537 -143
  7. package/dist/auth/auth.module.d.ts +15 -1
  8. package/dist/auth/auth.module.d.ts.map +1 -1
  9. package/dist/auth/auth.module.js +212 -30
  10. package/dist/auth/connections.d.ts +1 -1
  11. package/dist/auth/connections.d.ts.map +1 -1
  12. package/dist/auth/connections.js +14 -3
  13. package/dist/auth/default-session-facts-projector.d.ts +26 -0
  14. package/dist/auth/default-session-facts-projector.d.ts.map +1 -0
  15. package/dist/auth/default-session-facts-projector.js +21 -0
  16. package/dist/auth/mfa-step-up.d.ts +32 -0
  17. package/dist/auth/mfa-step-up.d.ts.map +1 -0
  18. package/dist/auth/mfa-step-up.js +109 -0
  19. package/dist/auth/pending-mfa-store.d.ts +50 -52
  20. package/dist/auth/pending-mfa-store.d.ts.map +1 -1
  21. package/dist/auth/pending-mfa-store.js +103 -61
  22. package/dist/auth/ports/session-facts-projector.d.ts +5 -0
  23. package/dist/auth/ports/session-facts-projector.d.ts.map +1 -1
  24. package/dist/auth/rate-limit/canonical-hint.d.ts +24 -0
  25. package/dist/auth/rate-limit/canonical-hint.d.ts.map +1 -0
  26. package/dist/auth/rate-limit/canonical-hint.js +63 -0
  27. package/dist/auth/rate-limit/trust-proxy-check.d.ts +46 -0
  28. package/dist/auth/rate-limit/trust-proxy-check.d.ts.map +1 -0
  29. package/dist/auth/rate-limit/trust-proxy-check.js +94 -0
  30. package/dist/auth/tokens.d.ts +70 -11
  31. package/dist/auth/tokens.d.ts.map +1 -1
  32. package/dist/auth/tokens.js +71 -12
  33. package/dist/authentication/log-email-sender.d.ts +9 -4
  34. package/dist/authentication/log-email-sender.d.ts.map +1 -1
  35. package/dist/authentication/mariadb-api-key-store.d.ts +5 -1
  36. package/dist/authentication/mariadb-api-key-store.d.ts.map +1 -1
  37. package/dist/authentication/mariadb-api-key-store.js +43 -7
  38. package/dist/authentication/mariadb-credential-resolver.d.ts +79 -65
  39. package/dist/authentication/mariadb-credential-resolver.d.ts.map +1 -1
  40. package/dist/authentication/mariadb-credential-resolver.js +116 -73
  41. package/dist/authentication/mariadb-credential-writer.d.ts.map +1 -1
  42. package/dist/authentication/mariadb-or-totp-verifier.d.ts +21 -3
  43. package/dist/authentication/mariadb-or-totp-verifier.d.ts.map +1 -1
  44. package/dist/authentication/mariadb-or-totp-verifier.js +37 -6
  45. package/dist/authentication/mariadb-profile-resolver.d.ts.map +1 -1
  46. package/dist/authentication/mariadb-profile-writer.d.ts.map +1 -1
  47. package/dist/authentication/mariadb-session-sweeper.d.ts +14 -1
  48. package/dist/authentication/mariadb-session-sweeper.d.ts.map +1 -1
  49. package/dist/authentication/mariadb-session-sweeper.js +29 -11
  50. package/dist/authentication/noop-email-sender.d.ts +12 -0
  51. package/dist/authentication/noop-email-sender.d.ts.map +1 -0
  52. package/dist/authentication/noop-email-sender.js +9 -0
  53. package/dist/authz/cross-tenant-grant-writer.d.ts.map +1 -1
  54. package/dist/authz/manifest-operation-authority-adapter.d.ts.map +1 -1
  55. package/dist/authz/mariadb-cross-tenant-grant-adapter.d.ts.map +1 -1
  56. package/dist/authz/mariadb-cross-tenant-grant-adapter.js +3 -1
  57. package/dist/authz/mariadb-policy-store.js +1 -1
  58. package/dist/authz/mariadb-record-rule-adapter.d.ts.map +1 -1
  59. package/dist/authz/mariadb-record-rule-adapter.js +1 -4
  60. package/dist/authz/permission-version-cache.d.ts +38 -0
  61. package/dist/authz/permission-version-cache.d.ts.map +1 -0
  62. package/dist/authz/permission-version-cache.js +81 -0
  63. package/dist/authz/wire-record-authorization.d.ts +25 -1
  64. package/dist/authz/wire-record-authorization.d.ts.map +1 -1
  65. package/dist/authz/wire-record-authorization.js +55 -2
  66. package/dist/completeness/catalog-reconciler.d.ts.map +1 -1
  67. package/dist/config/app-config.d.ts +36 -0
  68. package/dist/config/app-config.d.ts.map +1 -1
  69. package/dist/config/app-config.js +44 -4
  70. package/dist/encryption/local-kek-crypto.d.ts.map +1 -1
  71. package/dist/enforcement/enforcement.module.d.ts.map +1 -1
  72. package/dist/enforcement/enforcement.module.js +9 -0
  73. package/dist/enforcement/engine-error.filter.d.ts +57 -0
  74. package/dist/enforcement/engine-error.filter.d.ts.map +1 -0
  75. package/dist/enforcement/engine-error.filter.js +118 -0
  76. package/dist/enforcement/global-enforcement.guard.d.ts +29 -2
  77. package/dist/enforcement/global-enforcement.guard.d.ts.map +1 -1
  78. package/dist/enforcement/global-enforcement.guard.js +143 -5
  79. package/dist/identity/auth-facts.controller.d.ts.map +1 -1
  80. package/dist/identity/permission-facts.adapter.d.ts.map +1 -1
  81. package/dist/identity/permission-facts.adapter.js +2 -3
  82. package/dist/index.d.ts +13 -7
  83. package/dist/index.d.ts.map +1 -1
  84. package/dist/index.js +68 -28
  85. package/dist/internal.d.ts +12 -0
  86. package/dist/internal.d.ts.map +1 -0
  87. package/dist/internal.js +16 -0
  88. package/dist/module/shield.module.d.ts +9 -2
  89. package/dist/module/shield.module.d.ts.map +1 -1
  90. package/dist/module/shield.module.js +47 -38
  91. package/dist/request-auth/session-facts-als.d.ts +16 -1
  92. package/dist/request-auth/session-facts-als.d.ts.map +1 -1
  93. package/dist/request-auth/session-facts-als.js +23 -0
  94. package/dist/request-auth/session-token-binder.d.ts +166 -10
  95. package/dist/request-auth/session-token-binder.d.ts.map +1 -1
  96. package/dist/request-auth/session-token-binder.js +408 -28
  97. package/dist/secrets/encrypted-file-secrets.d.ts.map +1 -1
  98. package/dist/secrets/encrypted-file-secrets.js +9 -3
  99. package/dist/session/hot-revoking-session-record-store.d.ts +46 -0
  100. package/dist/session/hot-revoking-session-record-store.d.ts.map +1 -0
  101. package/dist/session/hot-revoking-session-record-store.js +58 -0
  102. package/dist/session/index.d.ts +3 -1
  103. package/dist/session/index.d.ts.map +1 -1
  104. package/dist/session/index.js +8 -1
  105. package/dist/session/mariadb-session-record-store.d.ts +6 -0
  106. package/dist/session/mariadb-session-record-store.d.ts.map +1 -1
  107. package/dist/session/mariadb-session-record-store.js +16 -1
  108. package/dist/session/session-lifetimes.d.ts +30 -0
  109. package/dist/session/session-lifetimes.d.ts.map +1 -0
  110. package/dist/session/session-lifetimes.js +21 -0
  111. package/dist/session/session-restriction-store.d.ts +43 -0
  112. package/dist/session/session-restriction-store.d.ts.map +1 -0
  113. package/dist/session/session-restriction-store.js +35 -0
  114. package/package.json +14 -3
  115. package/dist/auth/account.controller.js.map +0 -1
  116. package/dist/auth/auth.controller.js.map +0 -1
  117. package/dist/auth/auth.module.js.map +0 -1
  118. package/dist/auth/connections.js.map +0 -1
  119. package/dist/auth/dto.js.map +0 -1
  120. package/dist/auth/pending-mfa-store.js.map +0 -1
  121. package/dist/auth/persistence.module.js.map +0 -1
  122. package/dist/auth/ports/audit-sink.js.map +0 -1
  123. package/dist/auth/ports/credential-writer.js.map +0 -1
  124. package/dist/auth/ports/email-sender.js.map +0 -1
  125. package/dist/auth/ports/membership-checker.js.map +0 -1
  126. package/dist/auth/ports/mfa-device-store.js.map +0 -1
  127. package/dist/auth/ports/profile-writer.js.map +0 -1
  128. package/dist/auth/ports/recovery-code-store.js.map +0 -1
  129. package/dist/auth/ports/reset-token-store.js.map +0 -1
  130. package/dist/auth/ports/scope-subtree-resolver.js.map +0 -1
  131. package/dist/auth/ports/session-facts-projector.js.map +0 -1
  132. package/dist/auth/ports/session-sweeper.js.map +0 -1
  133. package/dist/auth/profile-resolver.js.map +0 -1
  134. package/dist/auth/rate-limit/attempt-limiter.js.map +0 -1
  135. package/dist/auth/tokens.js.map +0 -1
  136. package/dist/auth/totp-enrollment.js.map +0 -1
  137. package/dist/authentication/argon2id-password-hasher.js.map +0 -1
  138. package/dist/authentication/log-email-sender.js.map +0 -1
  139. package/dist/authentication/mariadb-credential-resolver.js.map +0 -1
  140. package/dist/authentication/mariadb-credential-writer.js.map +0 -1
  141. package/dist/authentication/mariadb-mfa-device-store.js.map +0 -1
  142. package/dist/authentication/mariadb-or-totp-verifier.js.map +0 -1
  143. package/dist/authentication/mariadb-profile-resolver.js.map +0 -1
  144. package/dist/authentication/mariadb-profile-writer.js.map +0 -1
  145. package/dist/authentication/mariadb-recovery-code-store.js.map +0 -1
  146. package/dist/authentication/mariadb-reset-token-store.js.map +0 -1
  147. package/dist/authentication/mariadb-session-sweeper.js.map +0 -1
  148. package/dist/authz/cross-tenant-grant-writer.js.map +0 -1
  149. package/dist/authz/list-scope-sql.js.map +0 -1
  150. package/dist/authz/manifest-operation-authority-adapter.js.map +0 -1
  151. package/dist/authz/mariadb-cross-tenant-grant-adapter.js.map +0 -1
  152. package/dist/authz/mariadb-list-scope-adapter.js.map +0 -1
  153. package/dist/authz/mariadb-policy-store.js.map +0 -1
  154. package/dist/authz/mariadb-record-rule-adapter.js.map +0 -1
  155. package/dist/authz/mariadb-record-share-adapter.js.map +0 -1
  156. package/dist/authz/record-share-cascade.js.map +0 -1
  157. package/dist/authz/record-share-writer.js.map +0 -1
  158. package/dist/authz/wire-record-authorization.js.map +0 -1
  159. package/dist/boot/boot-sequence.js.map +0 -1
  160. package/dist/boot/boot.module.js.map +0 -1
  161. package/dist/completeness/catalog-reconciler.js.map +0 -1
  162. package/dist/completeness/operation-inventory-scanner.js.map +0 -1
  163. package/dist/completeness/ports/catalog-store.js.map +0 -1
  164. package/dist/config/app-config.js.map +0 -1
  165. package/dist/config/config.module.js.map +0 -1
  166. package/dist/enforcement/classification.decorator.js.map +0 -1
  167. package/dist/enforcement/core-seam.js.map +0 -1
  168. package/dist/enforcement/enforcement.module.js.map +0 -1
  169. package/dist/enforcement/global-enforcement.guard.js.map +0 -1
  170. package/dist/enforcement/reenroll-allowlist.js.map +0 -1
  171. package/dist/identity/auth-facts.controller.js.map +0 -1
  172. package/dist/identity/identity-actions.js.map +0 -1
  173. package/dist/identity/module-registry.bridge.js.map +0 -1
  174. package/dist/identity/permission-facts.adapter.js.map +0 -1
  175. package/dist/index.js.map +0 -1
  176. package/dist/migrations-dir.js.map +0 -1
  177. package/dist/module/shield-tokens.js.map +0 -1
  178. package/dist/module/shield.module.js.map +0 -1
  179. package/dist/request-auth/request-principal.js.map +0 -1
  180. package/dist/request-auth/session-facts-als.js.map +0 -1
  181. package/dist/request-auth/session-token-binder.js.map +0 -1
  182. package/dist/request-auth/tokens.js.map +0 -1
  183. package/dist/scope/closure-builder.js.map +0 -1
  184. package/dist/session/crypto-adapters.js.map +0 -1
  185. package/dist/session/index.js.map +0 -1
  186. package/dist/session/mariadb-session-record-store.js.map +0 -1
  187. package/dist/session/redis-hot-session-store.js.map +0 -1
@@ -37,7 +37,30 @@
37
37
  */
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.NO_SESSION_FACTS = exports.SHIELD_SESSION_FACTS_ALS = void 0;
40
+ exports.signalGateFiveReloads = signalGateFiveReloads;
40
41
  const node_async_hooks_1 = require("node:async_hooks");
42
+ /**
43
+ * Wrap an app-owned `PermissionFactsPort`-shaped object so a gate-5 reload inside an enforcement
44
+ * request notifies that request's {@link SessionFactsStore.onReload}. Behaviour-identical
45
+ * otherwise: same results, same errors (a throwing reload propagates and notifies nothing).
46
+ * Outside a guard request (no ALS store, e.g. the engine's in-handler decide calls) it is a no-op.
47
+ */
48
+ function signalGateFiveReloads(port) {
49
+ return {
50
+ currentPermissionsVersion: (principalId, tenantId) => port.currentPermissionsVersion(principalId, tenantId),
51
+ loadCoarseGrants: async (principalId, tenantId) => {
52
+ const grants = await port.loadCoarseGrants(principalId, tenantId);
53
+ const store = exports.SHIELD_SESSION_FACTS_ALS.getStore();
54
+ try {
55
+ store?.onReload?.(principalId, tenantId, grants);
56
+ }
57
+ catch {
58
+ // A notification must never turn a decision into an error.
59
+ }
60
+ return grants;
61
+ },
62
+ };
63
+ }
41
64
  /**
42
65
  * The ONE shared session-facts `AsyncLocalStorage` (see the file header). The
43
66
  * enforcement guard imports this to `run()` the `decide()` call inside the request's
@@ -11,7 +11,10 @@
11
11
  * establish/rotate time and reads it per request. Zero Core changes.
12
12
  *
13
13
  * The binding: `shield:at:<accessToken>` → JSON {@link BoundPrincipalClaims}
14
- * ({ id, tenantId, roleIds, kind }), TTL = the access-token lifetime. The token
14
+ * ({ id, tenantId, roleIds, kind }), TTL ≤ the access-token lifetime (security hardening
15
+ * #8). Alongside it: `shield:sessclaims:<sessionId>` (the liveness entry, TTL = the refresh
16
+ * idle window when configured) and `shield:sessat:<sessionId>` (the session's current
17
+ * access token, so a rotation can grace-expire the old one and a revoke can drop it). The token
15
18
  * stays fully opaque (carries no data itself); the mapping is server-side, so the
16
19
  * session is server-REVOCABLE (drop the key) and the read is FAIL-CLOSED (absent or
17
20
  * Redis-unavailable → no principal → the guard denies).
@@ -79,26 +82,74 @@ export interface BoundPrincipalClaims {
79
82
  * never silently widen or empty a principal's reach.
80
83
  */
81
84
  readonly scopeSubtreeNodeIds?: readonly string[];
85
+ /**
86
+ * OPTIONAL: the permission version of `tenantId` read just BEFORE `scopeSubtreeNodeIds` was
87
+ * resolved (security hardening Phase 1C). When gate 5 reloads facts at a NEWER version (identity
88
+ * bumps it on a company create/move/delete), the enforcement guard re-derives the subtree once
89
+ * and advances this stamp. Read before the resolve, so a bump racing the resolve leaves the stamp
90
+ * too OLD (one extra re-derive), never too new (a missed one).
91
+ *
92
+ * Only meaningful beside a subtree: `resolve()` drops it when the subtree is absent or the value
93
+ * is not a non-negative integer. ABSENT with a subtree (a legacy binding, or a version read that
94
+ * failed) → the next gate-5 reload re-derives once and stamps.
95
+ */
96
+ readonly scopeSubtreeVersion?: number;
97
+ }
98
+ /**
99
+ * The `cachedFacts` slot a binding stores (ADR-0035): the version, the keys and the optional time
100
+ * bound. The projection's `roleIds` are NOT kept here — they are the claims' own `roleIds`, written
101
+ * in the same payload, so there is one copy of the roles.
102
+ */
103
+ export declare function factsSlot(grants: CoarseGrants): CoarseGrants;
104
+ /** The projection's live roles when it reported them (a string array), else `undefined`. */
105
+ export declare function liveRoleIds(grants: CoarseGrants | null | undefined): string[] | undefined;
106
+ /** Lifetimes the binder applies (security hardening #8). Both optional for back-compat. */
107
+ export interface SessionTokenBinderOptions {
108
+ /**
109
+ * TTL of the session-claims (liveness) entry and the current-token pointer. The auth
110
+ * module passes the refresh idle window (`refreshIdleMs`): the entry must outlive every
111
+ * access token of a live session, and no longer. Absent → the legacy `4 × access TTL`.
112
+ */
113
+ readonly sessionClaimsTtlMs?: number;
114
+ /**
115
+ * How long the PREVIOUS access token stays bound after `rebindOnRotate` (plan D7,
116
+ * default 30 s). It is then expired (`PEXPIRE`), so a rotated-away token never keeps
117
+ * its full access TTL.
118
+ */
119
+ readonly rotateGraceMs?: number;
82
120
  }
83
121
  export declare class SessionTokenBinder {
84
122
  private readonly redis;
85
- constructor(redis: Redis);
123
+ private readonly options;
124
+ constructor(redis: Redis, options?: SessionTokenBinderOptions);
125
+ /** TTL for the session-claims entry + current-token pointer. */
126
+ private sessionTtl;
86
127
  /**
87
128
  * Bind an access token to its principal claims for `ttlMs` (the access-token
88
129
  * lifetime). Called right after `SessionAuthority.establish` in the auth
89
130
  * controller (it holds both the freshly-minted access token and the Principal).
90
131
  * ALSO writes a session-id → claims entry so a later `rebindOnRotate` can carry the
91
132
  * full claims (roles/kind/root) forward across a token rotation, where the
92
- * durable session row alone lacks them.
133
+ * durable session row alone lacks them, and records the session's CURRENT access
134
+ * token so a rotation can grace-expire it and a revoke can drop it.
93
135
  */
94
136
  bind(accessToken: string, claims: BoundPrincipalClaims, ttlMs: number): Promise<void>;
137
+ /**
138
+ * Grace-expire the session's previous access token after a rotation (plan D7): it stays
139
+ * usable for at most `rotateGraceMs` (never longer than it already had), then expires.
140
+ * Best-effort — a failure leaves the old binding to its own (≤ access TTL) expiry.
141
+ */
142
+ private graceExpire;
95
143
  /**
96
144
  * On refresh/rotate the controller has the NEW access token + the session, but not
97
145
  * the full Principal (roles/kind/root live only in the binding, not the
98
146
  * durable session row). Re-bind by reading the session-claims entry and writing the
99
147
  * new access-token binding with the SAME claims. Returns true if it carried claims
100
- * forward, false if the session-claims entry was gone (then the caller has nothing
101
- * to bind — a later request will 401 and the user re-logs in; fail-closed).
148
+ * forward, false if the session-claims entry was gone or unreadable — the caller MUST
149
+ * then refuse the refresh (the auth controller answers 401: an unbound token is never
150
+ * handed out). A Redis error on the writes propagates (the caller fails closed too).
151
+ * The new binding's TTL is `ttlMs` (≤ the access TTL); the session's previous access
152
+ * token is grace-expired (≤ 30 s by default, plan D7).
102
153
  *
103
154
  * `freshFacts` (ADR-0011 eager projection at REFRESH):
104
155
  * - `undefined` (omitted, the default) → carry whatever `cachedFacts` the session
@@ -110,7 +161,70 @@ export declare class SessionTokenBinder {
110
161
  * - `null` → STRIP `cachedFacts` (a projection that yielded nothing → the next
111
162
  * request DB-reloads; fail-closed).
112
163
  */
113
- rebindOnRotate(newAccessToken: string, sessionId: string, ttlMs: number, freshFacts?: CoarseGrants | null, freshSubtree?: readonly string[]): Promise<boolean>;
164
+ rebindOnRotate(newAccessToken: string, sessionId: string, ttlMs: number, freshFacts?: CoarseGrants | null, freshSubtree?: readonly string[],
165
+ /**
166
+ * The permission version read just before `freshSubtree` was resolved (Phase 1C). Stamped
167
+ * when the fresh subtree is applied; absent → the stamp is dropped (unknown → re-derive once
168
+ * at the next reload). Ignored when no fresh subtree is applied (the carried subtree keeps its
169
+ * own stamp).
170
+ */
171
+ freshSubtreeVersion?: number,
172
+ /**
173
+ * The principal's LIVE role ids from the same projection as `freshFacts` (ADR-0035, GAPS #4).
174
+ * Given → they REPLACE the carried roles (roles and facts are written together). Absent → the
175
+ * carried roles stay (a projector that does not report roles, or no projection at all).
176
+ */
177
+ freshRoleIds?: readonly string[],
178
+ /**
179
+ * The node the fresh facts/roles/subtree were projected for (review follow-up: a scope-switch
180
+ * moves the claims off the login tenant). Given and the claims are now on a DIFFERENT node (a
181
+ * switch raced the refresh) → nothing fresh is applied and the cached facts are stripped (the
182
+ * next request reloads for the right node). Absent → applied as before.
183
+ */
184
+ freshForTenantId?: string): Promise<boolean>;
185
+ /**
186
+ * SILENT RECOVERY (ADR-0013 / ADR-0035, GAPS #5): write a session binding REBUILT from durable
187
+ * state after the session-claims entry was lost (Redis restart/flush). The caller (refresh) has
188
+ * already rotated the durable refresh token and assembled `claims` from the durable session
189
+ * record + live roles + the root allowlist + the subtree resolver.
190
+ *
191
+ * The claims entry is written with `NX`: if one appeared meanwhile, nothing is overwritten and
192
+ * the rebuild is refused (`false`). The caller MUST then re-read the durable session and call
193
+ * {@link unbindSession} if it is no longer active (a logout racing the rebuild). A Redis error
194
+ * propagates (the caller fails closed).
195
+ */
196
+ bindRecovered(accessToken: string, claims: BoundPrincipalClaims, ttlMs: number): Promise<boolean>;
197
+ /**
198
+ * The node a live session currently acts under (its claims' `tenantId` — the scope-switch target
199
+ * when it switched), or `null` when the claims are gone, unreadable, or Redis errors. Refresh uses
200
+ * it to project for the ACTIVE node; `null` → the durable login tenant.
201
+ */
202
+ sessionTenant(sessionId: string): Promise<string | null>;
203
+ /**
204
+ * Whether Redis answers right now (Phase 2 review F1). Refresh asks BEFORE the durable rotate
205
+ * spends the refresh token, so a Redis outage answers a retryable 503 instead of burning the
206
+ * token on a binding that cannot be written. A client without `ping` (a test double) counts as
207
+ * reachable.
208
+ *
209
+ * Bounded by `timeoutMs` (default 1 s): the framework's client queues commands while
210
+ * disconnected (`maxRetriesPerRequest: null`), so an unbounded PING would wait for Redis to come
211
+ * back and the refresh would then spend the token for a client that already gave up.
212
+ */
213
+ isAvailable(timeoutMs?: number): Promise<boolean>;
214
+ /**
215
+ * LIVE GRANTS re-bind (ADR-0035, GAPS #4/#6). Gate 5 reloaded this session's coarse grants (a
216
+ * version bump, an expired time bound, or no cache yet); write them, and the live roles from the
217
+ * same projection, onto the access-token entry AND the session-claims entry, so the next requests
218
+ * are served from the cache and every role-keyed path sees the new roles. Nothing else changes.
219
+ *
220
+ * Each entry is rewritten only when it still belongs to `sessionId` + `tenantId` (a concurrent
221
+ * scope-switch moved it → left alone) and `facts` is fresher than what it holds (a slower request
222
+ * never overwrites newer facts). Each keeps its remaining TTL and is written with `XX` (a logout
223
+ * that deleted it wins). Best-effort: any Redis error or miss → that entry is skipped; the caller
224
+ * still uses the reloaded roles for the current request. Returns `true` when the access-token
225
+ * entry was rewritten.
226
+ */
227
+ refreshGrants(accessToken: string, sessionId: string, tenantId: string, facts: CoarseGrants, roleIds?: readonly string[]): Promise<boolean>;
114
228
  /**
115
229
  * SCOPE-SWITCH re-bind (Task 5.2, ADR-0027 / ADR-0024). A principal is switching which
116
230
  * active node it acts under; re-bind its CURRENT session (the same access token — no
@@ -130,7 +244,39 @@ export declare class SessionTokenBinder {
130
244
  * Returns `true` on a successful re-bind, `false` when the binding is gone (logged out /
131
245
  * expired) or Redis is unavailable (the caller then denies the switch — fail-closed).
132
246
  */
133
- rebindActiveScope(accessToken: string, sessionId: string, targetNodeId: string, subtree: readonly string[]): Promise<boolean>;
247
+ rebindActiveScope(accessToken: string, sessionId: string, targetNodeId: string, subtree: readonly string[],
248
+ /**
249
+ * The TARGET node's permission version read just before `subtree` was resolved (Phase 1C).
250
+ * The old node's stamp is always dropped (versions are per tenant); this one is stamped when
251
+ * given, else the stamp stays absent (→ one re-derive at the next reload).
252
+ */
253
+ subtreeVersion?: number,
254
+ /**
255
+ * The coarse grants projected for the TARGET node (ADR-0035). Given → cached on the re-scoped
256
+ * binding instead of stripped (the first request under the new scope is served from cache).
257
+ * Absent/null → stripped (the next request reloads).
258
+ */
259
+ targetFacts?: CoarseGrants | null,
260
+ /**
261
+ * The principal's LIVE roles at the TARGET node (ADR-0035, GAPS #4), from the same projection
262
+ * as `targetFacts`. Given → they replace the old node's roles (roles are per node). Absent →
263
+ * the carried roles stay until the first request's reload replaces them.
264
+ */
265
+ targetRoleIds?: readonly string[]): Promise<boolean>;
266
+ /**
267
+ * RE-DERIVED SUBTREE re-bind (security hardening Phase 1C). The enforcement guard saw gate 5
268
+ * reload the facts at a permission version NEWER than the bound subtree's stamp and resolved
269
+ * the subtree again; write it (and its stamp) onto the access-token entry AND the session-claims
270
+ * entry, so this token and any later rotation carry it. Nothing else in the claims changes.
271
+ *
272
+ * Each entry is rewritten only when it still belongs to `sessionId` + `tenantId` (a concurrent
273
+ * scope-switch moved it elsewhere → left alone) and its stamp is absent or OLDER than `version`
274
+ * (a concurrent refresh already wrote something at least as fresh → left alone). Each keeps its
275
+ * remaining TTL (a re-derive never extends a session). Best-effort: any Redis error or miss →
276
+ * `false`; the caller still uses the fresh subtree for the current request and a later reload
277
+ * retries. Returns `true` when the access-token entry was rewritten.
278
+ */
279
+ rederiveScopeSubtree(accessToken: string, sessionId: string, tenantId: string, subtree: readonly string[], version: number): Promise<boolean>;
134
280
  /**
135
281
  * Resolve an access token to its bound claims, or `null` if the binding is
136
282
  * absent/expired. Fail-closed: any Redis error → `null` (the caller denies), never
@@ -155,9 +301,19 @@ export declare class SessionTokenBinder {
155
301
  /** Drop an access-token binding. Idempotent. */
156
302
  unbind(accessToken: string): Promise<void>;
157
303
  /**
158
- * Drop the session-claims entry on logout/revoke, so a rotate after logout cannot
159
- * re-mint a binding (defense-in-depth: the session is already revoked, but this
160
- * removes the claims cushion too). The per-access-token bindings expire by TTL.
304
+ * The THROWING variant of {@link unbindSession} (Phase 3 review P1): the same keys, but a
305
+ * Redis error PROPAGATES. An administrative revoke (an account disable through the session
306
+ * sweeper) must report a failed unbind so the admin can retry, instead of leaving the
307
+ * session's live access tokens valid until their TTL. Idempotent (absent keys are fine).
308
+ */
309
+ unbindSessionStrict(sessionId: string): Promise<void>;
310
+ /**
311
+ * Revoke a session in Redis (logout, password reset, refresh reuse-kill, admin revoke):
312
+ * drop the session-claims entry — which the {@link resolve} liveness gate requires, so
313
+ * EVERY access token of the session is denied on its next request — and also the
314
+ * session's current access-token binding and pointer. A grace-period token from an
315
+ * earlier rotation is covered by the liveness gate and expires within the grace.
316
+ * Best-effort per key; TTLs clean up regardless ({@link unbindSessionStrict} throws instead).
161
317
  */
162
318
  unbindSession(sessionId: string): Promise<void>;
163
319
  }
@@ -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;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"}
1
+ {"version":3,"file":"session-token-binder.d.ts","sourceRoot":"","sources":["../../src/request-auth/session-token-binder.ts"],"names":[],"mappings":"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"}