@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
@@ -136,7 +136,16 @@ export interface AuthModuleOptions {
136
136
  */
137
137
  readonly credentialWriter?: CredentialWriter;
138
138
  readonly resetTokenStore?: ResetTokenStore;
139
- readonly emailSender?: EmailSender;
139
+ /**
140
+ * The password-reset email delivery seam (security hardening #6):
141
+ * - an {@link EmailSender} → used in every environment (production needs one);
142
+ * - `'disabled'` → a no-op sender: forgot-password still answers 204, nothing is
143
+ * delivered or printed, one startup warning;
144
+ * - omitted → the dev `LogEmailSender` (prints reset links to stdout) ONLY when the raw
145
+ * `NODE_ENV` is `development` or `test`; otherwise (incl. unset `NODE_ENV`) auth
146
+ * composition throws "auth:true requires `emailSender` (or `emailSender: 'disabled'`)".
147
+ */
148
+ readonly emailSender?: EmailSender | 'disabled';
140
149
  readonly recoveryCodeStore?: RecoveryCodeStore;
141
150
  readonly profileWriter?: ProfileWriter;
142
151
  readonly sessionSweeper?: SessionSweeper;
@@ -210,6 +219,11 @@ export interface AuthModuleOptions {
210
219
  */
211
220
  readonly membershipChecker?: MembershipChecker;
212
221
  }
222
+ /**
223
+ * The no-override default sender decision (pure apart from the warning). Exported for
224
+ * the unit test only — not part of the package's public surface.
225
+ */
226
+ export declare function resolveDefaultEmailSender(rawNodeEnv: string | undefined): EmailSender;
213
227
  /**
214
228
  * The auth composition module. Imports {@link PersistenceModule} (the live
215
229
  * connections) and provides the adapters + composed engines. Exports the two
@@ -1 +1 @@
1
- {"version":3,"file":"auth.module.d.ts","sourceRoot":"","sources":["../../src/auth/auth.module.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"auth.module.d.ts","sourceRoot":"","sources":["../../src/auth/auth.module.ts"],"names":[],"mappings":"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"}
@@ -107,6 +107,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
107
107
  exports.AuthModule = exports.hmacSha1 = void 0;
108
108
  exports.resolveTotpVerifier = resolveTotpVerifier;
109
109
  exports.makeDefaultTotpVerifier = makeDefaultTotpVerifier;
110
+ exports.resolveDefaultEmailSender = resolveDefaultEmailSender;
110
111
  const common_1 = require("@nestjs/common");
111
112
  const node_crypto_1 = require("node:crypto");
112
113
  const app_config_js_1 = require("../config/app-config.js");
@@ -121,6 +122,7 @@ const mariadb_recovery_code_store_js_1 = require("../authentication/mariadb-reco
121
122
  const mariadb_mfa_device_store_js_1 = require("../authentication/mariadb-mfa-device-store.js");
122
123
  const mariadb_session_sweeper_js_1 = require("../authentication/mariadb-session-sweeper.js");
123
124
  const log_email_sender_js_1 = require("../authentication/log-email-sender.js");
125
+ const noop_email_sender_js_1 = require("../authentication/noop-email-sender.js");
124
126
  const mariadb_or_totp_verifier_js_1 = require("../authentication/mariadb-or-totp-verifier.js");
125
127
  const permission_facts_adapter_js_1 = require("../identity/permission-facts.adapter.js");
126
128
  const auth_facts_controller_js_1 = require("../identity/auth-facts.controller.js");
@@ -129,8 +131,12 @@ const persistence_module_js_1 = require("./persistence.module.js");
129
131
  const auth_controller_js_1 = require("./auth.controller.js");
130
132
  const account_controller_js_1 = require("./account.controller.js");
131
133
  const pending_mfa_store_js_1 = require("./pending-mfa-store.js");
134
+ const mfa_step_up_js_1 = require("./mfa-step-up.js");
132
135
  const tokens_js_1 = require("./tokens.js");
136
+ const permission_version_cache_js_1 = require("../authz/permission-version-cache.js");
137
+ const session_restriction_store_js_1 = require("../session/session-restriction-store.js");
133
138
  const attempt_limiter_js_1 = require("./rate-limit/attempt-limiter.js");
139
+ const trust_proxy_check_js_1 = require("./rate-limit/trust-proxy-check.js");
134
140
  const tokens_js_2 = require("../request-auth/tokens.js");
135
141
  const mariadb_api_key_store_js_1 = require("../authentication/mariadb-api-key-store.js");
136
142
  const core_1 = require("@astram/core");
@@ -238,14 +244,53 @@ function makeCredentialResolverProvider(override) {
238
244
  ? { provide: tokens_js_1.SHIELD_CREDENTIAL_RESOLVER, useValue: override }
239
245
  : {
240
246
  provide: tokens_js_1.SHIELD_CREDENTIAL_RESOLVER,
241
- useFactory: (pool) => new mariadb_credential_resolver_js_1.MariaDbCredentialResolver(pool),
242
- inject: [tokens_js_1.SHIELD_DB_POOL],
247
+ // #7: the default resolver folds the principal's CONFIRMED MFA devices into a
248
+ // `totp` required factor, so enrolled MFA is enforced at every password login.
249
+ useFactory: (pool, devices) => new mariadb_credential_resolver_js_1.MariaDbCredentialResolver(pool, devices),
250
+ inject: [tokens_js_1.SHIELD_DB_POOL, tokens_js_1.SHIELD_MFA_DEVICE_STORE],
243
251
  };
244
252
  }
245
- /** Durable session record store (MariaDB adapter, injected with the live pool). */
253
+ /**
254
+ * The resolved, validated session lifetimes (ADR-0015) — the ONE source the session
255
+ * authority, the token binder and the auth controller read (security hardening #8).
256
+ */
257
+ const sessionLifetimesProvider = {
258
+ provide: index_js_1.SHIELD_SESSION_LIFETIMES,
259
+ useFactory: (config) => (0, core_1.resolveLifetimes)(config.lifetimes),
260
+ inject: [config_module_js_1.APP_CONFIG],
261
+ };
262
+ /**
263
+ * Durable session record store (MariaDB adapter, injected with the live pool), decorated
264
+ * by {@link HotRevokingSessionRecordStore} (M-A): a durable lineage / session revoke is
265
+ * followed by a Redis unbind, so a reuse-killed session's live access token dies on its
266
+ * next request (the request path stays Redis-only).
267
+ */
246
268
  const sessionRecordStoreProvider = {
247
269
  provide: index_js_1.SHIELD_SESSION_RECORD_STORE,
248
- useFactory: (pool) => new index_js_1.MariaDbSessionRecordStore(pool),
270
+ useFactory: (pool, binder) => {
271
+ const durable = new index_js_1.MariaDbSessionRecordStore(pool);
272
+ return new index_js_1.HotRevokingSessionRecordStore(durable, binder, durable);
273
+ },
274
+ inject: [tokens_js_1.SHIELD_DB_POOL, tokens_js_2.SHIELD_SESSION_TOKEN_BINDER],
275
+ };
276
+ /**
277
+ * The Redis permission-version cache (ADR-0035, GAPS #6): gate 5 reads the tenant's version from
278
+ * Redis (one GET) instead of MariaDB, and identity's bumper publishes to it after commit. Exported
279
+ * so the framework-built decision authority (ShieldModule) and the identity bumper inject it.
280
+ */
281
+ const permissionVersionCacheProvider = {
282
+ provide: tokens_js_1.SHIELD_PERMISSION_VERSION_CACHE,
283
+ useFactory: (redis) => new permission_version_cache_js_1.RedisPermissionVersionCache(redis),
284
+ inject: [tokens_js_1.SHIELD_REDIS],
285
+ };
286
+ /**
287
+ * The durable session-restriction store (Phase 3B review P1): the recovery-login re-enroll marker
288
+ * on `shield_identity.session` + the account status the refresh rebuild checks. Exported so the
289
+ * account controller (device confirm) clears the marker.
290
+ */
291
+ const sessionRestrictionStoreProvider = {
292
+ provide: tokens_js_1.SHIELD_SESSION_RESTRICTIONS,
293
+ useFactory: (pool) => new session_restriction_store_js_1.MariaDbSessionRestrictionStore(pool),
249
294
  inject: [tokens_js_1.SHIELD_DB_POOL],
250
295
  };
251
296
  /** Hot session store (Redis adapter, injected with the live client). */
@@ -262,7 +307,33 @@ const hotSessionStoreProvider = {
262
307
  */
263
308
  const sessionTokenBinderProvider = {
264
309
  provide: tokens_js_2.SHIELD_SESSION_TOKEN_BINDER,
265
- useFactory: (redis) => new session_token_binder_js_1.SessionTokenBinder(redis),
310
+ // #8: the session-claims (liveness) entry lives for the refresh idle window, and the
311
+ // previous access token keeps a 30 s grace after a rotation (plan D7).
312
+ useFactory: (redis, lifetimes) => new session_token_binder_js_1.SessionTokenBinder(redis, {
313
+ sessionClaimsTtlMs: lifetimes.refreshIdleMs,
314
+ rotateGraceMs: index_js_1.ROTATE_GRACE_MS,
315
+ }),
316
+ inject: [tokens_js_1.SHIELD_REDIS, index_js_1.SHIELD_SESSION_LIFETIMES],
317
+ };
318
+ /**
319
+ * The pending-MFA challenge store (M6): Redis-backed, so `/auth/mfa` completes on any
320
+ * instance, and it holds the password step's RESULT (principal + hint + credential
321
+ * fingerprint) — never the password. Bound under the {@link PendingMfaStore} class token
322
+ * the controller injects.
323
+ */
324
+ const pendingMfaStoreProvider = {
325
+ provide: pending_mfa_store_js_1.PendingMfaStore,
326
+ useFactory: (redis) => new pending_mfa_store_js_1.RedisPendingMfaStore(redis),
327
+ inject: [tokens_js_1.SHIELD_REDIS],
328
+ };
329
+ /**
330
+ * The TOTP-step limiter (#9) — 5 failures / 15-min window, keyed `mfa:pid:<principalId>`.
331
+ * Checked before the factor is verified; fails open on a Redis outage like every limiter
332
+ * (the code itself still gates — and its replay guard fails closed).
333
+ */
334
+ const mfaLimiterProvider = {
335
+ provide: tokens_js_1.SHIELD_MFA_LIMITER,
336
+ useFactory: (redis) => new attempt_limiter_js_1.AttemptLimiter(redis, { maxAttempts: 5, windowMs: 15 * 60 * 1000 }),
266
337
  inject: [tokens_js_1.SHIELD_REDIS],
267
338
  };
268
339
  /**
@@ -286,9 +357,14 @@ const serviceAuthenticatorProvider = {
286
357
  }),
287
358
  inject: [tokens_js_1.SHIELD_DB_POOL],
288
359
  };
360
+ /** Boot-time warnings from the auth composition (dev email logger, disabled email, …). */
361
+ const authBootLogger = new common_1.Logger('ShieldAuth');
289
362
  /**
290
363
  * The password-login rate limiter (spec §5 / H1) — a Redis-injected
291
- * {@link AttemptLimiter}, 10 failures / 15-min window, keyed `login:<hint>`. Same
364
+ * {@link AttemptLimiter}, 10 failures / 15-min window, keyed
365
+ * per ACCOUNT, `login:pid:<principalId>` (security hardening #5 / review F1: every spelling
366
+ * the DB collation maps to one account shares one budget; an unknown hint falls back to
367
+ * `login:<sha256(canonical hint)>`; no PII in the Redis key). Same
292
368
  * live-Redis provider pattern as {@link sessionTokenBinderProvider} (a `useFactory`
293
369
  * injecting {@link SHIELD_REDIS}); provided only under `auth: true`. Fail-open on a
294
370
  * Redis outage is the limiter's own behavior (never locks out on a blip).
@@ -298,6 +374,22 @@ const loginLimiterProvider = {
298
374
  useFactory: (redis) => new attempt_limiter_js_1.AttemptLimiter(redis, { maxAttempts: 10, windowMs: 15 * 60 * 1000 }),
299
375
  inject: [tokens_js_1.SHIELD_REDIS],
300
376
  };
377
+ /**
378
+ * The per-client-IP password-login FAILURE limiter (security hardening #5, plan D3) — a
379
+ * Redis-injected {@link AttemptLimiter} keyed `login:ip:<req.ip>`, default 50 failures /
380
+ * 15-min window, configurable through `AppConfig.loginIpLimit` (env
381
+ * `LOGIN_IP_MAX_FAILURES` / `LOGIN_IP_WINDOW_MS`). Stops password spraying across many
382
+ * distinct hints from one client. Failures only; never reset on success. Fails open on
383
+ * a Redis outage like every limiter. ENFORCED ONLY when the app set Express `trust proxy`
384
+ * explicitly (0 = no proxy, else the hop count); otherwise {@link TrustProxyBootCheck}
385
+ * switches it OFF at boot with a warning, because every client behind a proxy would share
386
+ * one IP and 50 failures would lock everyone out.
387
+ */
388
+ const loginIpLimiterProvider = {
389
+ provide: tokens_js_1.SHIELD_LOGIN_IP_LIMITER,
390
+ useFactory: (redis, config) => new attempt_limiter_js_1.AttemptLimiter(redis, config.loginIpLimit ?? app_config_js_1.DEFAULT_LOGIN_IP_LIMIT),
391
+ inject: [tokens_js_1.SHIELD_REDIS, config_module_js_1.APP_CONFIG],
392
+ };
301
393
  /**
302
394
  * The recovery-code rate limiter (spec §5) — a Redis-injected {@link AttemptLimiter},
303
395
  * 5 failures / 15-min window, keyed `recovery:<principalId>` (a lower ceiling than
@@ -312,7 +404,8 @@ const recoveryLimiterProvider = {
312
404
  };
313
405
  /**
314
406
  * The forgot-password rate limiter (feature #141) — a Redis-injected
315
- * {@link AttemptLimiter}, keyed BY CLIENT IP (`forgot:ip:<ip>`, 5 per 15-min window)
407
+ * {@link AttemptLimiter}, keyed BY CLIENT IP (`forgot:ip:<ip>`, 5 per 15-min window;
408
+ * enforced only with an explicit Express `trust proxy`, see {@link TrustProxyBootCheck})
316
409
  * AND, separately, per-hint (`forgot:<hint>`, provided as {@link forgotHintLimiterProvider}).
317
410
  * NOTE the "attempt" semantics here differ from login: forgot-password is ALWAYS 204
318
411
  * (returning 429 would itself be an enumeration signal), so this limiter counts every
@@ -330,7 +423,7 @@ const forgotLimiterProvider = {
330
423
  };
331
424
  /**
332
425
  * The forgot-password PER-HINT rate limiter (feature #141) — a Redis-injected
333
- * {@link AttemptLimiter}, keyed by the presented hint (`forgot:<hint>`, 3 per 60-min
426
+ * {@link AttemptLimiter}, keyed per account (`forgot:pid:<principalId>`, unknown → `forgot:<sha256(canonical)>`), 3 per 60-min
334
427
  * window). This caps how often ANY caller (across IPs) can trigger a reset email for
335
428
  * ONE account, so a single victim's inbox can't be email-bombed even from a rotating
336
429
  * IP pool. A GENEROUS per-hint ceiling (a real user rarely needs >3 reset emails/hour)
@@ -344,7 +437,8 @@ const forgotHintLimiterProvider = {
344
437
  };
345
438
  /**
346
439
  * The reset-password rate limiter (feature #141) — a Redis-injected
347
- * {@link AttemptLimiter}, keyed by CLIENT IP (`reset:ip:<ip>`, 10 per 15-min window).
440
+ * {@link AttemptLimiter}, keyed by CLIENT IP (`reset:ip:<ip>`, 10 per 15-min window;
441
+ * enforced only with an explicit Express `trust proxy`, see {@link TrustProxyBootCheck}).
348
442
  * Reset is token-gated (a 256-bit single-use token), so this is DoS/abuse throttling,
349
443
  * NOT anti-brute-force — and unlike forgot it MAY return a generic 429 on lockout (a
350
444
  * token-gated endpoint is not enumeration-sensitive). `recordFailure` on a bad/invalid
@@ -382,7 +476,7 @@ const clockProvider = {
382
476
  const sessionAuthorityProvider = {
383
477
  provide: tokens_js_1.SHIELD_SESSION_AUTHORITY,
384
478
  // async: loads the core factory lazily (CJS→ESM, see loadCore()); Nest awaits it.
385
- useFactory: async (recordStore, hotStore, tokenGenerator, refreshHasher, clock, config) => {
479
+ useFactory: async (recordStore, hotStore, tokenGenerator, refreshHasher, clock, lifetimes) => {
386
480
  const { createSessionAuthority } = await loadCore();
387
481
  return createSessionAuthority({
388
482
  recordStore,
@@ -390,7 +484,7 @@ const sessionAuthorityProvider = {
390
484
  tokenGenerator,
391
485
  refreshHasher,
392
486
  clock,
393
- lifetimes: config.lifetimes,
487
+ lifetimes,
394
488
  });
395
489
  },
396
490
  inject: [
@@ -399,7 +493,7 @@ const sessionAuthorityProvider = {
399
493
  index_js_1.SHIELD_TOKEN_GENERATOR,
400
494
  index_js_1.SHIELD_REFRESH_HASHER,
401
495
  index_js_1.SHIELD_CLOCK,
402
- config_module_js_1.APP_CONFIG,
496
+ index_js_1.SHIELD_SESSION_LIFETIMES,
403
497
  ],
404
498
  };
405
499
  /**
@@ -411,18 +505,28 @@ const sessionAuthorityProvider = {
411
505
  * The override reaches this async factory via `inject` (not a captured closure) so
412
506
  * the default binding stays exactly as before when no override is plugged.
413
507
  */
508
+ /**
509
+ * The LOGIN `totp` verifier ({@link SHIELD_LOGIN_TOTP_VERIFIER}) — ALWAYS the
510
+ * OR-across-devices wrapper: it delegates to the resolved base (the consumer
511
+ * `totpOverride` when supplied, else the framework's single-secret RFC-6238 verifier) per
512
+ * device secret, so a principal's multiple confirmed devices OR-match, and it claims each
513
+ * accepted code in Redis (M-B replay guard, fail closed). ONE instance, shared by the
514
+ * authenticator and the MFA step-up.
515
+ */
516
+ const loginTotpVerifierProvider = {
517
+ provide: tokens_js_1.SHIELD_LOGIN_TOTP_VERIFIER,
518
+ useFactory: async (totpOverride, mfaDeviceStore, redis) => {
519
+ const { createTotpFactorVerifier } = await loadCore();
520
+ const base = resolveTotpVerifier(totpOverride, createTotpFactorVerifier({ hmacSha1: exports.hmacSha1 }));
521
+ return new mariadb_or_totp_verifier_js_1.OrTotpVerifier(base, mfaDeviceStore, redis);
522
+ },
523
+ inject: [tokens_js_1.SHIELD_TOTP_VERIFIER_OVERRIDE, tokens_js_1.SHIELD_MFA_DEVICE_STORE, tokens_js_1.SHIELD_REDIS],
524
+ };
414
525
  const authenticatorProvider = {
415
526
  provide: tokens_js_1.SHIELD_AUTHENTICATOR,
416
527
  // async: loads the core factories lazily (CJS→ESM, see loadCore()); Nest awaits it.
417
- useFactory: async (hasher, resolver, totpOverride, rootIds, mfaDeviceStore) => {
418
- const { createAuthenticator, createIdentityBroker, createTotpFactorVerifier } = await loadCore();
419
- // The login totp verifier is ALWAYS the OR-across-devices wrapper: it delegates
420
- // to the resolved base (the consumer `totpOverride` when supplied, else the
421
- // framework's single-secret RFC-6238 verifier) per device secret, so a
422
- // principal's multiple confirmed devices OR-match. A consumer override still wins
423
- // (it's the base inside the OR-wrap); multi-device works by default.
424
- const base = resolveTotpVerifier(totpOverride, createTotpFactorVerifier({ hmacSha1: exports.hmacSha1 }));
425
- const totp = new mariadb_or_totp_verifier_js_1.OrTotpVerifier(base, mfaDeviceStore);
528
+ useFactory: async (hasher, resolver, totp, rootIds) => {
529
+ const { createAuthenticator, createIdentityBroker } = await loadCore();
426
530
  return createAuthenticator({
427
531
  hasher,
428
532
  resolver,
@@ -437,11 +541,25 @@ const authenticatorProvider = {
437
541
  inject: [
438
542
  argon2id_password_hasher_js_1.SHIELD_PASSWORD_HASHER,
439
543
  tokens_js_1.SHIELD_CREDENTIAL_RESOLVER,
440
- tokens_js_1.SHIELD_TOTP_VERIFIER_OVERRIDE,
544
+ tokens_js_1.SHIELD_LOGIN_TOTP_VERIFIER,
441
545
  tokens_js_1.SHIELD_ROOT_IDS,
442
- tokens_js_1.SHIELD_MFA_DEVICE_STORE,
443
546
  ],
444
547
  };
548
+ /**
549
+ * The MFA step-up (M6) behind `/auth/mfa`'s TOTP branch: verifies ONLY the second factor
550
+ * for the principal the password step recorded, through the SAME resolver, the SAME login
551
+ * TOTP verifier (replay guard included) and an identity broker with the SAME root
552
+ * allowlist as {@link authenticatorProvider} — so the minted principal is identical to the
553
+ * one a single-step login would produce. No password is held between the two requests.
554
+ */
555
+ const mfaStepUpProvider = {
556
+ provide: tokens_js_1.SHIELD_MFA_STEP_UP,
557
+ useFactory: async (resolver, totp, rootIds) => {
558
+ const { createIdentityBroker } = await loadCore();
559
+ return new mfa_step_up_js_1.CredentialMfaStepUp(resolver, [totp], createIdentityBroker({ internalRootIds: rootIds ?? [] }));
560
+ },
561
+ inject: [tokens_js_1.SHIELD_CREDENTIAL_RESOLVER, tokens_js_1.SHIELD_LOGIN_TOTP_VERIFIER, tokens_js_1.SHIELD_ROOT_IDS],
562
+ };
445
563
  /**
446
564
  * Bind the OPTIONAL login `totp` verifier override (the Phase-2 multi-device seam).
447
565
  * Same value-provider pattern as the other seams: the supplied verifier when present,
@@ -526,10 +644,48 @@ function makeResetTokenStoreProvider(override) {
526
644
  inject: [tokens_js_1.SHIELD_DB_POOL],
527
645
  };
528
646
  }
647
+ /**
648
+ * Bind the {@link EmailSender} (security hardening #6):
649
+ * - a supplied sender → used verbatim (any environment);
650
+ * - `'disabled'` → {@link NoopEmailSender} + one startup warning (forgot-password stays 204;
651
+ * nothing is delivered or printed);
652
+ * - omitted → the dev {@link LogEmailSender} ONLY when the RAW `NODE_ENV` is explicitly
653
+ * `development` or `test` (+ one startup warning, since it prints live reset links);
654
+ * anything else, INCLUDING an unset `NODE_ENV`, THROWS at composition (boot fails closed).
655
+ * `NODE_ENV` is read when the factory runs (module compile), not at `forRoot()` time.
656
+ */
529
657
  function makeEmailSenderProvider(override) {
530
- return override
531
- ? { provide: tokens_js_1.SHIELD_EMAIL_SENDER, useValue: override }
532
- : { provide: tokens_js_1.SHIELD_EMAIL_SENDER, useFactory: () => new log_email_sender_js_1.LogEmailSender() };
658
+ if (override === 'disabled') {
659
+ return {
660
+ provide: tokens_js_1.SHIELD_EMAIL_SENDER,
661
+ useFactory: () => {
662
+ authBootLogger.warn("emailSender: 'disabled' — password-reset emails are NOT delivered " +
663
+ '(forgot-password still answers 204). Supply a real EmailSender to enable resets.');
664
+ return new noop_email_sender_js_1.NoopEmailSender();
665
+ },
666
+ };
667
+ }
668
+ if (override)
669
+ return { provide: tokens_js_1.SHIELD_EMAIL_SENDER, useValue: override };
670
+ return {
671
+ provide: tokens_js_1.SHIELD_EMAIL_SENDER,
672
+ useFactory: () => resolveDefaultEmailSender(process.env.NODE_ENV),
673
+ };
674
+ }
675
+ /**
676
+ * The no-override default sender decision (pure apart from the warning). Exported for
677
+ * the unit test only — not part of the package's public surface.
678
+ */
679
+ function resolveDefaultEmailSender(rawNodeEnv) {
680
+ if (!(0, app_config_js_1.isDevOrTestNodeEnv)(rawNodeEnv)) {
681
+ throw new Error("Astram auth composition: auth:true requires `emailSender` (or `emailSender: 'disabled'`) " +
682
+ `when NODE_ENV is not 'development' or 'test' (got: ${rawNodeEnv === undefined ? 'unset' : `'${rawNodeEnv}'`}). ` +
683
+ 'The dev LogEmailSender prints live password-reset links to stdout, so it is only ' +
684
+ 'used when NODE_ENV is explicitly development/test.');
685
+ }
686
+ authBootLogger.warn(`Using the dev LogEmailSender (NODE_ENV=${rawNodeEnv}): password-reset emails are printed ` +
687
+ 'to stdout, reset links included. Never run like this in production — supply an emailSender.');
688
+ return new log_email_sender_js_1.LogEmailSender();
533
689
  }
534
690
  function makeRecoveryCodeStoreProvider(override) {
535
691
  return override
@@ -690,18 +846,29 @@ let AuthModule = AuthModule_1 = class AuthModule {
690
846
  // The reset-link base URL value the forgot-password handler composes into the
691
847
  // emailed link (framework-owned URL composition; EmailSender stays dumb).
692
848
  resetLinkBaseUrlProvider,
849
+ // The resolved session lifetimes (#8) — one source for authority, binder, controller.
850
+ sessionLifetimesProvider,
693
851
  sessionRecordStoreProvider,
852
+ // ADR-0035: the Redis permission-version cache (gate 5 + the identity bumper).
853
+ permissionVersionCacheProvider,
854
+ // Phase 3B review P1: the durable re-enroll marker + account status for the rebuild.
855
+ sessionRestrictionStoreProvider,
694
856
  hotSessionStoreProvider,
695
857
  tokenGeneratorProvider,
696
858
  refreshHasherProvider,
697
859
  clockProvider,
698
860
  sessionAuthorityProvider,
861
+ // The login TOTP verifier (OR across devices + Redis replay guard, M-B), shared by
862
+ // the authenticator and the MFA step-up.
863
+ loginTotpVerifierProvider,
699
864
  authenticatorProvider,
700
- // The core TOTP verifier (login MFA + account MFA enroll/disable share it).
865
+ // The MFA step-up (M6): `/auth/mfa` verifies only the second factor.
866
+ mfaStepUpProvider,
867
+ // The single-secret TOTP verifier for account MFA enroll/disable.
701
868
  totpVerifierProvider,
702
- // Team C's server-side, single-use, TTL'd challenge store backing the
703
- // login→mfa handoff (a plain class provider the controller injects).
704
- pending_mfa_store_js_1.PendingMfaStore,
869
+ // The server-side, single-use, TTL'd challenge store backing the login→mfa
870
+ // handoff — Redis-backed, never holds the password (M6).
871
+ pendingMfaStoreProvider,
705
872
  // The request-auth token binder (access-token → principal claims).
706
873
  sessionTokenBinderProvider,
707
874
  // The service-account API-key authenticator (ADR-0033) — `ask_…` bearer → service_account
@@ -711,7 +878,15 @@ let AuthModule = AuthModule_1 = class AuthModule {
711
878
  // a Redis-injected AttemptLimiter with its own threshold. Injected into
712
879
  // AuthController (@Optional()).
713
880
  loginLimiterProvider,
881
+ // #5: the per-client-IP login failure limiter (D3: 50 / 15 min, configurable) +
882
+ // the boot-time `trust proxy` check that enables ALL per-IP limiters (login,
883
+ // forgot, reset) on an explicit trust proxy, or switches them OFF with a warning
884
+ // (unset → req.ip would be the shared proxy IP).
885
+ loginIpLimiterProvider,
886
+ trust_proxy_check_js_1.TrustProxyBootCheck,
714
887
  recoveryLimiterProvider,
888
+ // #9: the TOTP-step limiter (5 / 15 min per account, `mfa:pid:<principalId>`).
889
+ mfaLimiterProvider,
715
890
  // The forgot/reset-password limiters (feature #141): forgot keyed by client IP
716
891
  // + per-hint (request-rate caps → silently skip the send when exceeded, stays
717
892
  // 204); reset keyed by client IP (may 429 on lockout — token-gated, not enum-
@@ -748,6 +923,13 @@ let AuthModule = AuthModule_1 = class AuthModule {
748
923
  tokens_js_1.SHIELD_TOTP_VERIFIER,
749
924
  // The eager session-facts projector (consumed by AuthController).
750
925
  tokens_js_1.SHIELD_SESSION_FACTS_PROJECTOR,
926
+ // The scope-subtree resolver — exported so the (app-wide) enforcement guard can re-derive
927
+ // a session's subtree when gate 5 reloads at a newer permission version (Phase 1C).
928
+ tokens_js_1.SHIELD_SCOPE_SUBTREE_RESOLVER,
929
+ // ADR-0035: the Redis permission-version cache — the framework-built decision authority
930
+ // reads it (gate 5) and `@astram/identity`'s bumper publishes to it after commit.
931
+ tokens_js_1.SHIELD_PERMISSION_VERSION_CACHE,
932
+ tokens_js_1.SHIELD_SESSION_RESTRICTIONS,
751
933
  ],
752
934
  };
753
935
  }
@@ -24,7 +24,7 @@
24
24
  import { type OnModuleDestroy } from '@nestjs/common';
25
25
  import { type Pool } from 'mariadb';
26
26
  import Redis from 'ioredis';
27
- import type { AppConfig } from '../config/app-config.js';
27
+ import { type AppConfig } from '../config/app-config.js';
28
28
  /**
29
29
  * Create the ONE live `mariadb` {@link Pool} for the `shield_core` account from
30
30
  * {@link AppConfig}. The `connectionLimit` comes from the ADR-0018 budget
@@ -1 +1 @@
1
- {"version":3,"file":"connections.d.ts","sourceRoot":"","sources":["../../src/auth/connections.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAc,KAAK,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAClE,OAAgB,EAAE,KAAK,IAAI,EAAE,MAAM,SAAS,CAAC;AAC7C,OAAO,KAAK,MAAM,SAAS,CAAC;AAC5B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAEzD;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CAcpD;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,SAAS,GAAG,KAAK,CAI1D;AAED;;;;;;GAMG;AACH,qBACa,mBAAoB,YAAW,eAAe;IAEvD,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,KAAK;gBADL,IAAI,EAAE,IAAI,EACV,KAAK,EAAE,KAAK;IAGzB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC;CAavC"}
1
+ {"version":3,"file":"connections.d.ts","sourceRoot":"","sources":["../../src/auth/connections.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAc,KAAK,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAClE,OAAgB,EAAE,KAAK,IAAI,EAAE,MAAM,SAAS,CAAC;AAC7C,OAAO,KAAK,MAAM,SAAS,CAAC;AAC5B,OAAO,EAAoC,KAAK,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAE3F;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CAcpD;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,SAAS,GAAG,KAAK,CAc1D;AAED;;;;;;GAMG;AACH,qBACa,mBAAoB,YAAW,eAAe;IAEvD,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,KAAK;gBADL,IAAI,EAAE,IAAI,EACV,KAAK,EAAE,KAAK;IAGzB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC;CAavC"}
@@ -41,6 +41,7 @@ exports.createRedisClient = createRedisClient;
41
41
  const common_1 = require("@nestjs/common");
42
42
  const mariadb_1 = __importDefault(require("mariadb"));
43
43
  const ioredis_1 = __importDefault(require("ioredis"));
44
+ const app_config_js_1 = require("../config/app-config.js");
44
45
  /**
45
46
  * Create the ONE live `mariadb` {@link Pool} for the `shield_core` account from
46
47
  * {@link AppConfig}. The `connectionLimit` comes from the ADR-0018 budget
@@ -75,9 +76,19 @@ function createDbPool(config) {
75
76
  * `unavailable` hot-store outcome (fail-closed), never a silent `absent`.
76
77
  */
77
78
  function createRedisClient(config) {
78
- // `maxRetriesPerRequest: null` keeps a transient blip from turning into a thrown
79
- // command (the hot-store adapter classifies unreachable as `unavailable`).
80
- return new ioredis_1.default(config.redis.url, { maxRetriesPerRequest: null, lazyConnect: false });
79
+ // BOUNDED (GAPS #7, security hardening Phase 3). Previously `maxRetriesPerRequest: null`
80
+ // queued every command while Redis was down, so a classified request HUNG until Redis came
81
+ // back instead of failing closed (ADR-0013: "if Redis cannot answer, the request is denied").
82
+ // Now each command fails after `commandTimeout` (default 2 s, `REDIS_COMMAND_TIMEOUT_MS`) or
83
+ // after one reconnect attempt; every caller treats a Redis error as its fail-closed outcome
84
+ // (401 / 503), except the rate limiters, which fail OPEN by design (never a lockout blip).
85
+ // Reconnects keep ioredis's default strategy. Nothing in the framework relies on queued
86
+ // commands (no pub/sub, no blocking reads, no MULTI/pipeline).
87
+ return new ioredis_1.default(config.redis.url, {
88
+ maxRetriesPerRequest: 1,
89
+ commandTimeout: config.redis.commandTimeoutMs ?? app_config_js_1.DEFAULT_REDIS_COMMAND_TIMEOUT_MS,
90
+ lazyConnect: false,
91
+ });
81
92
  }
82
93
  /**
83
94
  * Lifecycle-aware holder that owns the pool + redis handles and closes them on
@@ -0,0 +1,26 @@
1
+ /**
2
+ * The framework-DEFAULT session-facts projector (ADR-0035, GAPS #6). Without a projector, a session
3
+ * carries no `cachedFacts` and gate 5 re-reads the grants from the DB on every request. When the app
4
+ * hands ShieldModule its `recordAuthorization` (and no explicit `sessionFactsProjector`), the facts
5
+ * the decision authority uses are the right ones to cache: this projector delegates to that SAME
6
+ * `PermissionFactsPort.loadCoarseGrants` (one definition of a principal's grants and live roles).
7
+ *
8
+ * DEFERRED: `AuthModule` is composed statically, but `recordAuthorization` may be resolved later
9
+ * (`forRootAsync`), and the port is only known when the decision authority is built. ShieldModule
10
+ * creates one instance per registration, hands it to `AuthModule`, and attaches the port when it
11
+ * builds the authority. Until attached it is INACTIVE: the auth controller then behaves exactly as
12
+ * with no projector (nothing cached at login, the existing cache carried forward at refresh).
13
+ *
14
+ * Free of runtime `@astram/core` imports (type-only).
15
+ */
16
+ import type { CoarseGrants, PermissionFactsPort } from '@astram/core';
17
+ import type { SessionFactsProjector } from './ports/session-facts-projector.js';
18
+ export declare class DeferredSessionFactsProjector implements SessionFactsProjector {
19
+ private source;
20
+ /** Attach the decision authority's facts port (once; a second attach is ignored). */
21
+ attach(source: Pick<PermissionFactsPort, 'loadCoarseGrants'>): void;
22
+ /** Whether a port was attached (inactive ⇒ the controller treats it as "no projector"). */
23
+ isActive(): boolean;
24
+ project(principalId: string, tenantId: string): Promise<CoarseGrants | null>;
25
+ }
26
+ //# sourceMappingURL=default-session-facts-projector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"default-session-facts-projector.d.ts","sourceRoot":"","sources":["../../src/auth/default-session-facts-projector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACtE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,oCAAoC,CAAC;AAEhF,qBAAa,6BAA8B,YAAW,qBAAqB;IACzE,OAAO,CAAC,MAAM,CAA4D;IAE1E,qFAAqF;IACrF,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,mBAAmB,EAAE,kBAAkB,CAAC,GAAG,IAAI;IAInE,2FAA2F;IAC3F,QAAQ,IAAI,OAAO;IAIb,OAAO,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC;CAInF"}
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DeferredSessionFactsProjector = void 0;
4
+ class DeferredSessionFactsProjector {
5
+ source;
6
+ /** Attach the decision authority's facts port (once; a second attach is ignored). */
7
+ attach(source) {
8
+ if (this.source === undefined)
9
+ this.source = source;
10
+ }
11
+ /** Whether a port was attached (inactive ⇒ the controller treats it as "no projector"). */
12
+ isActive() {
13
+ return this.source !== undefined;
14
+ }
15
+ async project(principalId, tenantId) {
16
+ if (this.source === undefined)
17
+ return null;
18
+ return this.source.loadCoarseGrants(principalId, tenantId);
19
+ }
20
+ }
21
+ exports.DeferredSessionFactsProjector = DeferredSessionFactsProjector;
@@ -0,0 +1,32 @@
1
+ import type { Credential, CredentialResolver, FactorVerifier, IdentityBroker, Principal, ResolvedCredential } from '@astram/core';
2
+ import type { PendingMfaChallenge } from './pending-mfa-store.js';
3
+ /** The step-up outcome the controller maps to 200 / 401 / 503. */
4
+ export type MfaStepUpResult = {
5
+ readonly outcome: 'authenticated';
6
+ readonly principal: Principal;
7
+ } | {
8
+ readonly outcome: 'rejected';
9
+ } | {
10
+ readonly outcome: 'unavailable';
11
+ };
12
+ /** The seam the controller injects under `SHIELD_MFA_STEP_UP`. */
13
+ export interface MfaStepUp {
14
+ verify(challenge: PendingMfaChallenge, credential: Credential): Promise<MfaStepUpResult>;
15
+ }
16
+ /** SHA-256 of the stored credential hash — binds a challenge to the credential state. */
17
+ export declare function credentialFingerprint(resolved: Pick<ResolvedCredential, 'passwordHash'>): string;
18
+ /**
19
+ * `true` iff `resolved` is still the credential the challenge was issued for: same
20
+ * principal, same stored credential hash. Shared by the TOTP step-up and the
21
+ * recovery-code branch.
22
+ */
23
+ export declare function challengeMatches(resolved: ResolvedCredential, challenge: PendingMfaChallenge): boolean;
24
+ /** The framework-default {@link MfaStepUp} over the injected core ports. */
25
+ export declare class CredentialMfaStepUp implements MfaStepUp {
26
+ private readonly resolver;
27
+ private readonly broker;
28
+ private readonly verifiers;
29
+ constructor(resolver: Pick<CredentialResolver, 'resolve'>, factorVerifiers: readonly FactorVerifier[], broker: IdentityBroker);
30
+ verify(challenge: PendingMfaChallenge, credential: Credential): Promise<MfaStepUpResult>;
31
+ }
32
+ //# sourceMappingURL=mfa-step-up.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mfa-step-up.d.ts","sourceRoot":"","sources":["../../src/auth/mfa-step-up.ts"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EACV,UAAU,EACV,kBAAkB,EAClB,cAAc,EACd,cAAc,EACd,SAAS,EACT,kBAAkB,EACnB,MAAM,cAAc,CAAC;AACtB,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAElE,kEAAkE;AAClE,MAAM,MAAM,eAAe,GACvB;IAAE,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAC;IAAC,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAA;CAAE,GACpE;IAAE,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAA;CAAE,GAChC;IAAE,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAA;CAAE,CAAC;AAExC,kEAAkE;AAClE,MAAM,WAAW,SAAS;IACxB,MAAM,CAAC,SAAS,EAAE,mBAAmB,EAAE,UAAU,EAAE,UAAU,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;CAC1F;AAED,yFAAyF;AACzF,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,IAAI,CAAC,kBAAkB,EAAE,cAAc,CAAC,GAAG,MAAM,CAIhG;AAQD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,kBAAkB,EAC5B,SAAS,EAAE,mBAAmB,GAC7B,OAAO,CAKT;AAKD,4EAA4E;AAC5E,qBAAa,mBAAoB,YAAW,SAAS;IAIjD,OAAO,CAAC,QAAQ,CAAC,QAAQ;IAEzB,OAAO,CAAC,QAAQ,CAAC,MAAM;IALzB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAqC;gBAG5C,QAAQ,EAAE,IAAI,CAAC,kBAAkB,EAAE,SAAS,CAAC,EAC9D,eAAe,EAAE,SAAS,cAAc,EAAE,EACzB,MAAM,EAAE,cAAc;IAUnC,MAAM,CAAC,SAAS,EAAE,mBAAmB,EAAE,UAAU,EAAE,UAAU,GAAG,OAAO,CAAC,eAAe,CAAC;CAqC/F"}