@astram/guard 0.19.0 → 0.20.1

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 (129) 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 +79 -6
  4. package/dist/auth/admin-signin-controls.d.ts +80 -0
  5. package/dist/auth/admin-signin-controls.d.ts.map +1 -0
  6. package/dist/auth/admin-signin-controls.js +91 -0
  7. package/dist/auth/auth-audit.d.ts +59 -0
  8. package/dist/auth/auth-audit.d.ts.map +1 -0
  9. package/dist/auth/auth-audit.js +93 -0
  10. package/dist/auth/auth.controller.d.ts +53 -6
  11. package/dist/auth/auth.controller.d.ts.map +1 -1
  12. package/dist/auth/auth.controller.js +330 -44
  13. package/dist/auth/auth.module.d.ts +49 -0
  14. package/dist/auth/auth.module.d.ts.map +1 -1
  15. package/dist/auth/auth.module.js +149 -4
  16. package/dist/auth/captcha-gate.d.ts +103 -0
  17. package/dist/auth/captcha-gate.d.ts.map +1 -0
  18. package/dist/auth/captcha-gate.js +245 -0
  19. package/dist/auth/dto.d.ts +22 -1
  20. package/dist/auth/dto.d.ts.map +1 -1
  21. package/dist/auth/login-name-count-boot.d.ts +24 -0
  22. package/dist/auth/login-name-count-boot.d.ts.map +1 -0
  23. package/dist/auth/login-name-count-boot.js +47 -0
  24. package/dist/auth/logout-proof.d.ts +55 -0
  25. package/dist/auth/logout-proof.d.ts.map +1 -0
  26. package/dist/auth/logout-proof.js +127 -0
  27. package/dist/auth/pending-mfa-store.d.ts +5 -0
  28. package/dist/auth/pending-mfa-store.d.ts.map +1 -1
  29. package/dist/auth/pending-mfa-store.js +2 -0
  30. package/dist/auth/ports/audit-sink.d.ts +5 -2
  31. package/dist/auth/ports/audit-sink.d.ts.map +1 -1
  32. package/dist/auth/ports/mfa-device-store.d.ts +7 -0
  33. package/dist/auth/ports/mfa-device-store.d.ts.map +1 -1
  34. package/dist/auth/profile-resolver.d.ts +6 -0
  35. package/dist/auth/profile-resolver.d.ts.map +1 -1
  36. package/dist/auth/rate-limit/attempt-limiter.d.ts +6 -0
  37. package/dist/auth/rate-limit/attempt-limiter.d.ts.map +1 -1
  38. package/dist/auth/rate-limit/attempt-limiter.js +14 -0
  39. package/dist/auth/tokens.d.ts +59 -0
  40. package/dist/auth/tokens.d.ts.map +1 -1
  41. package/dist/auth/tokens.js +61 -1
  42. package/dist/authentication/credential-provisioner.d.ts +7 -0
  43. package/dist/authentication/credential-provisioner.d.ts.map +1 -1
  44. package/dist/authentication/mariadb-credential-provisioner.d.ts +1 -0
  45. package/dist/authentication/mariadb-credential-provisioner.d.ts.map +1 -1
  46. package/dist/authentication/mariadb-credential-provisioner.js +7 -0
  47. package/dist/authentication/mariadb-credential-resolver.d.ts +23 -2
  48. package/dist/authentication/mariadb-credential-resolver.d.ts.map +1 -1
  49. package/dist/authentication/mariadb-credential-resolver.js +45 -1
  50. package/dist/authentication/mariadb-mfa-device-store.d.ts +7 -0
  51. package/dist/authentication/mariadb-mfa-device-store.d.ts.map +1 -1
  52. package/dist/authentication/mariadb-mfa-device-store.js +32 -0
  53. package/dist/authentication/mariadb-mfa-requirement-source.d.ts +55 -0
  54. package/dist/authentication/mariadb-mfa-requirement-source.d.ts.map +1 -0
  55. package/dist/authentication/mariadb-mfa-requirement-source.js +164 -0
  56. package/dist/authentication/mariadb-mfa-resetter.d.ts +42 -0
  57. package/dist/authentication/mariadb-mfa-resetter.d.ts.map +1 -0
  58. package/dist/authentication/mariadb-mfa-resetter.js +59 -0
  59. package/dist/authentication/mariadb-profile-writer.d.ts.map +1 -1
  60. package/dist/authentication/mariadb-profile-writer.js +5 -0
  61. package/dist/authentication/sign-in-with.d.ts +14 -0
  62. package/dist/authentication/sign-in-with.d.ts.map +1 -0
  63. package/dist/authentication/sign-in-with.js +36 -0
  64. package/dist/authz/core-access.d.ts +11 -0
  65. package/dist/authz/core-access.d.ts.map +1 -0
  66. package/dist/authz/core-access.js +14 -0
  67. package/dist/authz/list-scope-sql.d.ts.map +1 -1
  68. package/dist/authz/list-scope-sql.js +15 -5
  69. package/dist/authz/wire-record-authorization.d.ts.map +1 -1
  70. package/dist/authz/wire-record-authorization.js +21 -3
  71. package/dist/captcha/captcha.providers.d.ts +18 -0
  72. package/dist/captcha/captcha.providers.d.ts.map +1 -0
  73. package/dist/captcha/captcha.providers.js +78 -0
  74. package/dist/captcha/recaptcha-verifier.d.ts +20 -0
  75. package/dist/captcha/recaptcha-verifier.d.ts.map +1 -0
  76. package/dist/captcha/recaptcha-verifier.js +48 -0
  77. package/dist/captcha/siteverify.d.ts +47 -0
  78. package/dist/captcha/siteverify.d.ts.map +1 -0
  79. package/dist/captcha/siteverify.js +98 -0
  80. package/dist/captcha/turnstile-verifier.d.ts +18 -0
  81. package/dist/captcha/turnstile-verifier.d.ts.map +1 -0
  82. package/dist/captcha/turnstile-verifier.js +41 -0
  83. package/dist/config/captcha-config.d.ts +47 -0
  84. package/dist/config/captcha-config.d.ts.map +1 -0
  85. package/dist/config/captcha-config.js +163 -0
  86. package/dist/config/reserved-principal-ids.d.ts +15 -0
  87. package/dist/config/reserved-principal-ids.d.ts.map +1 -1
  88. package/dist/config/reserved-principal-ids.js +20 -3
  89. package/dist/enforcement/global-enforcement.guard.d.ts +9 -1
  90. package/dist/enforcement/global-enforcement.guard.d.ts.map +1 -1
  91. package/dist/enforcement/global-enforcement.guard.js +52 -10
  92. package/dist/enforcement/reenroll-allowlist.d.ts +5 -0
  93. package/dist/enforcement/reenroll-allowlist.d.ts.map +1 -1
  94. package/dist/enforcement/reenroll-allowlist.js +5 -0
  95. package/dist/identity/auth-facts.controller.d.ts +31 -4
  96. package/dist/identity/auth-facts.controller.d.ts.map +1 -1
  97. package/dist/identity/auth-facts.controller.js +46 -11
  98. package/dist/identity/identity-actions.d.ts +2 -2
  99. package/dist/identity/identity-actions.d.ts.map +1 -1
  100. package/dist/identity/module-registry.bridge.d.ts.map +1 -1
  101. package/dist/identity/module-registry.bridge.js +3 -6
  102. package/dist/identity/permission-facts.adapter.d.ts +22 -0
  103. package/dist/identity/permission-facts.adapter.d.ts.map +1 -1
  104. package/dist/identity/permission-facts.adapter.js +26 -1
  105. package/dist/index.d.ts +9 -2
  106. package/dist/index.d.ts.map +1 -1
  107. package/dist/index.js +36 -10
  108. package/dist/internal.d.ts +27 -0
  109. package/dist/internal.d.ts.map +1 -1
  110. package/dist/internal.js +36 -1
  111. package/dist/module/shield.module.d.ts +27 -0
  112. package/dist/module/shield.module.d.ts.map +1 -1
  113. package/dist/module/shield.module.js +10 -0
  114. package/dist/request-auth/request-principal.d.ts +8 -1
  115. package/dist/request-auth/request-principal.d.ts.map +1 -1
  116. package/dist/request-auth/request-principal.js +11 -4
  117. package/dist/request-auth/session-token-binder.d.ts +82 -0
  118. package/dist/request-auth/session-token-binder.d.ts.map +1 -1
  119. package/dist/request-auth/session-token-binder.js +203 -1
  120. package/dist/session/mfa-requirement-service.d.ts +168 -0
  121. package/dist/session/mfa-requirement-service.d.ts.map +1 -0
  122. package/dist/session/mfa-requirement-service.js +461 -0
  123. package/dist/session/session-restriction-store.d.ts +36 -0
  124. package/dist/session/session-restriction-store.d.ts.map +1 -1
  125. package/dist/session/session-restriction-store.js +57 -6
  126. package/dist/session/session-restriction.d.ts +22 -0
  127. package/dist/session/session-restriction.d.ts.map +1 -0
  128. package/dist/session/session-restriction.js +23 -0
  129. package/package.json +5 -5
@@ -85,8 +85,11 @@ const tokens_js_2 = require("../request-auth/tokens.js");
85
85
  const session_token_binder_js_1 = require("../request-auth/session-token-binder.js");
86
86
  const request_principal_js_1 = require("../request-auth/request-principal.js");
87
87
  const tokens_js_3 = require("./tokens.js");
88
+ const session_restriction_js_1 = require("../session/session-restriction.js");
89
+ const logout_proof_js_1 = require("./logout-proof.js");
88
90
  const deliver_email_js_1 = require("../messaging/deliver-email.js");
89
91
  const password_reset_message_js_1 = require("./password-reset-message.js");
92
+ const auth_audit_js_1 = require("./auth-audit.js");
90
93
  /**
91
94
  * The core `Authenticator`'s generic transient reason (see authenticator.ts —
92
95
  * `REASON_AUTH_UNAVAILABLE`). A `rejected` result carrying THIS reason is the
@@ -118,6 +121,26 @@ function sha256(value) {
118
121
  function genericUnauthorized() {
119
122
  throw new common_1.HttpException(GENERIC_AUTH_FAILURE, common_1.HttpStatus.UNAUTHORIZED);
120
123
  }
124
+ /**
125
+ * Auth capabilities R7.3: the generic 401 plus the challenge flag, identical for known and unknown
126
+ * names (X2). Returned when a challenge is required and was missing or failed, and on a failure
127
+ * after which the next attempt needs one.
128
+ */
129
+ const CAPTCHA_REQUIRED_FAILURE = Object.freeze({
130
+ error: 'invalid_credentials',
131
+ captcha: 'required',
132
+ });
133
+ /** The login 401: with the challenge flag when the next attempt needs a CAPTCHA token. */
134
+ function loginUnauthorized(captchaRequired) {
135
+ if (captchaRequired) {
136
+ throw new common_1.HttpException(CAPTCHA_REQUIRED_FAILURE, common_1.HttpStatus.UNAUTHORIZED);
137
+ }
138
+ genericUnauthorized();
139
+ }
140
+ /** The reset-password bad-token 400, with the challenge flag when one is (now) required. */
141
+ function invalidResetToken(captchaRequired) {
142
+ throw new common_1.HttpException(captchaRequired ? { error: 'invalid_token', captcha: 'required' } : { error: 'invalid_token' }, common_1.HttpStatus.BAD_REQUEST);
143
+ }
121
144
  /** Throw the retryable 503 (transient — NOT a deny; the client may retry). */
122
145
  function retryableUnavailable() {
123
146
  throw new common_1.HttpException(RETRYABLE_BODY, common_1.HttpStatus.SERVICE_UNAVAILABLE);
@@ -221,7 +244,12 @@ let AuthController = class AuthController {
221
244
  sessionRestrictions;
222
245
  emailChangeTokens;
223
246
  messageDispatcher;
224
- constructor(authenticator, sessionAuthority, pendingMfa, tokenBinder, profileResolver, credentialResolver, resetTokenStore, emailSender, credentialWriter, sessionSweeper, passwordHasher, resetLinkBaseUrl, recoveryCodes, loginLimiter, loginIpLimiter, trustProxyCheck, recoveryLimiter, forgotLimiter, forgotHintLimiter, resetLimiter, auditSink, sessionFactsProjector, scopeSubtreeResolver, membershipChecker, permissionFacts, mfaLimiter, mfaStepUp, clock, lifetimes, rootIds, sessionRecords, sessionRestrictions, emailChangeTokens, messageDispatcher) {
247
+ logoutProof;
248
+ logoutRequiresProof;
249
+ mfaRequirement;
250
+ captchaGate;
251
+ signInWith;
252
+ constructor(authenticator, sessionAuthority, pendingMfa, tokenBinder, profileResolver, credentialResolver, resetTokenStore, emailSender, credentialWriter, sessionSweeper, passwordHasher, resetLinkBaseUrl, recoveryCodes, loginLimiter, loginIpLimiter, trustProxyCheck, recoveryLimiter, forgotLimiter, forgotHintLimiter, resetLimiter, auditSink, sessionFactsProjector, scopeSubtreeResolver, membershipChecker, permissionFacts, mfaLimiter, mfaStepUp, clock, lifetimes, rootIds, sessionRecords, sessionRestrictions, emailChangeTokens, messageDispatcher, logoutProof, logoutRequiresProof, mfaRequirement, captchaGate, signInWith) {
225
253
  this.authenticator = authenticator;
226
254
  this.sessionAuthority = sessionAuthority;
227
255
  this.pendingMfa = pendingMfa;
@@ -256,6 +284,25 @@ let AuthController = class AuthController {
256
284
  this.sessionRestrictions = sessionRestrictions;
257
285
  this.emailChangeTokens = emailChangeTokens;
258
286
  this.messageDispatcher = messageDispatcher;
287
+ this.logoutProof = logoutProof;
288
+ this.logoutRequiresProof = logoutRequiresProof;
289
+ this.mfaRequirement = mfaRequirement;
290
+ this.captchaGate = captchaGate;
291
+ this.signInWith = signInWith;
292
+ }
293
+ /** True when bootstrap confirmed an explicit `trust proxy` (per-IP limits and CAPTCHA IP keys). */
294
+ perIpEnabled() {
295
+ return this.trustProxyCheck?.perIpLimitingEnabled === true;
296
+ }
297
+ /**
298
+ * R7.3: count one failure toward the CAPTCHA counters and say whether the next attempt needs a
299
+ * challenge. No gate (or endpoint not covered) ⇒ `false`.
300
+ */
301
+ async captchaAfterFailure(endpoint, subject) {
302
+ const gate = this.captchaGate;
303
+ if (gate === undefined || gate === null)
304
+ return false;
305
+ return gate.recordFailure(endpoint, subject);
259
306
  }
260
307
  /**
261
308
  * The access-token binding TTL for an establishment (security hardening #8):
@@ -275,7 +322,7 @@ let AuthController = class AuthController {
275
322
  */
276
323
  async bindSession(est, principal, mustReenroll = false) {
277
324
  if (this.tokenBinder === undefined)
278
- return;
325
+ return (0, session_restriction_js_1.restrictionOf)({ mustReenroll }, Date.now());
279
326
  const ttlMs = this.accessTtlFor(est);
280
327
  // EAGER session-facts projection (ADR-0011): compute the principal's coarse grants
281
328
  // NOW so the guard can serve gate 5 from the request's `cachedFacts`. Absent projector
@@ -301,6 +348,11 @@ let AuthController = class AuthController {
301
348
  // - a projection without `roleIds` (a port that does not report roles) or no projector at all
302
349
  // → the authenticated principal's roles (back-compat for apps that own their roles).
303
350
  const roleIds = projected === null ? [] : ((0, session_token_binder_js_1.liveRoleIds)(projected) ?? [...principal.roleIds]);
351
+ // MANDATORY 2FA (auth capabilities R2.3, R2.4): evaluated at sign-in (a lifecycle event; DB
352
+ // reads allowed) over the same projection. FAIL CLOSED: an evaluation that cannot complete, or
353
+ // a restriction that cannot be made durable, revokes the new session and answers 503 — never an
354
+ // unrestricted session. No policy feature ⇒ nothing here runs (the binding is unchanged).
355
+ const mfa = await this.evaluateMfaAtLogin(est, principal, projected);
304
356
  try {
305
357
  await this.tokenBinder.bind(est.tokens.accessToken, {
306
358
  id: principal.id,
@@ -324,12 +376,46 @@ let AuthController = class AuthController {
324
376
  ...(scopeSubtreeNodeIds && scopeSubtreeVersion !== undefined
325
377
  ? { scopeSubtreeVersion }
326
378
  : {}),
379
+ // R2.3 / R2.4: the requirement as evaluated at sign-in, and the version it is valid for.
380
+ ...(mfa ?? {}),
327
381
  }, ttlMs);
328
382
  }
329
383
  catch {
330
384
  // Best-effort: never fail a login on a binding hiccup. The guard denies if the
331
385
  // binding is missing, so this degrades safely (the user re-logs in).
332
386
  }
387
+ // The restriction the session was bound with (review round 1, item C): a restricted sign-in
388
+ // gets no inline grants.
389
+ return (0, session_restriction_js_1.restrictionOf)({ mustReenroll, ...(mfa ?? {}) }, Date.now());
390
+ }
391
+ /**
392
+ * The mandatory-2FA state to bind at sign-in (R2.3, R2.4), or `undefined` when the policy feature
393
+ * is not wired. `'required'` is also marked on the durable session BEFORE any token leaves, so a
394
+ * binding rebuilt after a Redis loss stays restricted. Any failure revokes the session and
395
+ * answers the generic 503 (fail closed).
396
+ */
397
+ async evaluateMfaAtLogin(est, principal, projected) {
398
+ const service = this.mfaRequirement;
399
+ if (service === undefined || service === null)
400
+ return undefined;
401
+ let state;
402
+ try {
403
+ state = await service.atLogin(principal, projected);
404
+ if (state.restriction === 'mfa_enrollment') {
405
+ const restrictions = this.sessionRestrictions;
406
+ if (restrictions !== undefined && restrictions !== null) {
407
+ if (typeof restrictions.markRestriction !== 'function') {
408
+ throw new Error('restriction store cannot record mfa_enrollment');
409
+ }
410
+ await restrictions.markRestriction(est.session.sessionId, 'mfa_enrollment');
411
+ }
412
+ }
413
+ }
414
+ catch {
415
+ await this.sessionAuthority.revoke(est.session.sessionId).catch(() => undefined);
416
+ retryableUnavailable();
417
+ }
418
+ return state;
333
419
  }
334
420
  /**
335
421
  * Project coarse grants for a principal id + tenant (ADR-0011), distinguishing THREE
@@ -454,8 +540,10 @@ let AuthController = class AuthController {
454
540
  * exactOptionalPropertyTypes an explicit `undefined` is not assignable to the optional
455
541
  * field, so a spread keeps it truly absent.
456
542
  */
457
- async sessionSuccessWithFacts(est, principal, extra) {
458
- const authFacts = await this.projectAuthFacts(principal);
543
+ async sessionSuccessWithFacts(est, principal, restricted, extra) {
544
+ // A restricted session (re-enrol or mandatory 2FA, review round 1 item C) gets NO inline
545
+ // grants: the frontend fetches `/identity/auth-facts`, which answers the restricted shape.
546
+ const authFacts = restricted === null ? await this.projectAuthFacts(principal) : undefined;
459
547
  return {
460
548
  ...toSessionSuccess(est),
461
549
  ...(extra?.mfaRecoveryUsed ? { mfaRecoveryUsed: true } : {}),
@@ -546,8 +634,10 @@ let AuthController = class AuthController {
546
634
  const ipLimiter = this.perIp(this.loginIpLimiter);
547
635
  const ipKey = (0, canonical_hint_js_1.loginIpKey)(ip);
548
636
  const ipVerdict = await consumeOrUnavailable(ipLimiter, ipKey);
549
- if (ipVerdict !== null && !ipVerdict.allowed)
550
- genericUnauthorized();
637
+ // R7.3: an IP over its login budget is far over the CAPTCHA threshold too.
638
+ if (ipVerdict !== null && !ipVerdict.allowed) {
639
+ loginUnauthorized(this.captchaGate?.covers('login') === true);
640
+ }
551
641
  /** Give back this attempt's IP count (it was not a password failure). */
552
642
  const refundIp = async () => {
553
643
  if (ipVerdict !== null)
@@ -565,7 +655,26 @@ let AuthController = class AuthController {
565
655
  retryableUnavailable();
566
656
  }
567
657
  const accountId = (0, canonical_hint_js_1.principalIdOf)(account);
658
+ const accountTenant = (0, auth_audit_js_1.tenantOfClaims)(account?.claims);
659
+ const accountLabel = accountId !== null ? 'known' : 'unknown';
568
660
  const loginKey = (0, canonical_hint_js_1.loginHintKey)(identityHint, accountId);
661
+ // ── CAPTCHA (auth capabilities R7.3, R7.4) ── decided on the resolved account, the typed text
662
+ // and the client IP, and verified BEFORE the account budget is spent and before any password
663
+ // hash. A missing or failed challenge answers the same 401 + `captcha: 'required'` for known
664
+ // and unknown names (X2). This attempt stays counted against the client IP (a failed
665
+ // challenge is a failed attempt from that client); the account budget is untouched, so a
666
+ // stranger without a token cannot lock the account through this gate.
667
+ const captchaSubject = {
668
+ accountId,
669
+ tenantId: accountTenant,
670
+ typedText: identityHint,
671
+ ip: this.perIpEnabled() ? ip : null,
672
+ };
673
+ if (this.captchaGate?.covers('login') === true) {
674
+ const verdict = await this.captchaGate.check('login', captchaSubject, body?.captchaToken, ip);
675
+ if (verdict.kind === 'failed')
676
+ loginUnauthorized(true);
677
+ }
569
678
  let hintVerdict;
570
679
  try {
571
680
  hintVerdict = await consumeOrUnavailable(this.loginLimiter, loginKey);
@@ -580,8 +689,10 @@ let AuthController = class AuthController {
580
689
  // is audited ONCE on the TRANSITION instead — in the wrong-password branch below,
581
690
  // when the attempt's count equals the threshold (feature #140 audit-on-the-edge).
582
691
  // An attempt refused by the account lock is not an IP failure (as before #10).
692
+ // R12.4: still a failed sign-in for the counter (no row: the lockout row was written once).
583
693
  await refundIp();
584
- genericUnauthorized();
694
+ (0, auth_audit_js_1.countLoginFailure)('password', accountLabel);
695
+ loginUnauthorized(await this.captchaAfterFailure('login', captchaSubject));
585
696
  }
586
697
  let result;
587
698
  try {
@@ -602,12 +713,14 @@ let AuthController = class AuthController {
602
713
  if (result.outcome === 'authenticated') {
603
714
  // Correct password → clear the login counter (do not carry earlier failures).
604
715
  await this.loginLimiter?.reset(loginKey);
716
+ await this.captchaGate?.recordSuccess('login', captchaSubject);
605
717
  const principal = requirePrincipal(result);
606
718
  const est = await this.establishOrReject(principal);
607
- await this.bindSession(est, principal);
719
+ const restricted = await this.bindSession(est, principal);
720
+ this.auditSignIn(principal, 'password');
608
721
  // Inline the caller's auth-facts so the permission-gated nav is ready the instant
609
722
  // login returns (one round-trip). Best-effort — omitted on any projection failure.
610
- return this.sessionSuccessWithFacts(est, principal);
723
+ return this.sessionSuccessWithFacts(est, principal, restricted);
611
724
  }
612
725
  if (result.outcome === 'mfa_required') {
613
726
  // Password verified, a second factor is outstanding. NOT a failure — but NOT a
@@ -615,6 +728,8 @@ let AuthController = class AuthController {
615
728
  // so a stolen password cannot loop login→mfa to guess TOTP codes for free. This
616
729
  // attempt's own count is given back (Phase 9 #10), leaving earlier failures intact.
617
730
  await this.loginLimiter?.refund(loginKey);
731
+ // R7.3: the password was right; the CAPTCHA counters track password failures only.
732
+ await this.captchaGate?.recordSuccess('login', captchaSubject);
618
733
  //
619
734
  // M6: record the password step's SUCCESS — the resolved principal, the typed hint,
620
735
  // and a fingerprint of the credential it was checked against — NEVER the password.
@@ -626,6 +741,8 @@ let AuthController = class AuthController {
626
741
  principalId: accountId,
627
742
  identityHint,
628
743
  credentialFingerprint: (0, mfa_step_up_js_1.credentialFingerprint)(account),
744
+ // R12.2: the tenant of a failed second-factor audit row (resolved server-side).
745
+ ...(accountTenant !== null ? { tenantId: accountTenant } : {}),
629
746
  };
630
747
  let challengeToken;
631
748
  try {
@@ -649,19 +766,31 @@ let AuthController = class AuthController {
649
766
  retryableUnavailable();
650
767
  }
651
768
  // A genuine wrong-password deny: the attempt was already counted by `consume` and
652
- // stays counted. When its count equals the limiter's threshold this attempt is the one
653
- // that TRIPPED the lockout — audit `auth.login.locked` exactly ONCE here (the edge),
654
- // never on the already-locked gate above (which would flood).
769
+ // stays counted (R12.4 counter, known/unknown). R12.3: a resolved account writes ONE
770
+ // `auth.login.failed` row per limiter window (the attempt that opened it); an unknown name
771
+ // writes none.
772
+ (0, auth_audit_js_1.countLoginFailure)('password', accountLabel);
773
+ if (accountId !== null && accountTenant !== null && (0, auth_audit_js_1.opensFailureWindow)(hintVerdict)) {
774
+ this.auditFailure(accountId, accountTenant, 'password');
775
+ }
776
+ // When its count equals the limiter's threshold this attempt is the one that TRIPPED the
777
+ // lockout — audit `auth.login.locked` exactly ONCE here (the edge), never on the
778
+ // already-locked gate above (which would flood). R12.2/R12.3: the row names the RESOLVED
779
+ // account, never the typed hint; an unknown name only counts.
655
780
  // NO notify: this is pre-auth (the caller has not proven ownership), and mailing the
656
781
  // account on every lockout would let anyone email-bomb it / confirm it exists
657
782
  // (recordSecurityEvent enforces this too).
658
783
  if (this.loginLimiter != null && hintVerdict?.count === this.loginLimiter.maxAttempts) {
659
- this.recordSecurityEvent({
660
- action: 'auth.login.locked',
661
- identityHint,
662
- attributes: { reason: 'rate_limit' },
663
- notify: false,
664
- });
784
+ (0, auth_audit_js_1.countLockout)('password', accountLabel);
785
+ if (accountId !== null && accountTenant !== null) {
786
+ this.recordSecurityEvent({
787
+ action: 'auth.login.locked',
788
+ actorId: accountId,
789
+ tenantId: accountTenant,
790
+ attributes: { reason: 'rate_limit' },
791
+ notify: false,
792
+ });
793
+ }
665
794
  }
666
795
  // #5: the failure also stays counted against the client IP (failures only — a success
667
796
  // gives its attempt back and never resets this counter, so an attacker's own valid
@@ -669,6 +798,7 @@ let AuthController = class AuthController {
669
798
  // spans many accounts).
670
799
  if (ipLimiter != null && ipVerdict !== null) {
671
800
  if (ipVerdict.count === ipLimiter.maxAttempts) {
801
+ (0, auth_audit_js_1.countLockout)('password', 'ip');
672
802
  this.recordSecurityEvent({
673
803
  action: 'auth.login.ip_locked',
674
804
  identityHint: null,
@@ -677,7 +807,22 @@ let AuthController = class AuthController {
677
807
  });
678
808
  }
679
809
  }
680
- genericUnauthorized();
810
+ // R7.3: count the failure toward the CAPTCHA counters; flag the 401 once the next attempt
811
+ // needs a challenge (same for known and unknown names).
812
+ loginUnauthorized(await this.captchaAfterFailure('login', captchaSubject));
813
+ }
814
+ /**
815
+ * `GET /auth/login-options` (auth capabilities R7.3, R13.2, AC-23) — what the sign-in page needs
816
+ * before a session exists: the accepted sign-in forms and the public CAPTCHA settings
817
+ * (`{ provider, siteKey, mode }`, or `null` when CAPTCHA is off). Never the secret. `@Public()`:
818
+ * pre-session, reveals nothing about any account.
819
+ */
820
+ loginOptions() {
821
+ const signInWith = this.signInWith ?? ['email'];
822
+ return {
823
+ signInWith: [...signInWith],
824
+ captcha: this.captchaGate ? { ...this.captchaGate.publicOptions } : null,
825
+ };
681
826
  }
682
827
  /**
683
828
  * `GET /auth/me` — the signed-in user's **display profile** (name, email, active
@@ -706,10 +851,12 @@ let AuthController = class AuthController {
706
851
  // Auth surface is up but request-auth isn't wired — cannot authenticate.
707
852
  throw new common_1.HttpException({ error: 'unauthorized' }, common_1.HttpStatus.UNAUTHORIZED);
708
853
  }
709
- const principal = await (0, request_principal_js_1.resolveRequestPrincipal)(authorization, this.tokenBinder);
710
- if (principal === null) {
854
+ // Auth capabilities R2.5: the mandatory-2FA recompute runs here too (a version move only).
855
+ const resolved = await (0, request_principal_js_1.resolveRequestAuth)(authorization, this.tokenBinder, undefined, this.mfaRequirement);
856
+ if (resolved === null) {
711
857
  throw new common_1.HttpException({ error: 'unauthorized' }, common_1.HttpStatus.UNAUTHORIZED);
712
858
  }
859
+ const { principal, claims } = resolved;
713
860
  // 2) Resolve the display profile via the consumer seam. No seam → honest 501.
714
861
  // (The provider binds `null` when the consumer supplied none, so check nullish.)
715
862
  if (this.profileResolver === undefined || this.profileResolver === null) {
@@ -729,6 +876,12 @@ let AuthController = class AuthController {
729
876
  // login). 404 — never fall back to returning the raw ids.
730
877
  throw new common_1.HttpException({ error: 'profile_not_found' }, common_1.HttpStatus.NOT_FOUND);
731
878
  }
879
+ // R2.4: the grace deadline for the enrolment banner, as an ISO-8601 instant (UTC), only while
880
+ // one is bound (a passed one is still returned: the session is restricted from then on).
881
+ const deadline = claims.mfaEnrollmentDeadlineMs;
882
+ if (typeof deadline === 'number' && Number.isFinite(deadline)) {
883
+ return { ...profile, mfaEnrollmentDeadline: new Date(deadline).toISOString() };
884
+ }
732
885
  return profile;
733
886
  }
734
887
  /**
@@ -772,10 +925,20 @@ let AuthController = class AuthController {
772
925
  if (token === null) {
773
926
  throw new common_1.HttpException({ error: 'unauthorized' }, common_1.HttpStatus.UNAUTHORIZED);
774
927
  }
775
- const claims = await this.tokenBinder.resolve(token);
776
- if (claims === null) {
928
+ const resolvedClaims = await this.tokenBinder.resolve(token);
929
+ if (resolvedClaims === null) {
777
930
  throw new common_1.HttpException({ error: 'unauthorized' }, common_1.HttpStatus.UNAUTHORIZED);
778
931
  }
932
+ // Auth capabilities R2.5: the mandatory-2FA recompute (a version move only; 503 on failure).
933
+ const claims = this.mfaRequirement !== undefined && this.mfaRequirement !== null
934
+ ? await this.mfaRequirement.refresh(token, resolvedClaims)
935
+ : resolvedClaims;
936
+ // R15.2 (F9): a restricted session (re-enrol, or mandatory 2FA: D-2) may not change its scope.
937
+ // `@Public()` routes are not covered by the guard's gate, so the check lives here (same bodies).
938
+ const restriction = (0, session_restriction_js_1.restrictionOf)(claims, Date.now());
939
+ if (restriction !== null) {
940
+ throw new common_1.HttpException({ error: (0, session_restriction_js_1.restrictionErrorCode)(restriction) }, common_1.HttpStatus.FORBIDDEN);
941
+ }
779
942
  // Phase 11 #1 / KI #79: only a canonical uuid is checked and BOUND. The membership check is SQL
780
943
  // (`utf8mb4_unicode_ci` ignores case, trailing spaces and width), so any other spelling would
781
944
  // pass the gate and be bound as the session's tenant in the caller's spelling — its
@@ -913,7 +1076,8 @@ let AuthController = class AuthController {
913
1076
  // the account and minting/sending must never leak into the response timing. Only when
914
1077
  // there IS a hint (an empty hint never sends, and must not burn a shared key).
915
1078
  if (hint !== '') {
916
- void this.meterAndDeliverReset(hint, ipAllowed).catch(() => {
1079
+ const captchaToken = body?.captchaToken;
1080
+ void this.meterAndDeliverReset(hint, ipAllowed, ip, captchaToken).catch(() => {
917
1081
  /* swallow — anti-enumeration: never let a store/email failure be observable */
918
1082
  });
919
1083
  }
@@ -930,7 +1094,7 @@ let AuthController = class AuthController {
930
1094
  * 3. send only when both budgets allow, the account resolved, and the reset seams exist.
931
1095
  * A lookup error still meters (fallback key) and skips the send (still 204).
932
1096
  */
933
- async meterAndDeliverReset(hint, ipAllowed) {
1097
+ async meterAndDeliverReset(hint, ipAllowed, ip, captchaToken) {
934
1098
  const resolver = this.credentialResolver;
935
1099
  let resolved = null;
936
1100
  let lookupFailed = false;
@@ -959,6 +1123,23 @@ let AuthController = class AuthController {
959
1123
  });
960
1124
  }
961
1125
  }
1126
+ // R7.2–R7.4: the CAPTCHA gate runs here, detached like the rest of the found-path work, so
1127
+ // neither the challenge decision nor the provider call shows in the 204's timing. Every
1128
+ // request counts toward the forgot counters (a request-rate cap); a missing or failed
1129
+ // challenge does no work (still 204, counted).
1130
+ const gate = this.captchaGate;
1131
+ if (gate !== undefined && gate !== null && gate.covers('forgot_password')) {
1132
+ const subject = {
1133
+ accountId: principalId,
1134
+ tenantId: (0, auth_audit_js_1.tenantOfClaims)(resolved?.claims),
1135
+ typedText: hint,
1136
+ ip: this.perIpEnabled() ? (ip ?? null) : null,
1137
+ };
1138
+ const verdict = await gate.check('forgot_password', subject, captchaToken, ip);
1139
+ await gate.recordFailure('forgot_password', subject);
1140
+ if (verdict.kind === 'failed')
1141
+ return;
1142
+ }
962
1143
  const store = this.resetTokenStore;
963
1144
  const sender = this.emailSender;
964
1145
  if (!ipAllowed || !hintAllowed || lookupFailed || resolved === null || !principalId)
@@ -1057,6 +1238,16 @@ let AuthController = class AuthController {
1057
1238
  hasher === undefined) {
1058
1239
  throw new common_1.HttpException({ error: 'not_available' }, common_1.HttpStatus.NOT_IMPLEMENTED);
1059
1240
  }
1241
+ // R7.2–R7.4: the CAPTCHA gate runs BEFORE the reset token is used (and before any hash). Its
1242
+ // only counter is the client IP (no account is known before the token is consumed), so
1243
+ // adaptive mode needs an explicit `trust proxy`; `always` works without it. A missing or
1244
+ // failed challenge answers the bad-token 400 plus `captcha: 'required'`.
1245
+ const resetSubject = { ip: this.perIpEnabled() ? ip : null };
1246
+ if (this.captchaGate?.covers('reset_password') === true) {
1247
+ const verdict = await this.captchaGate.check('reset_password', resetSubject, body?.captchaToken, ip);
1248
+ if (verdict.kind === 'failed')
1249
+ invalidResetToken(true);
1250
+ }
1060
1251
  // Consume the token (single-use, TTL-checked). Unknown/expired/consumed → 400.
1061
1252
  const principalId = await store.consume(sha256(token));
1062
1253
  if (principalId === null) {
@@ -1078,7 +1269,7 @@ let AuthController = class AuthController {
1078
1269
  });
1079
1270
  }
1080
1271
  }
1081
- throw new common_1.HttpException({ error: 'invalid_token' }, common_1.HttpStatus.BAD_REQUEST);
1272
+ invalidResetToken(await this.captchaAfterFailure('reset_password', resetSubject));
1082
1273
  }
1083
1274
  const newHash = await hasher.hash(newPassword);
1084
1275
  await writer.setPassword(principalId, newHash);
@@ -1184,8 +1375,15 @@ let AuthController = class AuthController {
1184
1375
  retryableUnavailable();
1185
1376
  }
1186
1377
  if (outcome.outcome !== 'authenticated' || outcome.principal.id !== pending.principalId) {
1378
+ // R12.3/R12.4: count it; one `auth.login.failed` row per window (the challenge proves the
1379
+ // password step, so the account is resolved; its tenant came from that step).
1380
+ (0, auth_audit_js_1.countLoginFailure)('totp', 'known');
1381
+ if (pending.tenantId !== undefined && (0, auth_audit_js_1.opensFailureWindow)(verdict)) {
1382
+ this.auditFailure(pending.principalId, pending.tenantId, 'totp');
1383
+ }
1187
1384
  // The attempt stays counted; the edge (count === threshold) audits ONCE.
1188
1385
  if (this.mfaLimiter != null && verdict?.count === this.mfaLimiter.maxAttempts) {
1386
+ (0, auth_audit_js_1.countLockout)('totp', 'known');
1189
1387
  this.recordSecurityEvent({
1190
1388
  action: 'auth.mfa.locked',
1191
1389
  actorId: pending.principalId,
@@ -1197,11 +1395,12 @@ let AuthController = class AuthController {
1197
1395
  }
1198
1396
  const principal = outcome.principal;
1199
1397
  const est = await this.establishOrReject(principal);
1200
- await this.bindSession(est, principal);
1398
+ const restricted = await this.bindSession(est, principal);
1201
1399
  await this.mfaLimiter?.reset(mfaKey);
1202
1400
  await this.loginLimiter?.reset((0, canonical_hint_js_1.loginHintKey)(pending.identityHint, pending.principalId));
1401
+ this.auditSignIn(principal, 'totp');
1203
1402
  // Same inline-facts enrichment as the password-login success (one round-trip).
1204
- return this.sessionSuccessWithFacts(est, principal);
1403
+ return this.sessionSuccessWithFacts(est, principal, restricted);
1205
1404
  }
1206
1405
  /**
1207
1406
  * The `/auth/mfa` RECOVERY-CODE branch (spec §3). The lost-device user completes
@@ -1253,6 +1452,12 @@ let AuthController = class AuthController {
1253
1452
  // 401 (never surface a store error as a distinct outcome).
1254
1453
  const ok = await this.recoveryCodes.consume(principalId, code).catch(() => false);
1255
1454
  if (!ok) {
1455
+ // R12.3/R12.4: count it; one `auth.login.failed` row per window for the resolved account.
1456
+ (0, auth_audit_js_1.countLoginFailure)('recovery_code', 'known');
1457
+ const recoveryTenant = (0, auth_audit_js_1.tenantOfClaims)(claims);
1458
+ if (recoveryTenant !== null && (0, auth_audit_js_1.opensFailureWindow)(verdict)) {
1459
+ this.auditFailure(principalId, recoveryTenant, 'recovery_code');
1460
+ }
1256
1461
  // A wrong (or unmatched) recovery code → the attempt stays counted, THEN the
1257
1462
  // identical generic 401. When its count equals the threshold this attempt TRIPPED
1258
1463
  // the lockout — audit `auth.mfa.recovery_locked` exactly ONCE on that edge (never on
@@ -1261,6 +1466,7 @@ let AuthController = class AuthController {
1261
1466
  // emailing on every recovery lockout would let an attacker email-bomb the owner —
1262
1467
  // audit-only is safer; the once-per-window edge already dedups it.
1263
1468
  if (this.recoveryLimiter != null && verdict?.count === this.recoveryLimiter.maxAttempts) {
1469
+ (0, auth_audit_js_1.countLockout)('recovery_code', 'known');
1264
1470
  this.recordSecurityEvent({
1265
1471
  action: 'auth.mfa.recovery_locked',
1266
1472
  actorId: principalId,
@@ -1298,7 +1504,7 @@ let AuthController = class AuthController {
1298
1504
  // Enforced re-enroll (spec §4): a recovery-code login is lower-assurance, so the
1299
1505
  // session is bound with mustReenroll:true — the enforcement gate then restricts it
1300
1506
  // to the re-secure allowlist until a fresh device is confirmed.
1301
- await this.bindSession(est, principal, true);
1507
+ const restricted = await this.bindSession(est, principal, true);
1302
1508
  // Notify + audit (spec §6) via the REUSABLE security-event seam (feature #140):
1303
1509
  // records `mfa.recovery_used` AND notifies the account owner, BOTH fire-and-forget +
1304
1510
  // best-effort. A throwing/slow sink or sender NEVER fails the (legitimate) login
@@ -1311,11 +1517,10 @@ let AuthController = class AuthController {
1311
1517
  tenantId: principal.tenantId,
1312
1518
  notify: true,
1313
1519
  });
1314
- // Inline auth-facts here too (one round-trip), alongside the recovery-used flag. The
1315
- // session is enforced-re-enroll-restricted, but the frontend still seeds its facts
1316
- // from this projection (fail-closed: `principalFromClaims` forced isRoot:false, so no
1317
- // control-plane keys leak into a recovery session).
1318
- return this.sessionSuccessWithFacts(est, principal, { mfaRecoveryUsed: true });
1520
+ this.auditSignIn(principal, 'recovery_code');
1521
+ // The session is enforced-re-enroll-restricted, so no inline auth-facts (review round 1,
1522
+ // item C): the frontend reads the restricted shape from `/identity/auth-facts`.
1523
+ return this.sessionSuccessWithFacts(est, principal, restricted, { mfaRecoveryUsed: true });
1319
1524
  }
1320
1525
  /**
1321
1526
  * Fire-and-forget security notice for a recovery-code login (spec §6.1). Resolves the
@@ -1360,6 +1565,34 @@ let AuthController = class AuthController {
1360
1565
  /* best-effort: a resolver/email failure never fails the login */
1361
1566
  });
1362
1567
  }
1568
+ /** R12.2: `auth.login.succeeded` for an established session (fire-and-forget). */
1569
+ auditSignIn(principal, method) {
1570
+ (0, auth_audit_js_1.emitAuthAudit)(this.auditSink, (0, auth_audit_js_1.authEvent)(auth_audit_js_1.AUTH_LOGIN_SUCCEEDED, principal.id, principal.tenantId, { method }));
1571
+ }
1572
+ /** R12.2/R12.3: `auth.login.failed` for a RESOLVED account (the caller bounds it per window). */
1573
+ auditFailure(actorId, tenantId, method) {
1574
+ (0, auth_audit_js_1.emitAuthAudit)(this.auditSink, (0, auth_audit_js_1.authEvent)(auth_audit_js_1.AUTH_LOGIN_FAILED, actorId, tenantId, { method }));
1575
+ }
1576
+ /**
1577
+ * The durable session `logout` is about to end, when it is ACTIVE and a sink is bound; else
1578
+ * `null` (no row). A store error → `null` (the logout itself is unaffected).
1579
+ */
1580
+ async activeSessionForAudit(sessionId) {
1581
+ if (this.auditSink === undefined || this.auditSink === null)
1582
+ return null;
1583
+ if (this.sessionRecords === undefined || this.sessionRecords === null || sessionId === '') {
1584
+ return null;
1585
+ }
1586
+ try {
1587
+ const record = await this.sessionRecords.findSessionById(sessionId);
1588
+ return record !== null && record.status === 'active'
1589
+ ? { principalId: record.principalId, tenantId: record.tenantId }
1590
+ : null;
1591
+ }
1592
+ catch {
1593
+ return null;
1594
+ }
1595
+ }
1363
1596
  /**
1364
1597
  * Record an auth-SECURITY event (best-effort, fire-and-forget) and OPTIONALLY notify
1365
1598
  * the account owner. The REUSABLE audit+notify primitive (feature #140) — it
@@ -1570,7 +1803,11 @@ let AuthController = class AuthController {
1570
1803
  }
1571
1804
  if (durableFacts.accountActive !== true)
1572
1805
  return false;
1573
- const restricted = durableFacts.mustReenroll !== false;
1806
+ // Decision (1): `s_restriction ?? (s_must_reenroll ? 'reenroll' : null)`.
1807
+ const restricted = durableFacts.mustReenroll !== false || durableFacts.restriction === 'reenroll';
1808
+ // D-2: the mandatory-2FA restriction survives the rebuild. The rebuilt binding carries no
1809
+ // requirement stamp, so its first request re-evaluates (and restores a grace deadline).
1810
+ const mfaRestricted = !restricted && durableFacts.restriction === 'mfa_enrollment';
1574
1811
  const claims = {
1575
1812
  id: principalId,
1576
1813
  tenantId,
@@ -1579,6 +1816,7 @@ let AuthController = class AuthController {
1579
1816
  isRoot: !restricted && Array.isArray(this.rootIds) && this.rootIds.includes(principalId),
1580
1817
  sessionId,
1581
1818
  mustReenroll: restricted,
1819
+ ...(mfaRestricted ? { restriction: 'mfa_enrollment' } : {}),
1582
1820
  ...(fresh.facts ? { cachedFacts: (0, session_token_binder_js_1.factsSlot)(fresh.facts) } : {}),
1583
1821
  ...(fresh.subtree !== undefined && fresh.subtree.length > 0
1584
1822
  ? { scopeSubtreeNodeIds: [...fresh.subtree] }
@@ -1612,17 +1850,47 @@ let AuthController = class AuthController {
1612
1850
  }
1613
1851
  }
1614
1852
  /**
1615
- * `POST /auth/logout` — revoke the session. Idempotent + leak-free: revoking an
1616
- * unknown or already-revoked session still returns 204 (the engine's `revoke`
1617
- * swallows and returns void either way), so existence is never revealed.
1853
+ * `POST /auth/logout` — revoke the session the caller proves it owns (R15.1, D-9).
1854
+ * Proof is a bearer access token bound to `sessionId` or a refresh token of it
1855
+ * ({@link LogoutProofVerifier}):
1856
+ * - proven → revoke + unbind;
1857
+ * - proof for another session, or an unknown/invalid token → revoke nothing;
1858
+ * - no proof (an old frontend), or a proof lookup outage → revoke only while
1859
+ * `logoutRequiresProof` is false (counted `auth.logout.unproven`, one deprecation warning);
1860
+ * strict mode revokes nothing (fail closed).
1861
+ * Always 204 — never a 500, never an existence or ownership signal. Tokens are never logged.
1618
1862
  */
1619
- async logout(body) {
1863
+ async logout(body, authorization) {
1620
1864
  const sessionId = typeof body?.sessionId === 'string' ? body.sessionId : '';
1621
- // `revoke` is void + best-effort-idempotent by contract; it never throws a
1622
- // resource-existence signal. We still guard against an unexpected throw so a
1623
- // logout never 500s (which could itself be an existence oracle).
1624
1865
  try {
1866
+ const accessToken = (0, request_principal_js_1.extractBearerToken)(authorization);
1867
+ const refreshToken = typeof body?.refreshToken === 'string' && body.refreshToken !== ''
1868
+ ? body.refreshToken
1869
+ : null;
1870
+ const proof = this.logoutProof !== undefined && this.logoutProof !== null
1871
+ ? await this.logoutProof.verify(sessionId, accessToken, refreshToken)
1872
+ : accessToken === null && refreshToken === null
1873
+ ? 'none'
1874
+ : 'unavailable';
1875
+ if (proof === 'mismatch')
1876
+ return;
1877
+ if (proof !== 'proven') {
1878
+ // 'none' or 'unavailable': strict mode fails closed; compatibility mode keeps today's revoke.
1879
+ if (this.logoutRequiresProof === true)
1880
+ return;
1881
+ (0, logout_proof_js_1.recordUnprovenLogout)();
1882
+ }
1883
+ // R12.2: read the session before revoking, so `auth.logout` is written only when an ACTIVE
1884
+ // session ends, attributed to its own principal and tenant (never the request). Only with a
1885
+ // sink bound; logout is a lifecycle event, so this one read is allowed (ADR-0013).
1886
+ const ending = await this.activeSessionForAudit(sessionId);
1887
+ // `revoke` is void + best-effort-idempotent by contract; it never throws a
1888
+ // resource-existence signal. We still guard against an unexpected throw so a
1889
+ // logout never 500s (which could itself be an existence oracle).
1625
1890
  await this.sessionAuthority.revoke(sessionId);
1891
+ if (ending !== null) {
1892
+ (0, auth_audit_js_1.emitAuthAudit)(this.auditSink, (0, auth_audit_js_1.logoutEvent)(ending.principalId, ending.tenantId, proof === 'proven'));
1893
+ }
1626
1894
  // Drop the session-claims cushion so a post-logout rotate can't re-mint a
1627
1895
  // binding (the session is revoked anyway; this is defense-in-depth).
1628
1896
  if (this.tokenBinder !== undefined)
@@ -1658,6 +1926,13 @@ __decorate([
1658
1926
  __metadata("design:paramtypes", [Object, String]),
1659
1927
  __metadata("design:returntype", Promise)
1660
1928
  ], AuthController.prototype, "login", null);
1929
+ __decorate([
1930
+ (0, classification_decorator_js_1.Public)(),
1931
+ (0, common_1.Get)('login-options'),
1932
+ __metadata("design:type", Function),
1933
+ __metadata("design:paramtypes", []),
1934
+ __metadata("design:returntype", Object)
1935
+ ], AuthController.prototype, "loginOptions", null);
1661
1936
  __decorate([
1662
1937
  (0, classification_decorator_js_1.Public)(),
1663
1938
  (0, common_1.Get)('me'),
@@ -1720,8 +1995,9 @@ __decorate([
1720
1995
  (0, common_1.Post)('logout'),
1721
1996
  (0, common_1.HttpCode)(common_1.HttpStatus.NO_CONTENT),
1722
1997
  __param(0, (0, common_1.Body)()),
1998
+ __param(1, (0, common_1.Headers)('authorization')),
1723
1999
  __metadata("design:type", Function),
1724
- __metadata("design:paramtypes", [Object]),
2000
+ __metadata("design:paramtypes", [Object, String]),
1725
2001
  __metadata("design:returntype", Promise)
1726
2002
  ], AuthController.prototype, "logout", null);
1727
2003
  exports.AuthController = AuthController = __decorate([
@@ -1790,7 +2066,17 @@ exports.AuthController = AuthController = __decorate([
1790
2066
  __param(32, (0, common_1.Inject)(tokens_js_1.SHIELD_EMAIL_CHANGE_TOKEN_STORE)),
1791
2067
  __param(33, (0, common_2.Optional)()),
1792
2068
  __param(33, (0, common_1.Inject)(tokens_js_1.SHIELD_MESSAGE_DISPATCHER)),
1793
- __metadata("design:paramtypes", [Object, Object, pending_mfa_store_js_1.PendingMfaStore, Function, Object, Object, Object, Object, Object, Object, Object, String, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object])
2069
+ __param(34, (0, common_2.Optional)()),
2070
+ __param(34, (0, common_1.Inject)(logout_proof_js_1.SHIELD_LOGOUT_PROOF_VERIFIER)),
2071
+ __param(35, (0, common_2.Optional)()),
2072
+ __param(35, (0, common_1.Inject)(logout_proof_js_1.SHIELD_LOGOUT_REQUIRES_PROOF)),
2073
+ __param(36, (0, common_2.Optional)()),
2074
+ __param(36, (0, common_1.Inject)(tokens_js_3.SHIELD_MFA_REQUIREMENT)),
2075
+ __param(37, (0, common_2.Optional)()),
2076
+ __param(37, (0, common_1.Inject)(tokens_js_3.SHIELD_CAPTCHA_GATE)),
2077
+ __param(38, (0, common_2.Optional)()),
2078
+ __param(38, (0, common_1.Inject)(tokens_js_3.SHIELD_SIGN_IN_WITH)),
2079
+ __metadata("design:paramtypes", [Object, Object, pending_mfa_store_js_1.PendingMfaStore, Function, Object, Object, Object, Object, Object, Object, Object, String, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object])
1794
2080
  ], AuthController);
1795
2081
  /**
1796
2082
  * Narrow an `authenticated` result to its guaranteed principal. The core engine