@astram/guard 0.19.0 → 0.20.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 (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
@@ -0,0 +1,245 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CaptchaGate = exports.RedisCaptchaCounterStore = exports.CAPTCHA_BUMP_SCRIPT = exports.AUTH_CAPTCHA_FAIL_OPEN = exports.AUTH_CAPTCHA_FAILED = exports.CAPTCHA_WINDOW_MS = void 0;
4
+ /**
5
+ * The CAPTCHA gate on `/auth/login`, `/auth/forgot-password` and `/auth/reset-password`
6
+ * (auth capabilities R7.2–R7.5, X2, X3; AC-12, AC-13).
7
+ *
8
+ * ── WHEN A CHALLENGE IS REQUIRED ────────────────────────────────────────────
9
+ * `always` ⇒ every request to a covered endpoint. `adaptive` ⇒ once ANY of these per-endpoint
10
+ * counters reaches `CAPTCHA_THRESHOLD`:
11
+ * `captcha:<endpoint>:pid:<principalId>` the resolved account (every spelling shares it);
12
+ * `captcha:<endpoint>:txt:<sha256(canonical typed text)>` the typed hint, bumped for known
13
+ * AND unknown names alike, so the same typed text reaches the threshold at the same attempt
14
+ * whether or not an account exists (X2);
15
+ * `captcha:<endpoint>:ip:<ip>` only when per-IP limiting is on (explicit `trust proxy`),
16
+ * like every other per-IP limiter (otherwise every client shares the proxy's IP).
17
+ * Keys live under `shield:captcha:` with a fixed 15-minute window from the first hit; no
18
+ * personal data in a key. Login bumps on a failed password; forgot-password on every request
19
+ * (a request-rate cap: it has no failure signal); reset-password on a bad reset token (IP only:
20
+ * no account is known before the token is consumed). A success clears the account and
21
+ * typed-text keys, never the IP key.
22
+ *
23
+ * ── REDIS ── one MGET to decide, one EVAL to bump. A Redis error while deciding REQUIRES the
24
+ * challenge (fail closed, Shield #1): the attempt then needs a valid token, and the failure
25
+ * response carries `captcha: 'required'`. A bump error is treated the same way for the body flag.
26
+ *
27
+ * ── VERIFYING ── the token is checked BEFORE any password hash, code check or reset-token use
28
+ * (R7.4). A missing token never calls the provider. Outcomes:
29
+ * passed / not required ⇒ proceed;
30
+ * rejected ⇒ fail (counter `auth.captcha.failed`; audit row `auth.captcha.failed` for a
31
+ * RESOLVED account, at most one per account, endpoint and window — R12.3 bounding);
32
+ * provider unavailable ⇒ `CAPTCHA_FAIL_MODE`: `closed` (default) is a failure with reason
33
+ * `unavailable`; `open` proceeds, counted `auth.captcha.fail_open` every time and audited
34
+ * `auth.captcha.fail_open` when the account resolved (the default sink skips actor-less
35
+ * rows, so the counter is the record for unknown names). Lockout and limits still apply.
36
+ *
37
+ * Counters are one structured log line each (R12.4 style): fixed keys, no hint, no token.
38
+ */
39
+ const common_1 = require("@nestjs/common");
40
+ const core_1 = require("@astram/core");
41
+ const auth_audit_js_1 = require("./auth-audit.js");
42
+ const canonical_hint_js_1 = require("./rate-limit/canonical-hint.js");
43
+ /** Fixed counter window (ms). */
44
+ exports.CAPTCHA_WINDOW_MS = 15 * 60 * 1000;
45
+ exports.AUTH_CAPTCHA_FAILED = 'auth.captcha.failed';
46
+ exports.AUTH_CAPTCHA_FAIL_OPEN = 'auth.captcha.fail_open';
47
+ /**
48
+ * Bump every key and arm the window on the first hit (or a key left without a TTL). Returns the
49
+ * new counts, in key order. KEYS = the counter keys; ARGV[1] = window ms.
50
+ */
51
+ exports.CAPTCHA_BUMP_SCRIPT = `local out = {}
52
+ for i, k in ipairs(KEYS) do
53
+ local n = redis.call('INCR', k)
54
+ if n == 1 or redis.call('PTTL', k) == -1 then
55
+ redis.call('PEXPIRE', k, ARGV[1])
56
+ end
57
+ out[i] = n
58
+ end
59
+ return out`;
60
+ /** {@link CaptchaCounterStore} over the guard's bounded ioredis client. */
61
+ class RedisCaptchaCounterStore {
62
+ redis;
63
+ windowMs;
64
+ constructor(redis, windowMs = exports.CAPTCHA_WINDOW_MS) {
65
+ this.redis = redis;
66
+ this.windowMs = windowMs;
67
+ }
68
+ k(key) {
69
+ return `shield:captcha:${key}`;
70
+ }
71
+ async counts(keys) {
72
+ if (keys.length === 0)
73
+ return [];
74
+ const raw = await this.redis.mget(...keys.map((k) => this.k(k)));
75
+ return raw.map((v) => {
76
+ const n = v === null ? 0 : Number(v);
77
+ return Number.isFinite(n) ? n : 0;
78
+ });
79
+ }
80
+ async bump(keys) {
81
+ if (keys.length === 0)
82
+ return [];
83
+ const out = (await this.redis.eval(exports.CAPTCHA_BUMP_SCRIPT, keys.length, ...keys.map((k) => this.k(k)), String(this.windowMs)));
84
+ if (!Array.isArray(out))
85
+ throw new Error('captcha counter: unexpected reply');
86
+ return out.map((n) => Number(n));
87
+ }
88
+ async clear(keys) {
89
+ if (keys.length === 0)
90
+ return;
91
+ try {
92
+ await this.redis.del(...keys.map((k) => this.k(k)));
93
+ }
94
+ catch {
95
+ // Best-effort: the window expires the keys anyway.
96
+ }
97
+ }
98
+ /**
99
+ * Remove keys and REJECT on a store error (auth capabilities R11.1: an admin unlock clears the
100
+ * account counters and must not report a success it did not achieve). Not on the port: only the
101
+ * unlocker uses it.
102
+ */
103
+ async clearStrict(keys) {
104
+ if (keys.length === 0)
105
+ return;
106
+ await this.redis.del(...keys.map((k) => this.k(k)));
107
+ }
108
+ async firstInWindow(key) {
109
+ try {
110
+ return (await this.redis.set(this.k(key), '1', 'PX', this.windowMs, 'NX')) === 'OK';
111
+ }
112
+ catch {
113
+ return false;
114
+ }
115
+ }
116
+ }
117
+ exports.RedisCaptchaCounterStore = RedisCaptchaCounterStore;
118
+ class CaptchaGate {
119
+ deps;
120
+ logger;
121
+ constructor(deps) {
122
+ this.deps = deps;
123
+ this.logger = deps.logger ?? new common_1.Logger('ShieldCaptcha');
124
+ }
125
+ /** The public part of the config (provider, site key, mode). */
126
+ get publicOptions() {
127
+ const { provider, siteKey, mode } = this.deps.config;
128
+ return { provider, siteKey, mode };
129
+ }
130
+ /** True when this endpoint is protected (`CAPTCHA_ENDPOINTS`). */
131
+ covers(endpoint) {
132
+ return this.deps.config.endpoints.includes(endpoint);
133
+ }
134
+ /** True when the mode is `always` (every attempt is challenged). */
135
+ get always() {
136
+ return this.deps.config.mode === 'always';
137
+ }
138
+ keys(endpoint, s) {
139
+ const keys = [];
140
+ if (s.accountId)
141
+ keys.push(`${endpoint}:pid:${s.accountId}`);
142
+ if (typeof s.typedText === 'string' && s.typedText !== '') {
143
+ keys.push(`${endpoint}:txt:${(0, canonical_hint_js_1.hintKeyDigest)(s.typedText)}`);
144
+ }
145
+ if (s.ip)
146
+ keys.push(`${endpoint}:ip:${s.ip}`);
147
+ return keys;
148
+ }
149
+ overThreshold(counts) {
150
+ return counts.some((n) => n >= this.deps.config.threshold);
151
+ }
152
+ /** Is a challenge required now? A store error ⇒ `true` (fail closed). */
153
+ async required(endpoint, subject) {
154
+ if (!this.covers(endpoint))
155
+ return false;
156
+ if (this.always)
157
+ return true;
158
+ const keys = this.keys(endpoint, subject);
159
+ if (keys.length === 0)
160
+ return false;
161
+ try {
162
+ return this.overThreshold(await this.deps.counters.counts(keys));
163
+ }
164
+ catch {
165
+ return true;
166
+ }
167
+ }
168
+ /**
169
+ * Decide and, when required, verify `token`. Call BEFORE any hash / code check. Never throws.
170
+ */
171
+ async check(endpoint, subject, token, remoteIp) {
172
+ if (!(await this.required(endpoint, subject)))
173
+ return { kind: 'not_required' };
174
+ const account = subject.accountId ? 'known' : 'unknown';
175
+ if (typeof token !== 'string' || token.trim() === '') {
176
+ this.count(exports.AUTH_CAPTCHA_FAILED, endpoint, account, 'missing');
177
+ return { kind: 'failed' };
178
+ }
179
+ let result;
180
+ try {
181
+ result = await this.deps.verifier.verify({
182
+ token,
183
+ expectedAction: endpoint,
184
+ ...(remoteIp ? { remoteIp } : {}),
185
+ });
186
+ }
187
+ catch {
188
+ result = { ok: false, reason: 'unavailable' };
189
+ }
190
+ if (result.ok)
191
+ return { kind: 'passed' };
192
+ if ((0, core_1.isCaptchaUnavailable)(result) && this.deps.config.failMode === 'open') {
193
+ this.count(exports.AUTH_CAPTCHA_FAIL_OPEN, endpoint, account);
194
+ if (subject.accountId && subject.tenantId) {
195
+ (0, auth_audit_js_1.emitAuthAudit)(this.deps.auditSink, (0, auth_audit_js_1.authEvent)(exports.AUTH_CAPTCHA_FAIL_OPEN, subject.accountId, subject.tenantId, { endpoint }));
196
+ }
197
+ return { kind: 'fail_open' };
198
+ }
199
+ this.count(exports.AUTH_CAPTCHA_FAILED, endpoint, account, result.reason);
200
+ if (subject.accountId && subject.tenantId) {
201
+ const accountId = subject.accountId;
202
+ const tenantId = subject.tenantId;
203
+ const reason = result.reason;
204
+ void this.deps.counters
205
+ .firstInWindow(`audit:${endpoint}:pid:${accountId}`)
206
+ .then((first) => {
207
+ if (first) {
208
+ (0, auth_audit_js_1.emitAuthAudit)(this.deps.auditSink, (0, auth_audit_js_1.authEvent)(exports.AUTH_CAPTCHA_FAILED, accountId, tenantId, { endpoint, reason }));
209
+ }
210
+ })
211
+ .catch(() => undefined);
212
+ }
213
+ return { kind: 'failed' };
214
+ }
215
+ /**
216
+ * Count one failure (login: wrong password; forgot: every request; reset: bad token). Returns
217
+ * whether the NEXT attempt needs a challenge (the failure body's `captcha: 'required'` flag). A
218
+ * store error ⇒ `true` (fail closed, consistent with {@link required}).
219
+ */
220
+ async recordFailure(endpoint, subject) {
221
+ if (!this.covers(endpoint))
222
+ return false;
223
+ if (this.always)
224
+ return true;
225
+ const keys = this.keys(endpoint, subject);
226
+ let counts = [];
227
+ try {
228
+ counts = await this.deps.counters.bump(keys);
229
+ }
230
+ catch {
231
+ return true;
232
+ }
233
+ return this.overThreshold(counts);
234
+ }
235
+ /** A success clears the account and typed-text counters (never the IP one). Best-effort. */
236
+ async recordSuccess(endpoint, subject) {
237
+ if (!this.covers(endpoint))
238
+ return;
239
+ await this.deps.counters.clear(this.keys(endpoint, { ...subject, ip: null }));
240
+ }
241
+ count(counter, endpoint, account, reason) {
242
+ this.logger.log({ counter, endpoint, account, ...(reason ? { reason } : {}) });
243
+ }
244
+ }
245
+ exports.CaptchaGate = CaptchaGate;
@@ -102,7 +102,22 @@ export interface LoginRequest {
102
102
  /** Username or email — resolved to a principal by the credential resolver. */
103
103
  readonly identityHint: string;
104
104
  /** ⛔ The plaintext password — verified then discarded; NEVER logged/echoed. */
105
- readonly password: string;
105
+ readonly password: string; /**
106
+ * Auth capabilities R7: the CAPTCHA widget's response token, required once the server answers
107
+ * `captcha: 'required'` (or always, in `always` mode). Verified with the provider, never logged.
108
+ */
109
+ readonly captchaToken?: string;
110
+ }
111
+ /** `GET /auth/login-options` (auth capabilities R7.3, R13.2): public sign-in page settings. */
112
+ export interface LoginOptionsView {
113
+ /** Accepted sign-in forms (`['email']` or `['email', 'username']`). */
114
+ readonly signInWith: readonly ('email' | 'username')[];
115
+ /** Public CAPTCHA settings, or `null` when CAPTCHA is off. Never the secret. */
116
+ readonly captcha: {
117
+ readonly provider: 'turnstile' | 'recaptcha-v3' | 'recaptcha-v2';
118
+ readonly siteKey: string;
119
+ readonly mode: 'adaptive' | 'always';
120
+ } | null;
106
121
  }
107
122
  /**
108
123
  * The MFA-challenge branch of a login/step-up response: the password was correct
@@ -174,6 +189,12 @@ export type RefreshResponse = SessionSuccess;
174
189
  */
175
190
  export interface LogoutRequest {
176
191
  readonly sessionId: string;
192
+ /**
193
+ * OPTIONAL proof of ownership (R15.1): a refresh token of the named session. An
194
+ * `Authorization: Bearer <access token>` bound to the session is the other accepted proof.
195
+ * Without either, the session is revoked only while `logoutRequiresProof` is `false`.
196
+ */
197
+ readonly refreshToken?: string;
177
198
  }
178
199
  /**
179
200
  * `POST /auth/logout` acknowledgement. The endpoint returns HTTP 204 (no body) on
@@ -1 +1 @@
1
- {"version":3,"file":"dto.d.ts","sourceRoot":"","sources":["../../src/auth/dto.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AASH;;;;;;GAMG;AACH,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,QAAQ,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,CAAC;IAC9D,oBAAoB;IACpB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,oBAAoB;IACpB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;GAKG;AACH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC;QAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;QACxB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;QAC1B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAC3B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;KAC9B,CAAC,CAAC;IACH,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;CACtC;AAED,kFAAkF;AAClF,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAC;IACnC;;;;;;OAMG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,OAAO,CAAC;IACnC;;;;;;;;;OASG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,aAAa,CAAC;CACpC;AAMD,uCAAuC;AACvC,MAAM,WAAW,YAAY;IAC3B,8EAA8E;IAC9E,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,+EAA+E;IAC/E,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;GAMG;AACH,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,WAAW,EAAE,IAAI,CAAC;IAC3B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;CACjC;AAED;;;;;;GAMG;AACH,MAAM,MAAM,aAAa,GAAG,cAAc,GAAG,oBAAoB,CAAC;AAMlE;;;;;;;GAOG;AACH,MAAM,WAAW,UAAU;IACzB,kFAAkF;IAClF,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,+EAA+E;IAC/E,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,eAAe,CAAC;IAC1C,iEAAiE;IACjE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED;;;;GAIG;AACH,MAAM,MAAM,WAAW,GAAG,cAAc,CAAC;AAMzC;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,eAAe,GAAG,cAAc,CAAC;AAM7C;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED;;;;GAIG;AACH,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAC;CACxB"}
1
+ {"version":3,"file":"dto.d.ts","sourceRoot":"","sources":["../../src/auth/dto.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Auth capabilities R1.9: when `signInWith` includes `'username'`, log once at boot how many
3
+ * active users have a login name (`users.u_login_name`). Count only, no names. Read through the
4
+ * guard's runtime pool. A missing table (errno 1146) or column (1054, migration 103 not applied)
5
+ * skips with one warning; any other error warns without detail. Never blocks boot.
6
+ */
7
+ import { type OnApplicationBootstrap } from '@nestjs/common';
8
+ import type { SignInMethod } from '../authentication/sign-in-with.js';
9
+ export type CountQuery = (sql: string) => Promise<unknown[]>;
10
+ export declare class LoginNameCountBoot implements OnApplicationBootstrap {
11
+ private readonly query;
12
+ /** Already-qualified `<app>`.`users` (from `qualifiedTableName`). */
13
+ private readonly usersTable;
14
+ private readonly signInWith;
15
+ private readonly logger;
16
+ constructor(query: CountQuery,
17
+ /** Already-qualified `<app>`.`users` (from `qualifiedTableName`). */
18
+ usersTable: string, signInWith: readonly SignInMethod[], logger?: {
19
+ log(message: string): void;
20
+ warn(message: string): void;
21
+ });
22
+ onApplicationBootstrap(): Promise<void>;
23
+ }
24
+ //# sourceMappingURL=login-name-count-boot.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"login-name-count-boot.d.ts","sourceRoot":"","sources":["../../src/auth/login-name-count-boot.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAU,KAAK,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AAErE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mCAAmC,CAAC;AAKtE,MAAM,MAAM,UAAU,GAAG,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;AAE7D,qBAAa,kBAAmB,YAAW,sBAAsB;IAI7D,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,qEAAqE;IACrE,OAAO,CAAC,QAAQ,CAAC,UAAU;IAC3B,OAAO,CAAC,QAAQ,CAAC,UAAU;IAN7B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA8D;gBAGlE,KAAK,EAAE,UAAU;IAClC,qEAAqE;IACpD,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,SAAS,YAAY,EAAE,EACpD,MAAM,CAAC,EAAE;QAAE,GAAG,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE;IAKhE,sBAAsB,IAAI,OAAO,CAAC,IAAI,CAAC;CAsB9C"}
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LoginNameCountBoot = void 0;
4
+ /**
5
+ * Auth capabilities R1.9: when `signInWith` includes `'username'`, log once at boot how many
6
+ * active users have a login name (`users.u_login_name`). Count only, no names. Read through the
7
+ * guard's runtime pool. A missing table (errno 1146) or column (1054, migration 103 not applied)
8
+ * skips with one warning; any other error warns without detail. Never blocks boot.
9
+ */
10
+ const common_1 = require("@nestjs/common");
11
+ const ER_BAD_FIELD_ERROR = 1054;
12
+ const ER_NO_SUCH_TABLE = 1146;
13
+ class LoginNameCountBoot {
14
+ query;
15
+ usersTable;
16
+ signInWith;
17
+ logger;
18
+ constructor(query,
19
+ /** Already-qualified `<app>`.`users` (from `qualifiedTableName`). */
20
+ usersTable, signInWith, logger) {
21
+ this.query = query;
22
+ this.usersTable = usersTable;
23
+ this.signInWith = signInWith;
24
+ this.logger = logger ?? new common_1.Logger('ShieldAuth');
25
+ }
26
+ async onApplicationBootstrap() {
27
+ if (!this.signInWith.includes('username'))
28
+ return;
29
+ try {
30
+ const rows = await this.query(`SELECT COUNT(*) AS n FROM ${this.usersTable} ` +
31
+ `WHERE u_login_name IS NOT NULL AND u_status = 'active'`);
32
+ const raw = rows[0]?.n;
33
+ const n = typeof raw === 'bigint' ? raw.toString() : String(Number(raw ?? 0));
34
+ this.logger.log(`signInWith includes username: ${n} active users have a login name`);
35
+ }
36
+ catch (err) {
37
+ const errno = err?.errno;
38
+ if (errno === ER_NO_SUCH_TABLE || errno === ER_BAD_FIELD_ERROR) {
39
+ this.logger.warn('signInWith includes username but users.u_login_name is missing (apply migration 103); ' +
40
+ 'login-name count skipped');
41
+ return;
42
+ }
43
+ this.logger.warn('signInWith includes username: could not count users with a login name');
44
+ }
45
+ }
46
+ }
47
+ exports.LoginNameCountBoot = LoginNameCountBoot;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Logout proof (R15.1, D-9, AC-19). `POST /auth/logout` is `@Public()`, so the session it ends
3
+ * must be proven by the caller, not just named:
4
+ *
5
+ * - a bearer access token whose bound claims carry the named `sessionId`, OR
6
+ * - a refresh token whose keyed hash (`SHIELD_REFRESH_HASHER`) finds a refresh record of that
7
+ * session (`SessionRecordStore.findRefreshByHash`). Any record status counts: replaying a
8
+ * rotated token already kills the whole lineage through `/auth/refresh`, so accepting it here
9
+ * gives a holder no new power, and it keeps logout working across a refresh race.
10
+ *
11
+ * Outcomes:
12
+ * - `proven` → a supplied token proves the named session.
13
+ * - `mismatch` → a token was supplied but proves another session, or is unknown / invalid.
14
+ * - `none` → no bearer and no refresh token were supplied (an old frontend).
15
+ * - `unavailable` → no token proved the session and a lookup failed (Redis or DB error). The
16
+ * controller treats it like `none`: strict mode revokes nothing (fail closed),
17
+ * compatibility mode revokes as before (today's behaviour, counted).
18
+ *
19
+ * Raw tokens are never logged; the unproven counter carries no ids.
20
+ */
21
+ import { Logger, type Provider } from '@nestjs/common';
22
+ import type { RefreshTokenHasher, SessionRecordStore } from '@astram/core';
23
+ import type { SessionTokenBinder } from '../request-auth/session-token-binder.js';
24
+ /** DI token for the {@link LogoutProofVerifier}. */
25
+ export declare const SHIELD_LOGOUT_PROOF_VERIFIER: unique symbol;
26
+ /** DI token for the `logoutRequiresProof` option (boolean; default `false`). */
27
+ export declare const SHIELD_LOGOUT_REQUIRES_PROOF: unique symbol;
28
+ /** The fixed counter key written for every unproven logout that still revokes (R12.4 style). */
29
+ export declare const LOGOUT_UNPROVEN_COUNTER = "auth.logout.unproven";
30
+ export type LogoutProofOutcome = 'proven' | 'mismatch' | 'none' | 'unavailable';
31
+ export declare class LogoutProofVerifier {
32
+ private readonly binder;
33
+ private readonly hasher;
34
+ private readonly records;
35
+ constructor(binder: Pick<SessionTokenBinder, 'resolve'> | null, hasher: RefreshTokenHasher | null, records: Pick<SessionRecordStore, 'findRefreshByHash'> | null);
36
+ /**
37
+ * Classify the proof a logout request carries for `sessionId`. Never throws.
38
+ * @param accessToken the bearer token from `Authorization`, or `null`.
39
+ * @param refreshToken the body's `refreshToken`, or `null`.
40
+ */
41
+ verify(sessionId: string, accessToken: string | null, refreshToken: string | null): Promise<LogoutProofOutcome>;
42
+ }
43
+ /** The process-local count of unproven logouts that still revoked (compatibility mode). */
44
+ export declare function unprovenLogoutCount(): number;
45
+ /**
46
+ * Count one unproven logout that revoked (compatibility mode): a structured log line with the
47
+ * fixed key {@link LOGOUT_UNPROVEN_COUNTER} and no ids, plus a one-time deprecation warning per
48
+ * process.
49
+ */
50
+ export declare function recordUnprovenLogout(logger?: Pick<Logger, 'log' | 'warn'>): void;
51
+ /** The verifier over the bound token binder, refresh hasher and durable record store. */
52
+ export declare const logoutProofVerifierProvider: Provider;
53
+ /** Bind the `logoutRequiresProof` option (absent → `false`, the compatibility default). */
54
+ export declare function makeLogoutRequiresProofProvider(requiresProof?: boolean): Provider;
55
+ //# sourceMappingURL=logout-proof.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logout-proof.d.ts","sourceRoot":"","sources":["../../src/auth/logout-proof.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAE,MAAM,EAAE,KAAK,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AACvD,OAAO,KAAK,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAC3E,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yCAAyC,CAAC;AAKlF,oDAAoD;AACpD,eAAO,MAAM,4BAA4B,eAAyC,CAAC;AACnF,gFAAgF;AAChF,eAAO,MAAM,4BAA4B,eAAyC,CAAC;AACnF,gGAAgG;AAChG,eAAO,MAAM,uBAAuB,yBAAyB,CAAC;AAE9D,MAAM,MAAM,kBAAkB,GAAG,QAAQ,GAAG,UAAU,GAAG,MAAM,GAAG,aAAa,CAAC;AAEhF,qBAAa,mBAAmB;IAE5B,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAFP,MAAM,EAAE,IAAI,CAAC,kBAAkB,EAAE,SAAS,CAAC,GAAG,IAAI,EAClD,MAAM,EAAE,kBAAkB,GAAG,IAAI,EACjC,OAAO,EAAE,IAAI,CAAC,kBAAkB,EAAE,mBAAmB,CAAC,GAAG,IAAI;IAGhF;;;;OAIG;IACG,MAAM,CACV,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,GAAG,IAAI,EAC1B,YAAY,EAAE,MAAM,GAAG,IAAI,GAC1B,OAAO,CAAC,kBAAkB,CAAC;CAoC/B;AAMD,2FAA2F;AAC3F,wBAAgB,mBAAmB,IAAI,MAAM,CAE5C;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,GAAE,IAAI,CAAC,MAAM,EAAE,KAAK,GAAG,MAAM,CAAe,GAAG,IAAI,CAY7F;AAED,yFAAyF;AACzF,eAAO,MAAM,2BAA2B,EAAE,QAazC,CAAC;AAEF,2FAA2F;AAC3F,wBAAgB,+BAA+B,CAAC,aAAa,CAAC,EAAE,OAAO,GAAG,QAAQ,CAEjF"}
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+ /**
3
+ * Logout proof (R15.1, D-9, AC-19). `POST /auth/logout` is `@Public()`, so the session it ends
4
+ * must be proven by the caller, not just named:
5
+ *
6
+ * - a bearer access token whose bound claims carry the named `sessionId`, OR
7
+ * - a refresh token whose keyed hash (`SHIELD_REFRESH_HASHER`) finds a refresh record of that
8
+ * session (`SessionRecordStore.findRefreshByHash`). Any record status counts: replaying a
9
+ * rotated token already kills the whole lineage through `/auth/refresh`, so accepting it here
10
+ * gives a holder no new power, and it keeps logout working across a refresh race.
11
+ *
12
+ * Outcomes:
13
+ * - `proven` → a supplied token proves the named session.
14
+ * - `mismatch` → a token was supplied but proves another session, or is unknown / invalid.
15
+ * - `none` → no bearer and no refresh token were supplied (an old frontend).
16
+ * - `unavailable` → no token proved the session and a lookup failed (Redis or DB error). The
17
+ * controller treats it like `none`: strict mode revokes nothing (fail closed),
18
+ * compatibility mode revokes as before (today's behaviour, counted).
19
+ *
20
+ * Raw tokens are never logged; the unproven counter carries no ids.
21
+ */
22
+ Object.defineProperty(exports, "__esModule", { value: true });
23
+ exports.logoutProofVerifierProvider = exports.LogoutProofVerifier = exports.LOGOUT_UNPROVEN_COUNTER = exports.SHIELD_LOGOUT_REQUIRES_PROOF = exports.SHIELD_LOGOUT_PROOF_VERIFIER = void 0;
24
+ exports.unprovenLogoutCount = unprovenLogoutCount;
25
+ exports.recordUnprovenLogout = recordUnprovenLogout;
26
+ exports.makeLogoutRequiresProofProvider = makeLogoutRequiresProofProvider;
27
+ const common_1 = require("@nestjs/common");
28
+ const tokens_js_1 = require("../request-auth/tokens.js");
29
+ const mariadb_session_record_store_js_1 = require("../session/mariadb-session-record-store.js");
30
+ const crypto_adapters_js_1 = require("../session/crypto-adapters.js");
31
+ /** DI token for the {@link LogoutProofVerifier}. */
32
+ exports.SHIELD_LOGOUT_PROOF_VERIFIER = Symbol('SHIELD_LOGOUT_PROOF_VERIFIER');
33
+ /** DI token for the `logoutRequiresProof` option (boolean; default `false`). */
34
+ exports.SHIELD_LOGOUT_REQUIRES_PROOF = Symbol('SHIELD_LOGOUT_REQUIRES_PROOF');
35
+ /** The fixed counter key written for every unproven logout that still revokes (R12.4 style). */
36
+ exports.LOGOUT_UNPROVEN_COUNTER = 'auth.logout.unproven';
37
+ class LogoutProofVerifier {
38
+ binder;
39
+ hasher;
40
+ records;
41
+ constructor(binder, hasher, records) {
42
+ this.binder = binder;
43
+ this.hasher = hasher;
44
+ this.records = records;
45
+ }
46
+ /**
47
+ * Classify the proof a logout request carries for `sessionId`. Never throws.
48
+ * @param accessToken the bearer token from `Authorization`, or `null`.
49
+ * @param refreshToken the body's `refreshToken`, or `null`.
50
+ */
51
+ async verify(sessionId, accessToken, refreshToken) {
52
+ if (accessToken === null && refreshToken === null)
53
+ return 'none';
54
+ let failed = false;
55
+ if (accessToken !== null) {
56
+ if (this.binder === null) {
57
+ failed = true;
58
+ }
59
+ else {
60
+ try {
61
+ const claims = await this.binder.resolve(accessToken);
62
+ if (claims !== null && sessionId !== '' && claims.sessionId === sessionId) {
63
+ return 'proven';
64
+ }
65
+ }
66
+ catch {
67
+ failed = true;
68
+ }
69
+ }
70
+ }
71
+ if (refreshToken !== null) {
72
+ if (this.hasher === null || this.records === null) {
73
+ failed = true;
74
+ }
75
+ else {
76
+ try {
77
+ const record = await this.records.findRefreshByHash(this.hasher.hash(refreshToken));
78
+ if (record !== null && sessionId !== '' && record.sessionId === sessionId) {
79
+ return 'proven';
80
+ }
81
+ }
82
+ catch {
83
+ failed = true;
84
+ }
85
+ }
86
+ }
87
+ return failed ? 'unavailable' : 'mismatch';
88
+ }
89
+ }
90
+ exports.LogoutProofVerifier = LogoutProofVerifier;
91
+ let unprovenCount = 0;
92
+ let deprecationWarned = false;
93
+ const proofLogger = new common_1.Logger('ShieldAuth');
94
+ /** The process-local count of unproven logouts that still revoked (compatibility mode). */
95
+ function unprovenLogoutCount() {
96
+ return unprovenCount;
97
+ }
98
+ /**
99
+ * Count one unproven logout that revoked (compatibility mode): a structured log line with the
100
+ * fixed key {@link LOGOUT_UNPROVEN_COUNTER} and no ids, plus a one-time deprecation warning per
101
+ * process.
102
+ */
103
+ function recordUnprovenLogout(logger = proofLogger) {
104
+ unprovenCount += 1;
105
+ if (!deprecationWarned) {
106
+ deprecationWarned = true;
107
+ logger.warn('POST /auth/logout without proof (no refresh token, no bearer) still revokes the named ' +
108
+ 'session; this is deprecated. Send the refresh token or the access token, then set ' +
109
+ 'ShieldModule.forRoot({ logoutRequiresProof: true }). It becomes the default at the next ' +
110
+ 'guard major.');
111
+ }
112
+ logger.log({ counter: exports.LOGOUT_UNPROVEN_COUNTER, count: unprovenCount });
113
+ }
114
+ /** The verifier over the bound token binder, refresh hasher and durable record store. */
115
+ exports.logoutProofVerifierProvider = {
116
+ provide: exports.SHIELD_LOGOUT_PROOF_VERIFIER,
117
+ useFactory: (binder, hasher, records) => new LogoutProofVerifier(binder ?? null, hasher ?? null, records ?? null),
118
+ inject: [
119
+ { token: tokens_js_1.SHIELD_SESSION_TOKEN_BINDER, optional: true },
120
+ { token: crypto_adapters_js_1.SHIELD_REFRESH_HASHER, optional: true },
121
+ { token: mariadb_session_record_store_js_1.SHIELD_SESSION_RECORD_STORE, optional: true },
122
+ ],
123
+ };
124
+ /** Bind the `logoutRequiresProof` option (absent → `false`, the compatibility default). */
125
+ function makeLogoutRequiresProofProvider(requiresProof) {
126
+ return { provide: exports.SHIELD_LOGOUT_REQUIRES_PROOF, useValue: requiresProof === true };
127
+ }
@@ -33,6 +33,11 @@ export interface PendingMfaChallenge {
33
33
  readonly identityHint: string;
34
34
  /** SHA-256 of the stored credential hash at password time (credential-change guard). */
35
35
  readonly credentialFingerprint: string;
36
+ /**
37
+ * The resolved account's tenant at password time (R12.2): the tenant of a failed second-factor
38
+ * audit row. Optional, so a challenge stored before this field still parses.
39
+ */
40
+ readonly tenantId?: string;
36
41
  }
37
42
  /**
38
43
  * The in-process challenge store (single instance / tests) and the class token the
@@ -1 +1 @@
1
- {"version":3,"file":"pending-mfa-store.d.ts","sourceRoot":"","sources":["../../src/auth/pending-mfa-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAGH,OAAO,KAAK,KAAK,MAAM,SAAS,CAAC;AAEjC,wFAAwF;AACxF,MAAM,WAAW,mBAAmB;IAClC,kFAAkF;IAClF,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,qFAAqF;IACrF,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,wFAAwF;IACxF,QAAQ,CAAC,qBAAqB,EAAE,MAAM,CAAC;CACxC;AA2CD;;;GAGG;AACH,qBAAa,eAAe;IAIxB,SAAS,CAAC,QAAQ,CAAC,KAAK,EAAE,MAAM;IAChC,iEAAiE;IACjE,OAAO,CAAC,QAAQ,CAAC,KAAK;IALxB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAkC;gBAGrC,KAAK,GAAE,MAAuB;IACjD,iEAAiE;IAChD,KAAK,GAAE,MAAM,MAAiB;IAGjD,uFAAuF;IACjF,KAAK,CAAC,SAAS,EAAE,mBAAmB,GAAG,OAAO,CAAC,MAAM,CAAC;IAU5D;;;OAGG;IACG,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC;IAUjE,mFAAmF;IACnF,OAAO,CAAC,KAAK;CAMd;AAKD;;;;GAIG;AACH,qBAAa,oBAAqB,SAAQ,eAAe;IAErD,OAAO,CAAC,QAAQ,CAAC,KAAK;gBAAL,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,KAAK,GAAG,QAAQ,CAAC,EACrD,KAAK,GAAE,MAAuB;IAKjB,KAAK,CAAC,SAAS,EAAE,mBAAmB,GAAG,OAAO,CAAC,MAAM,CAAC;IAWtD,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC;CAU3E"}
1
+ {"version":3,"file":"pending-mfa-store.d.ts","sourceRoot":"","sources":["../../src/auth/pending-mfa-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAGH,OAAO,KAAK,KAAK,MAAM,SAAS,CAAC;AAEjC,wFAAwF;AACxF,MAAM,WAAW,mBAAmB;IAClC,kFAAkF;IAClF,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,qFAAqF;IACrF,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,wFAAwF;IACxF,QAAQ,CAAC,qBAAqB,EAAE,MAAM,CAAC;IACvC;;;OAGG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC5B;AA4CD;;;GAGG;AACH,qBAAa,eAAe;IAIxB,SAAS,CAAC,QAAQ,CAAC,KAAK,EAAE,MAAM;IAChC,iEAAiE;IACjE,OAAO,CAAC,QAAQ,CAAC,KAAK;IALxB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAkC;gBAGrC,KAAK,GAAE,MAAuB;IACjD,iEAAiE;IAChD,KAAK,GAAE,MAAM,MAAiB;IAGjD,uFAAuF;IACjF,KAAK,CAAC,SAAS,EAAE,mBAAmB,GAAG,OAAO,CAAC,MAAM,CAAC;IAU5D;;;OAGG;IACG,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC;IAUjE,mFAAmF;IACnF,OAAO,CAAC,KAAK;CAMd;AAKD;;;;GAIG;AACH,qBAAa,oBAAqB,SAAQ,eAAe;IAErD,OAAO,CAAC,QAAQ,CAAC,KAAK;gBAAL,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,KAAK,GAAG,QAAQ,CAAC,EACrD,KAAK,GAAE,MAAuB;IAKjB,KAAK,CAAC,SAAS,EAAE,mBAAmB,GAAG,OAAO,CAAC,MAAM,CAAC;IAYtD,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC;CAU3E"}
@@ -56,6 +56,7 @@ function parseChallenge(value) {
56
56
  principalId: v.principalId,
57
57
  identityHint: v.identityHint,
58
58
  credentialFingerprint: v.credentialFingerprint,
59
+ ...(typeof v.tenantId === 'string' && v.tenantId !== '' ? { tenantId: v.tenantId } : {}),
59
60
  };
60
61
  }
61
62
  /**
@@ -127,6 +128,7 @@ class RedisPendingMfaStore extends PendingMfaStore {
127
128
  principalId: challenge.principalId,
128
129
  identityHint: challenge.identityHint,
129
130
  credentialFingerprint: challenge.credentialFingerprint,
131
+ ...(challenge.tenantId !== undefined ? { tenantId: challenge.tenantId } : {}),
130
132
  });
131
133
  await this.redis.set(`${REDIS_PREFIX}${tokenDigest(token)}`, value, 'PX', this.ttlMs);
132
134
  return token;
@@ -22,8 +22,11 @@
22
22
  * `attributes` carries non-secret structured detail (never a code/token/secret).
23
23
  */
24
24
  export interface AuthAuditEvent {
25
- /** The audit category. Always `'authentication'` for auth-surface events. */
26
- readonly type: 'authentication';
25
+ /**
26
+ * The audit category: `'authentication'` for sign-in, factor and account events;
27
+ * `'session_lifecycle'` for logout (R12.2). Both are existing core `AuditEventType` members.
28
+ */
29
+ readonly type: 'authentication' | 'session_lifecycle';
27
30
  /** When the event occurred (epoch ms). */
28
31
  readonly occurredAtMs: number;
29
32
  /** The acting principal's id (the account the event is about). */
@@ -1 +1 @@
1
- {"version":3,"file":"audit-sink.d.ts","sourceRoot":"","sources":["../../../src/auth/ports/audit-sink.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC7B,6EAA6E;IAC7E,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAC;IAChC,0CAA0C;IAC1C,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,kEAAkE;IAClE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,kEAAkE;IAClE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,0DAA0D;IAC1D,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,mEAAmE;IACnE,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/C;AAED;;;;GAIG;AACH,MAAM,WAAW,SAAS;IACxB,iFAAiF;IACjF,MAAM,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9C"}
1
+ {"version":3,"file":"audit-sink.d.ts","sourceRoot":"","sources":["../../../src/auth/ports/audit-sink.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC7B;;;OAGG;IACH,QAAQ,CAAC,IAAI,EAAE,gBAAgB,GAAG,mBAAmB,CAAC;IACtD,0CAA0C;IAC1C,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,kEAAkE;IAClE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,kEAAkE;IAClE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,0DAA0D;IAC1D,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,mEAAmE;IACnE,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/C;AAED;;;;GAIG;AACH,MAAM,WAAW,SAAS;IACxB,iFAAiF;IACjF,MAAM,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9C"}
@@ -37,6 +37,13 @@ export interface MfaDeviceStore {
37
37
  rename(principalId: string, deviceId: string, name: string): Promise<MfaDevice | null>;
38
38
  /** Delete a device. False if not found / not owned. */
39
39
  remove(principalId: string, deviceId: string): Promise<boolean>;
40
+ /**
41
+ * OPTIONAL (auth capabilities R2.7): delete a device unless it is the principal's LAST confirmed
42
+ * one, atomically per principal (the default store locks the principal's confirmed rows, re-counts
43
+ * and deletes in one transaction). `last_device` ⇒ nothing deleted. Used while a mandatory-2FA
44
+ * policy applies, so two concurrent removals cannot leave zero devices.
45
+ */
46
+ removeKeepingOne?(principalId: string, deviceId: string): Promise<'removed' | 'not_found' | 'last_device'>;
40
47
  /** Count of CONFIRMED devices (drives "first device" / "mfa enabled"). */
41
48
  countConfirmed(principalId: string): Promise<number>;
42
49
  /** Best-effort: stamp last-used for the device whose secret verified (match by unique secret). */
@@ -1 +1 @@
1
- {"version":3,"file":"mfa-device-store.d.ts","sourceRoot":"","sources":["../../../src/auth/ports/mfa-device-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,iFAAiF;AACjF,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;CAC7B;AAED,MAAM,WAAW,cAAc;IAC7B,6GAA6G;IAC7G,OAAO,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC;IACnD,wFAAwF;IACxF,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAC7D;;;;;OAKG;IACH,SAAS,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACzE,4GAA4G;IAC5G,KAAK,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC1E,2FAA2F;IAC3F,OAAO,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACjE,qFAAqF;IACrF,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC;IACvF,uDAAuD;IACvD,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAChE,0EAA0E;IAC1E,cAAc,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACrD,kGAAkG;IAClG,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1C"}
1
+ {"version":3,"file":"mfa-device-store.d.ts","sourceRoot":"","sources":["../../../src/auth/ports/mfa-device-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,iFAAiF;AACjF,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;CAC7B;AAED,MAAM,WAAW,cAAc;IAC7B,6GAA6G;IAC7G,OAAO,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC;IACnD,wFAAwF;IACxF,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAC7D;;;;;OAKG;IACH,SAAS,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACzE,4GAA4G;IAC5G,KAAK,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC1E,2FAA2F;IAC3F,OAAO,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACjE,qFAAqF;IACrF,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC;IACvF,uDAAuD;IACvD,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAChE;;;;;OAKG;IACH,gBAAgB,CAAC,CACf,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,SAAS,GAAG,WAAW,GAAG,aAAa,CAAC,CAAC;IACpD,0EAA0E;IAC1E,cAAc,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACrD,kGAAkG;IAClG,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1C"}