@astram/guard 0.6.0 → 0.8.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 (124) hide show
  1. package/dist/auth/account.controller.d.ts +3 -1
  2. package/dist/auth/account.controller.d.ts.map +1 -1
  3. package/dist/auth/account.controller.js +12 -5
  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 +241 -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 +46 -0
  36. package/dist/authentication/mariadb-api-key-store.d.ts.map +1 -0
  37. package/dist/authentication/mariadb-api-key-store.js +111 -0
  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 +52 -0
  68. package/dist/config/app-config.d.ts.map +1 -1
  69. package/dist/config/app-config.js +66 -4
  70. package/dist/encryption/local-kek-crypto.d.ts +15 -0
  71. package/dist/encryption/local-kek-crypto.d.ts.map +1 -0
  72. package/dist/encryption/local-kek-crypto.js +79 -0
  73. package/dist/enforcement/enforcement.module.d.ts.map +1 -1
  74. package/dist/enforcement/enforcement.module.js +9 -0
  75. package/dist/enforcement/engine-error.filter.d.ts +57 -0
  76. package/dist/enforcement/engine-error.filter.d.ts.map +1 -0
  77. package/dist/enforcement/engine-error.filter.js +118 -0
  78. package/dist/enforcement/global-enforcement.guard.d.ts +31 -2
  79. package/dist/enforcement/global-enforcement.guard.d.ts.map +1 -1
  80. package/dist/enforcement/global-enforcement.guard.js +148 -6
  81. package/dist/identity/auth-facts.controller.d.ts.map +1 -1
  82. package/dist/identity/identity-actions.d.ts +9 -0
  83. package/dist/identity/identity-actions.d.ts.map +1 -1
  84. package/dist/identity/identity-actions.js +16 -1
  85. package/dist/identity/permission-facts.adapter.d.ts.map +1 -1
  86. package/dist/identity/permission-facts.adapter.js +2 -3
  87. package/dist/index.d.ts +17 -7
  88. package/dist/index.d.ts.map +1 -1
  89. package/dist/index.js +82 -25
  90. package/dist/module/shield.module.d.ts +9 -2
  91. package/dist/module/shield.module.d.ts.map +1 -1
  92. package/dist/module/shield.module.js +47 -38
  93. package/dist/request-auth/request-principal.d.ts +11 -3
  94. package/dist/request-auth/request-principal.d.ts.map +1 -1
  95. package/dist/request-auth/request-principal.js +58 -2
  96. package/dist/request-auth/session-facts-als.d.ts +16 -1
  97. package/dist/request-auth/session-facts-als.d.ts.map +1 -1
  98. package/dist/request-auth/session-facts-als.js +23 -0
  99. package/dist/request-auth/session-token-binder.d.ts +166 -10
  100. package/dist/request-auth/session-token-binder.d.ts.map +1 -1
  101. package/dist/request-auth/session-token-binder.js +408 -28
  102. package/dist/request-auth/tokens.d.ts +10 -0
  103. package/dist/request-auth/tokens.d.ts.map +1 -1
  104. package/dist/request-auth/tokens.js +11 -1
  105. package/dist/secrets/encrypted-file-secrets.d.ts +16 -0
  106. package/dist/secrets/encrypted-file-secrets.d.ts.map +1 -0
  107. package/dist/secrets/encrypted-file-secrets.js +98 -0
  108. package/dist/session/hot-revoking-session-record-store.d.ts +46 -0
  109. package/dist/session/hot-revoking-session-record-store.d.ts.map +1 -0
  110. package/dist/session/hot-revoking-session-record-store.js +58 -0
  111. package/dist/session/index.d.ts +3 -1
  112. package/dist/session/index.d.ts.map +1 -1
  113. package/dist/session/index.js +8 -1
  114. package/dist/session/mariadb-session-record-store.d.ts +6 -0
  115. package/dist/session/mariadb-session-record-store.d.ts.map +1 -1
  116. package/dist/session/mariadb-session-record-store.js +16 -1
  117. package/dist/session/session-lifetimes.d.ts +30 -0
  118. package/dist/session/session-lifetimes.d.ts.map +1 -0
  119. package/dist/session/session-lifetimes.js +21 -0
  120. package/dist/session/session-restriction-store.d.ts +43 -0
  121. package/dist/session/session-restriction-store.d.ts.map +1 -0
  122. package/dist/session/session-restriction-store.js +35 -0
  123. package/migrations/1710000000017-CreateServiceAccountKey.cjs +41 -0
  124. package/package.json +3 -3
@@ -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,9 +131,15 @@ 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");
141
+ const mariadb_api_key_store_js_1 = require("../authentication/mariadb-api-key-store.js");
142
+ const core_1 = require("@astram/core");
135
143
  const session_token_binder_js_1 = require("../request-auth/session-token-binder.js");
136
144
  /**
137
145
  * The injected HMAC-SHA1 primitive the core TOTP verifier needs (Core is zero-dep
@@ -236,14 +244,53 @@ function makeCredentialResolverProvider(override) {
236
244
  ? { provide: tokens_js_1.SHIELD_CREDENTIAL_RESOLVER, useValue: override }
237
245
  : {
238
246
  provide: tokens_js_1.SHIELD_CREDENTIAL_RESOLVER,
239
- useFactory: (pool) => new mariadb_credential_resolver_js_1.MariaDbCredentialResolver(pool),
240
- 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],
241
251
  };
242
252
  }
243
- /** 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
+ */
244
268
  const sessionRecordStoreProvider = {
245
269
  provide: index_js_1.SHIELD_SESSION_RECORD_STORE,
246
- 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),
247
294
  inject: [tokens_js_1.SHIELD_DB_POOL],
248
295
  };
249
296
  /** Hot session store (Redis adapter, injected with the live client). */
@@ -260,12 +307,64 @@ const hotSessionStoreProvider = {
260
307
  */
261
308
  const sessionTokenBinderProvider = {
262
309
  provide: tokens_js_2.SHIELD_SESSION_TOKEN_BINDER,
263
- 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),
264
327
  inject: [tokens_js_1.SHIELD_REDIS],
265
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 }),
337
+ inject: [tokens_js_1.SHIELD_REDIS],
338
+ };
339
+ /**
340
+ * The service-account API-key authenticator (ADR-0033) — resolves an `Authorization: Bearer ask_…`
341
+ * into a `service_account` Principal. Built HERE (in the guard's own AuthModule) over the live
342
+ * {@link SHIELD_DB_POOL} + the framework Argon2id hasher, and EXPORTED so the `APP_GUARD` (in this
343
+ * same module tree) injects it — the SAME reason the {@link sessionTokenBinderProvider} lives here.
344
+ *
345
+ * WHY HERE, not in the consumer's identity module: the enforcement guard is provided by ShieldModule;
346
+ * a token provided in a later, sibling feature module is NOT visible to it (that DI boundary made an
347
+ * identity-module-provided authenticator resolve to `undefined` → an `ask_…` bearer 401'd). Providing
348
+ * it alongside the guard (like the binder) is what makes the machine auth path work out-of-the-box
349
+ * under `auth: true`. The store reads `shield_authz.service_account_key` (created by the framework
350
+ * migration); the account admin surface lives in @astram/identity, but AUTHENTICATION is the guard's.
351
+ */
352
+ const serviceAuthenticatorProvider = {
353
+ provide: tokens_js_2.SHIELD_SERVICE_AUTHENTICATOR,
354
+ useFactory: (pool) => (0, core_1.createServiceAuthenticator)({
355
+ store: new mariadb_api_key_store_js_1.MariaDbApiKeyStore(pool),
356
+ hasher: new argon2id_password_hasher_js_1.Argon2idPasswordHasher(),
357
+ }),
358
+ inject: [tokens_js_1.SHIELD_DB_POOL],
359
+ };
360
+ /** Boot-time warnings from the auth composition (dev email logger, disabled email, …). */
361
+ const authBootLogger = new common_1.Logger('ShieldAuth');
266
362
  /**
267
363
  * The password-login rate limiter (spec §5 / H1) — a Redis-injected
268
- * {@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
269
368
  * live-Redis provider pattern as {@link sessionTokenBinderProvider} (a `useFactory`
270
369
  * injecting {@link SHIELD_REDIS}); provided only under `auth: true`. Fail-open on a
271
370
  * Redis outage is the limiter's own behavior (never locks out on a blip).
@@ -275,6 +374,22 @@ const loginLimiterProvider = {
275
374
  useFactory: (redis) => new attempt_limiter_js_1.AttemptLimiter(redis, { maxAttempts: 10, windowMs: 15 * 60 * 1000 }),
276
375
  inject: [tokens_js_1.SHIELD_REDIS],
277
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
+ };
278
393
  /**
279
394
  * The recovery-code rate limiter (spec §5) — a Redis-injected {@link AttemptLimiter},
280
395
  * 5 failures / 15-min window, keyed `recovery:<principalId>` (a lower ceiling than
@@ -289,7 +404,8 @@ const recoveryLimiterProvider = {
289
404
  };
290
405
  /**
291
406
  * The forgot-password rate limiter (feature #141) — a Redis-injected
292
- * {@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})
293
409
  * AND, separately, per-hint (`forgot:<hint>`, provided as {@link forgotHintLimiterProvider}).
294
410
  * NOTE the "attempt" semantics here differ from login: forgot-password is ALWAYS 204
295
411
  * (returning 429 would itself be an enumeration signal), so this limiter counts every
@@ -307,7 +423,7 @@ const forgotLimiterProvider = {
307
423
  };
308
424
  /**
309
425
  * The forgot-password PER-HINT rate limiter (feature #141) — a Redis-injected
310
- * {@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
311
427
  * window). This caps how often ANY caller (across IPs) can trigger a reset email for
312
428
  * ONE account, so a single victim's inbox can't be email-bombed even from a rotating
313
429
  * IP pool. A GENEROUS per-hint ceiling (a real user rarely needs >3 reset emails/hour)
@@ -321,7 +437,8 @@ const forgotHintLimiterProvider = {
321
437
  };
322
438
  /**
323
439
  * The reset-password rate limiter (feature #141) — a Redis-injected
324
- * {@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}).
325
442
  * Reset is token-gated (a 256-bit single-use token), so this is DoS/abuse throttling,
326
443
  * NOT anti-brute-force — and unlike forgot it MAY return a generic 429 on lockout (a
327
444
  * token-gated endpoint is not enumeration-sensitive). `recordFailure` on a bad/invalid
@@ -359,7 +476,7 @@ const clockProvider = {
359
476
  const sessionAuthorityProvider = {
360
477
  provide: tokens_js_1.SHIELD_SESSION_AUTHORITY,
361
478
  // async: loads the core factory lazily (CJS→ESM, see loadCore()); Nest awaits it.
362
- useFactory: async (recordStore, hotStore, tokenGenerator, refreshHasher, clock, config) => {
479
+ useFactory: async (recordStore, hotStore, tokenGenerator, refreshHasher, clock, lifetimes) => {
363
480
  const { createSessionAuthority } = await loadCore();
364
481
  return createSessionAuthority({
365
482
  recordStore,
@@ -367,7 +484,7 @@ const sessionAuthorityProvider = {
367
484
  tokenGenerator,
368
485
  refreshHasher,
369
486
  clock,
370
- lifetimes: config.lifetimes,
487
+ lifetimes,
371
488
  });
372
489
  },
373
490
  inject: [
@@ -376,7 +493,7 @@ const sessionAuthorityProvider = {
376
493
  index_js_1.SHIELD_TOKEN_GENERATOR,
377
494
  index_js_1.SHIELD_REFRESH_HASHER,
378
495
  index_js_1.SHIELD_CLOCK,
379
- config_module_js_1.APP_CONFIG,
496
+ index_js_1.SHIELD_SESSION_LIFETIMES,
380
497
  ],
381
498
  };
382
499
  /**
@@ -388,18 +505,28 @@ const sessionAuthorityProvider = {
388
505
  * The override reaches this async factory via `inject` (not a captured closure) so
389
506
  * the default binding stays exactly as before when no override is plugged.
390
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
+ };
391
525
  const authenticatorProvider = {
392
526
  provide: tokens_js_1.SHIELD_AUTHENTICATOR,
393
527
  // async: loads the core factories lazily (CJS→ESM, see loadCore()); Nest awaits it.
394
- useFactory: async (hasher, resolver, totpOverride, rootIds, mfaDeviceStore) => {
395
- const { createAuthenticator, createIdentityBroker, createTotpFactorVerifier } = await loadCore();
396
- // The login totp verifier is ALWAYS the OR-across-devices wrapper: it delegates
397
- // to the resolved base (the consumer `totpOverride` when supplied, else the
398
- // framework's single-secret RFC-6238 verifier) per device secret, so a
399
- // principal's multiple confirmed devices OR-match. A consumer override still wins
400
- // (it's the base inside the OR-wrap); multi-device works by default.
401
- const base = resolveTotpVerifier(totpOverride, createTotpFactorVerifier({ hmacSha1: exports.hmacSha1 }));
402
- 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();
403
530
  return createAuthenticator({
404
531
  hasher,
405
532
  resolver,
@@ -414,11 +541,25 @@ const authenticatorProvider = {
414
541
  inject: [
415
542
  argon2id_password_hasher_js_1.SHIELD_PASSWORD_HASHER,
416
543
  tokens_js_1.SHIELD_CREDENTIAL_RESOLVER,
417
- tokens_js_1.SHIELD_TOTP_VERIFIER_OVERRIDE,
544
+ tokens_js_1.SHIELD_LOGIN_TOTP_VERIFIER,
418
545
  tokens_js_1.SHIELD_ROOT_IDS,
419
- tokens_js_1.SHIELD_MFA_DEVICE_STORE,
420
546
  ],
421
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
+ };
422
563
  /**
423
564
  * Bind the OPTIONAL login `totp` verifier override (the Phase-2 multi-device seam).
424
565
  * Same value-provider pattern as the other seams: the supplied verifier when present,
@@ -503,10 +644,48 @@ function makeResetTokenStoreProvider(override) {
503
644
  inject: [tokens_js_1.SHIELD_DB_POOL],
504
645
  };
505
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
+ */
506
657
  function makeEmailSenderProvider(override) {
507
- return override
508
- ? { provide: tokens_js_1.SHIELD_EMAIL_SENDER, useValue: override }
509
- : { 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();
510
689
  }
511
690
  function makeRecoveryCodeStoreProvider(override) {
512
691
  return override
@@ -667,25 +846,47 @@ let AuthModule = AuthModule_1 = class AuthModule {
667
846
  // The reset-link base URL value the forgot-password handler composes into the
668
847
  // emailed link (framework-owned URL composition; EmailSender stays dumb).
669
848
  resetLinkBaseUrlProvider,
849
+ // The resolved session lifetimes (#8) — one source for authority, binder, controller.
850
+ sessionLifetimesProvider,
670
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,
671
856
  hotSessionStoreProvider,
672
857
  tokenGeneratorProvider,
673
858
  refreshHasherProvider,
674
859
  clockProvider,
675
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,
676
864
  authenticatorProvider,
677
- // 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.
678
868
  totpVerifierProvider,
679
- // Team C's server-side, single-use, TTL'd challenge store backing the
680
- // login→mfa handoff (a plain class provider the controller injects).
681
- 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,
682
872
  // The request-auth token binder (access-token → principal claims).
683
873
  sessionTokenBinderProvider,
874
+ // The service-account API-key authenticator (ADR-0033) — `ask_…` bearer → service_account
875
+ // Principal. Provided here (with the guard) so the APP_GUARD sees it; exported below.
876
+ serviceAuthenticatorProvider,
684
877
  // The anti-brute-force rate limiters (spec §5): login (H1) + recovery, each
685
878
  // a Redis-injected AttemptLimiter with its own threshold. Injected into
686
879
  // AuthController (@Optional()).
687
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,
688
887
  recoveryLimiterProvider,
888
+ // #9: the TOTP-step limiter (5 / 15 min per account, `mfa:pid:<principalId>`).
889
+ mfaLimiterProvider,
689
890
  // The forgot/reset-password limiters (feature #141): forgot keyed by client IP
690
891
  // + per-hint (request-rate caps → silently skip the send when exceeded, stays
691
892
  // 204); reset keyed by client IP (may 429 on lockout — token-gated, not enum-
@@ -701,6 +902,9 @@ let AuthModule = AuthModule_1 = class AuthModule {
701
902
  argon2id_password_hasher_js_1.SHIELD_PASSWORD_HASHER,
702
903
  // Exported so the (app-wide) enforcement guard can inject it.
703
904
  tokens_js_2.SHIELD_SESSION_TOKEN_BINDER,
905
+ // Exported so the (app-wide) enforcement guard injects it → the `ask_…` machine auth path
906
+ // (ADR-0033) works out-of-the-box under `auth: true`.
907
+ tokens_js_2.SHIELD_SERVICE_AUTHENTICATOR,
704
908
  // The account/credential-surface seams — exported so the consumer's impls
705
909
  // and the account/auth controllers see them app-wide through @Global Shield.
706
910
  tokens_js_1.SHIELD_CREDENTIAL_WRITER,
@@ -719,6 +923,13 @@ let AuthModule = AuthModule_1 = class AuthModule {
719
923
  tokens_js_1.SHIELD_TOTP_VERIFIER,
720
924
  // The eager session-facts projector (consumed by AuthController).
721
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,
722
933
  ],
723
934
  };
724
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"}