@astram/guard 0.2.0 → 0.4.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 (120) hide show
  1. package/dist/auth/account.controller.d.ts +245 -0
  2. package/dist/auth/account.controller.d.ts.map +1 -0
  3. package/dist/auth/account.controller.js +673 -0
  4. package/dist/auth/auth.controller.d.ts +219 -5
  5. package/dist/auth/auth.controller.d.ts.map +1 -1
  6. package/dist/auth/auth.controller.js +790 -12
  7. package/dist/auth/auth.module.d.ts +101 -3
  8. package/dist/auth/auth.module.d.ts.map +1 -1
  9. package/dist/auth/auth.module.js +300 -8
  10. package/dist/auth/dto.d.ts +16 -6
  11. package/dist/auth/dto.d.ts.map +1 -1
  12. package/dist/auth/ports/audit-sink.d.ts +47 -0
  13. package/dist/auth/ports/audit-sink.d.ts.map +1 -0
  14. package/dist/auth/ports/audit-sink.js +20 -0
  15. package/dist/auth/ports/credential-writer.d.ts +23 -0
  16. package/dist/auth/ports/credential-writer.d.ts.map +1 -0
  17. package/dist/auth/ports/credential-writer.js +2 -0
  18. package/dist/auth/ports/email-sender.d.ts +14 -0
  19. package/dist/auth/ports/email-sender.d.ts.map +1 -0
  20. package/dist/auth/ports/email-sender.js +2 -0
  21. package/dist/auth/ports/mfa-device-store.d.ts +45 -0
  22. package/dist/auth/ports/mfa-device-store.d.ts.map +1 -0
  23. package/dist/auth/ports/mfa-device-store.js +13 -0
  24. package/dist/auth/ports/profile-writer.d.ts +12 -0
  25. package/dist/auth/ports/profile-writer.d.ts.map +1 -0
  26. package/dist/auth/ports/profile-writer.js +2 -0
  27. package/dist/auth/ports/recovery-code-store.d.ts +10 -0
  28. package/dist/auth/ports/recovery-code-store.d.ts.map +1 -0
  29. package/dist/auth/ports/recovery-code-store.js +2 -0
  30. package/dist/auth/ports/reset-token-store.d.ts +12 -0
  31. package/dist/auth/ports/reset-token-store.d.ts.map +1 -0
  32. package/dist/auth/ports/reset-token-store.js +2 -0
  33. package/dist/auth/ports/session-facts-projector.d.ts +44 -0
  34. package/dist/auth/ports/session-facts-projector.d.ts.map +1 -0
  35. package/dist/auth/ports/session-facts-projector.js +30 -0
  36. package/dist/auth/ports/session-sweeper.d.ts +12 -0
  37. package/dist/auth/ports/session-sweeper.d.ts.map +1 -0
  38. package/dist/auth/ports/session-sweeper.js +2 -0
  39. package/dist/auth/profile-resolver.d.ts +73 -0
  40. package/dist/auth/profile-resolver.d.ts.map +1 -0
  41. package/dist/auth/profile-resolver.js +28 -0
  42. package/dist/auth/rate-limit/attempt-limiter.d.ts +91 -0
  43. package/dist/auth/rate-limit/attempt-limiter.d.ts.map +1 -0
  44. package/dist/auth/rate-limit/attempt-limiter.js +133 -0
  45. package/dist/auth/tokens.d.ts +174 -0
  46. package/dist/auth/tokens.d.ts.map +1 -1
  47. package/dist/auth/tokens.js +190 -1
  48. package/dist/auth/totp-enrollment.d.ts +42 -0
  49. package/dist/auth/totp-enrollment.d.ts.map +1 -0
  50. package/dist/auth/totp-enrollment.js +96 -0
  51. package/dist/authentication/mariadb-credential-resolver.d.ts +38 -24
  52. package/dist/authentication/mariadb-credential-resolver.d.ts.map +1 -1
  53. package/dist/authentication/mariadb-credential-resolver.js +42 -28
  54. package/dist/authz/cross-tenant-grant-writer.d.ts +259 -0
  55. package/dist/authz/cross-tenant-grant-writer.d.ts.map +1 -0
  56. package/dist/authz/cross-tenant-grant-writer.js +397 -0
  57. package/dist/authz/manifest-operation-authority-adapter.d.ts +65 -0
  58. package/dist/authz/manifest-operation-authority-adapter.d.ts.map +1 -0
  59. package/dist/authz/manifest-operation-authority-adapter.js +108 -0
  60. package/dist/authz/mariadb-cross-tenant-grant-adapter.d.ts +90 -0
  61. package/dist/authz/mariadb-cross-tenant-grant-adapter.d.ts.map +1 -0
  62. package/dist/authz/mariadb-cross-tenant-grant-adapter.js +211 -0
  63. package/dist/authz/mariadb-record-rule-adapter.d.ts +60 -0
  64. package/dist/authz/mariadb-record-rule-adapter.d.ts.map +1 -0
  65. package/dist/authz/mariadb-record-rule-adapter.js +154 -0
  66. package/dist/authz/mariadb-record-share-adapter.d.ts +75 -0
  67. package/dist/authz/mariadb-record-share-adapter.d.ts.map +1 -0
  68. package/dist/authz/mariadb-record-share-adapter.js +106 -0
  69. package/dist/authz/record-share-cascade.d.ts +52 -0
  70. package/dist/authz/record-share-cascade.d.ts.map +1 -0
  71. package/dist/authz/record-share-cascade.js +49 -0
  72. package/dist/authz/record-share-writer.d.ts +175 -0
  73. package/dist/authz/record-share-writer.d.ts.map +1 -0
  74. package/dist/authz/record-share-writer.js +225 -0
  75. package/dist/authz/wire-record-authorization.d.ts +66 -0
  76. package/dist/authz/wire-record-authorization.d.ts.map +1 -0
  77. package/dist/authz/wire-record-authorization.js +34 -0
  78. package/dist/completeness/catalog-reconciler.d.ts +21 -0
  79. package/dist/completeness/catalog-reconciler.d.ts.map +1 -0
  80. package/dist/completeness/catalog-reconciler.js +74 -0
  81. package/dist/completeness/operation-inventory-scanner.d.ts +17 -0
  82. package/dist/completeness/operation-inventory-scanner.d.ts.map +1 -0
  83. package/dist/completeness/operation-inventory-scanner.js +79 -0
  84. package/dist/completeness/ports/catalog-store.d.ts +33 -0
  85. package/dist/completeness/ports/catalog-store.d.ts.map +1 -0
  86. package/dist/completeness/ports/catalog-store.js +2 -0
  87. package/dist/config/app-config.d.ts +18 -16
  88. package/dist/config/app-config.d.ts.map +1 -1
  89. package/dist/config/app-config.js +68 -35
  90. package/dist/enforcement/classification.decorator.d.ts +33 -14
  91. package/dist/enforcement/classification.decorator.d.ts.map +1 -1
  92. package/dist/enforcement/classification.decorator.js +34 -14
  93. package/dist/enforcement/global-enforcement.guard.d.ts +29 -19
  94. package/dist/enforcement/global-enforcement.guard.d.ts.map +1 -1
  95. package/dist/enforcement/global-enforcement.guard.js +108 -25
  96. package/dist/enforcement/reenroll-allowlist.d.ts +49 -0
  97. package/dist/enforcement/reenroll-allowlist.d.ts.map +1 -0
  98. package/dist/enforcement/reenroll-allowlist.js +77 -0
  99. package/dist/index.d.ts +35 -3
  100. package/dist/index.d.ts.map +1 -1
  101. package/dist/index.js +159 -7
  102. package/dist/module/shield-tokens.d.ts +15 -0
  103. package/dist/module/shield-tokens.d.ts.map +1 -0
  104. package/dist/module/shield-tokens.js +17 -0
  105. package/dist/module/shield.module.d.ts +154 -10
  106. package/dist/module/shield.module.d.ts.map +1 -1
  107. package/dist/module/shield.module.js +181 -24
  108. package/dist/request-auth/request-principal.d.ts +42 -0
  109. package/dist/request-auth/request-principal.d.ts.map +1 -0
  110. package/dist/request-auth/request-principal.js +87 -0
  111. package/dist/request-auth/session-facts-als.d.ts +65 -0
  112. package/dist/request-auth/session-facts-als.d.ts.map +1 -0
  113. package/dist/request-auth/session-facts-als.js +57 -0
  114. package/dist/request-auth/session-token-binder.d.ts +130 -0
  115. package/dist/request-auth/session-token-binder.d.ts.map +1 -0
  116. package/dist/request-auth/session-token-binder.js +277 -0
  117. package/dist/request-auth/tokens.d.ts +14 -0
  118. package/dist/request-auth/tokens.d.ts.map +1 -0
  119. package/dist/request-auth/tokens.js +16 -0
  120. package/package.json +2 -2
@@ -0,0 +1,133 @@
1
+ "use strict";
2
+ /**
3
+ * `AttemptLimiter` — a guard-owned, Redis-backed sliding-window attempt counter for
4
+ * anti-brute-force on the auth surface (spec §5). ONE class, used at TWO call sites
5
+ * with independent thresholds: `/auth/login` (H1 password-login lockout, default
6
+ * 10/15min, keyed `login:<identityHint>`) and the `/auth/mfa` recovery branch
7
+ * (default 5/15min, keyed `recovery:<principalId>`).
8
+ *
9
+ * ── CONTRACT ─────────────────────────────────────────────────────────────────
10
+ * check(key) → `true` if the caller is UNDER the threshold (allowed);
11
+ * `false` if AT/over it (locked out for the window).
12
+ * recordFailure(key) → increment the counter; on the FIRST failure in a window,
13
+ * set the window TTL (`PEXPIRE windowMs`) so the counter
14
+ * auto-expires (a sliding window that resets on inactivity).
15
+ * RETURNS the new count so a caller can detect the LOCKOUT
16
+ * TRANSITION (count === maxAttempts is the exact edge where
17
+ * the NEXT check() flips to `false`) and act ONCE on it — e.g.
18
+ * emit a single lockout audit event, never one per attempt
19
+ * (feature #140's audit-on-the-edge, anti-flood). On a Redis
20
+ * best-effort miss it returns `0` (an unknown count → the
21
+ * caller simply doesn't treat the miss as a transition).
22
+ * reset(key) → clear the counter (a SUCCESS resets — a legitimate user is
23
+ * never penalized for their earlier fat-fingers).
24
+ *
25
+ * ── FAIL-OPEN-WITH-LOG (spec §5, the deliberate posture) ──────────────────────
26
+ * A limiter differs from the credential path: a Redis outage must NOT lock out
27
+ * every legitimate login/recovery. So `check()` FAILS OPEN on a Redis error —
28
+ * it returns `true` (allowed) and LOGS the blip. This never grants access on its
29
+ * own (the password / recovery code still gates); it only transiently LOOSENS the
30
+ * brute-force ceiling. `recordFailure`/`reset` are best-effort and swallow (a
31
+ * count we couldn't write just means the window is a touch more lenient — never a
32
+ * thrown error into the request path).
33
+ *
34
+ * ── ANTI-ENUMERATION ── the limiter itself is silent; the caller maps a `false`
35
+ * from `check()` to the SAME generic 401 as a wrong password / wrong code, so a
36
+ * locked-out account is indistinguishable from a bad credential.
37
+ *
38
+ * ADR-0023: guard owns the infra adapter (ioredis); the class is a thin, injectable
39
+ * collaborator the controller consumes via a DI token (`SHIELD_LOGIN_LIMITER` /
40
+ * `SHIELD_RECOVERY_LIMITER`), each provided with the live Redis + its config.
41
+ */
42
+ Object.defineProperty(exports, "__esModule", { value: true });
43
+ exports.AttemptLimiter = void 0;
44
+ const common_1 = require("@nestjs/common");
45
+ class AttemptLimiter {
46
+ redis;
47
+ cfg;
48
+ /** One shared logger — the fail-open path warns so a Redis outage is observable. */
49
+ static logger = new common_1.Logger(AttemptLimiter.name);
50
+ constructor(redis, cfg) {
51
+ this.redis = redis;
52
+ this.cfg = cfg;
53
+ }
54
+ /**
55
+ * The failure ceiling for this limiter (read-only). A caller uses it to detect the
56
+ * lockout TRANSITION: {@link recordFailure} returns the new count, and the caller
57
+ * acts ONCE when that count first equals `maxAttempts` (the edge where the next
58
+ * {@link check} flips to `false`) — the audit-on-the-edge / anti-flood contract.
59
+ */
60
+ get maxAttempts() {
61
+ return this.cfg.maxAttempts;
62
+ }
63
+ /** Namespace every key so the two limiters + other Redis users never collide. */
64
+ k(key) {
65
+ return `shield:attempts:${key}`;
66
+ }
67
+ /**
68
+ * `true` iff the caller is UNDER the threshold (allowed). FAILS OPEN on a Redis
69
+ * error — returns `true` and logs — so a Redis blip never locks out every login
70
+ * (spec §5). The credential/recovery check still gates access; this only bounds
71
+ * brute-force.
72
+ */
73
+ async check(key) {
74
+ try {
75
+ const n = await this.redis.get(this.k(key));
76
+ return n === null || Number(n) < this.cfg.maxAttempts;
77
+ }
78
+ catch (err) {
79
+ // FAIL-OPEN: a limiter must not lock out legitimate users on a Redis outage.
80
+ // Allow, but LOG so the transient loosening of brute-force protection is seen.
81
+ AttemptLimiter.logger.warn(`attempt-limiter check failed open (allowing) for a key — Redis unavailable: ${describeError(err)}`);
82
+ return true;
83
+ }
84
+ }
85
+ /**
86
+ * Increment the failure counter; set the window TTL on the FIRST failure so the
87
+ * counter auto-expires after `windowMs` of inactivity. Best-effort — a write we
88
+ * can't land just makes the window slightly more lenient (never a thrown error).
89
+ *
90
+ * RETURNS the new failure count so the caller can detect the lockout TRANSITION
91
+ * (count === {@link maxAttempts} is the exact edge where the next {@link check}
92
+ * flips to `false`) and act on it exactly ONCE — feature #140's audit-on-the-edge
93
+ * (never one audit event per attempt past the threshold). On a Redis best-effort
94
+ * miss it returns `0`: an unknown count the caller must NOT treat as a transition
95
+ * (so a Redis blip never spuriously fires — or suppresses — the lockout audit).
96
+ */
97
+ async recordFailure(key) {
98
+ try {
99
+ const k = this.k(key);
100
+ const n = await this.redis.incr(k);
101
+ // First failure in a fresh window → arm the expiry (INCR alone is persistent).
102
+ if (n === 1)
103
+ await this.redis.pexpire(k, this.cfg.windowMs);
104
+ return n;
105
+ }
106
+ catch (err) {
107
+ // Best-effort: a missed increment loosens the ceiling transiently, never fails
108
+ // the request. Log so a persistent Redis fault is visible. Return 0 (unknown
109
+ // count) so the caller's transition check (`=== maxAttempts`) never trips off a
110
+ // miss — the audit fires only on a genuine, counted crossing.
111
+ AttemptLimiter.logger.warn(`attempt-limiter recordFailure best-effort miss — Redis unavailable: ${describeError(err)}`);
112
+ return 0;
113
+ }
114
+ }
115
+ /**
116
+ * Clear the counter — a SUCCESS resets the window (the user proved themselves; do
117
+ * not carry their earlier failures forward). Best-effort; a swallowed error just
118
+ * leaves the (soon-to-expire) counter in place.
119
+ */
120
+ async reset(key) {
121
+ try {
122
+ await this.redis.del(this.k(key));
123
+ }
124
+ catch {
125
+ // Ignore — the counter TTL will expire it anyway; never fail a success path.
126
+ }
127
+ }
128
+ }
129
+ exports.AttemptLimiter = AttemptLimiter;
130
+ /** A tiny, secret-free error description for the fail-open log (never logs a key). */
131
+ function describeError(err) {
132
+ return err instanceof Error ? err.message : String(err);
133
+ }
@@ -44,6 +44,15 @@ export declare const SHIELD_REDIS: unique symbol;
44
44
  * `createAuthenticator({ resolver })`.
45
45
  */
46
46
  export declare const SHIELD_CREDENTIAL_RESOLVER: unique symbol;
47
+ /**
48
+ * The `ProfileResolver` (guard port, see `../auth/profile-resolver.ts`) that turns
49
+ * an authenticated `Principal` → the current-user display profile for `GET /auth/me`.
50
+ * OPTIONAL: supplied by the consumer via `ShieldModule.forRoot({ profileResolver })`
51
+ * and threaded through `AuthModule.forRoot`. When ABSENT the `/auth/me` route returns
52
+ * an honest "profile unavailable" (the framework is org-schema-agnostic — see the
53
+ * port header). Consumed by {@link AuthController}'s `me()` handler.
54
+ */
55
+ export declare const SHIELD_PROFILE_RESOLVER: unique symbol;
47
56
  /**
48
57
  * The composed `Authenticator` (core engine) — `createAuthenticator({ hasher,
49
58
  * resolver, identityBroker, factorVerifiers:[totp] })`. **Provided by Team B**
@@ -59,4 +68,169 @@ export declare const SHIELD_AUTHENTICATOR: unique symbol;
59
68
  * and `/auth/mfa` (establish after factor).
60
69
  */
61
70
  export declare const SHIELD_SESSION_AUTHORITY: unique symbol;
71
+ /**
72
+ * The `CredentialWriter` (guard port, `../auth/ports/credential-writer.ts`) — the
73
+ * WRITE side of the credential store (set password hash, read the stored hash by
74
+ * principal id). Consumed by change-password + reset-password. Absent (`null`) →
75
+ * those routes return 501. (TOTP secrets no longer live here — the multi-device
76
+ * MFA model stores them in {@link SHIELD_MFA_DEVICE_STORE}.)
77
+ */
78
+ export declare const SHIELD_CREDENTIAL_WRITER: unique symbol;
79
+ /**
80
+ * The `ResetTokenStore` (guard port, `../auth/ports/reset-token-store.ts`) — the
81
+ * single-use, TTL'd password-reset token store (framework hands it a SHA-256 hash,
82
+ * never the raw token). Consumed by forgot-password + reset-password.
83
+ */
84
+ export declare const SHIELD_RESET_TOKEN_STORE: unique symbol;
85
+ /**
86
+ * The `EmailSender` (guard port, `../auth/ports/email-sender.ts`) — the delivery
87
+ * seam (SendGrid/SMTP/test capturer). The framework builds the message; the
88
+ * consumer delivers it. Consumed by forgot-password.
89
+ */
90
+ export declare const SHIELD_EMAIL_SENDER: unique symbol;
91
+ /**
92
+ * The `RecoveryCodeStore` (guard port, `../auth/ports/recovery-code-store.ts`) —
93
+ * persists Argon2-hashed one-time MFA recovery codes. Consumed by MFA enrollment.
94
+ */
95
+ export declare const SHIELD_RECOVERY_CODE_STORE: unique symbol;
96
+ /**
97
+ * The `ProfileWriter` (guard port, `../auth/ports/profile-writer.ts`) — the
98
+ * self-service profile-edit seam (name/email). Consumed by `PATCH /account/profile`.
99
+ */
100
+ export declare const SHIELD_PROFILE_WRITER: unique symbol;
101
+ /**
102
+ * The `AuditSink` (guard port, `../auth/ports/audit-sink.ts`) — the auth-surface audit
103
+ * seam (spec §6.2). `record(event)` persists a framework-built {@link AuthAuditEvent}
104
+ * (the FIRST auth audit event: the recovery-code login). OPTIONAL, consumer-plugged
105
+ * exactly like {@link SHIELD_PROFILE_RESOLVER}: supplied → bound app-wide; omitted →
106
+ * bound `null`, so the `@Optional()` injection in {@link AuthController} resolves to
107
+ * "no seam" and the best-effort audit call is a no-op (the login still works,
108
+ * un-audited). Consumed by the `/auth/mfa` recovery branch (fire-and-forget).
109
+ */
110
+ export declare const SHIELD_AUDIT_SINK: unique symbol;
111
+ /**
112
+ * The `MfaDeviceStore` (guard port, `../auth/ports/mfa-device-store.ts`) — persists
113
+ * a principal's N TOTP authenticator DEVICES (multi-device MFA). Consumed
114
+ * by the `/account/mfa/devices/*` routes (list/begin/confirm/rename/remove) + the
115
+ * consumer's login resolver (which bundles {@link MfaDeviceStore.listConfirmedSecrets}
116
+ * for the OR-match). Absent (`null`) → the device routes return 501.
117
+ */
118
+ export declare const SHIELD_MFA_DEVICE_STORE: unique symbol;
119
+ /**
120
+ * The `SessionSweeper` (guard port, `../auth/ports/session-sweeper.ts`) — revokes a
121
+ * principal's sessions after a credential change (all, or all-but-current). The core
122
+ * has no revoke-all-for-principal, so the consumer implements it. Consumed by
123
+ * change-password (others) + reset-password (all).
124
+ */
125
+ export declare const SHIELD_SESSION_SWEEPER: unique symbol;
126
+ /**
127
+ * The reset-link base URL (a plain `string` value, NOT a port). The framework
128
+ * composes the emailed reset link as `${base}?token=<rawToken>` so the
129
+ * {@link SHIELD_EMAIL_SENDER} seam stays a dumb delivery channel (no URL/token
130
+ * logic). Bound app-wide by `AuthModule` from the environment
131
+ * (`RESET_LINK_BASE_URL`, default `http://localhost:5173/reset-password`). Consumed
132
+ * by `POST /auth/forgot-password`.
133
+ */
134
+ export declare const SHIELD_RESET_LINK_BASE_URL: unique symbol;
135
+ /**
136
+ * The core single-secret `FactorVerifier` for TOTP (`createTotpFactorVerifier({
137
+ * hmacSha1 })`), PROVIDED by `AuthModule` (which loads the core factory lazily — the
138
+ * CJS↔ESM invariant, same as {@link SHIELD_AUTHENTICATOR}). It is the SAME base
139
+ * verifier the login default uses, provided pre-built app-wide so the multi-device
140
+ * MFA routes (Phase 3) can check a presented code against a SPECIFIC device's secret
141
+ * without importing `@astram/core` at the controller's top level (that becomes a
142
+ * runtime `require` on the ESM-only core in the built CJS dist). Distinct from the
143
+ * login OR-verifier the consumer plugs through {@link SHIELD_TOTP_VERIFIER_OVERRIDE}
144
+ * (Phase 2): this stays single-secret, for per-device checks.
145
+ */
146
+ export declare const SHIELD_TOTP_VERIFIER: unique symbol;
147
+ /**
148
+ * OPTIONAL consumer override for the LOGIN `totp` {@link FactorVerifier} — the
149
+ * verifier the composed `Authenticator` uses to satisfy a `RequiredFactor` of
150
+ * factor `'totp'`. Threaded through `ShieldModule.forRoot({ totpVerifier })` →
151
+ * `AuthModule.forRoot`, `@Optional()` (bound `null` when absent). When supplied it
152
+ * REPLACES the framework's default `createTotpFactorVerifier({ hmacSha1 })` in the
153
+ * authenticator's `factorVerifiers` — the seam multi-device MFA needs so a
154
+ * consumer can plug an OR-matching verifier (one required-factor whose `secret` is a
155
+ * bundle of the user's N device secrets; the verifier matches ANY). Default absent →
156
+ * behavior EXACTLY unchanged (single-secret RFC-6238). Distinct from
157
+ * {@link SHIELD_TOTP_VERIFIER} (the pre-built single-secret verifier the account MFA
158
+ * routes use for per-device checks — that one is never overridden).
159
+ */
160
+ export declare const SHIELD_TOTP_VERIFIER_OVERRIDE: unique symbol;
161
+ /**
162
+ * OPTIONAL root allowlist — the server-side, trusted set of canonical
163
+ * principal ids that ARE roots on the framework's Internal identity
164
+ * source. Threaded through `ShieldModule.forRoot({ rootIds })` →
165
+ * `AuthModule.forRoot` and passed to `createIdentityBroker({ internalRootIds })`,
166
+ * `@Optional()` (bound `[]` when absent → nobody is a root, default-deny).
167
+ *
168
+ * ROOT SAFETY (core `internal-adapter.ts`): the `isRoot` flag is set
169
+ * `true` iff the authenticated principal's `id` is in THIS allowlist — it is NEVER read
170
+ * from a caller-supplied claim. So this is the ONLY way a login mints a root, and
171
+ * an attacker who can influence claim VALUES cannot flip it (they'd have to be in this
172
+ * server-side list, which they can't write). Default absent → `[]` → default-deny,
173
+ * exactly the historical behavior (`createIdentityBroker()` with an empty allowlist).
174
+ */
175
+ export declare const SHIELD_ROOT_IDS: unique symbol;
176
+ /**
177
+ * OPTIONAL {@link import('./ports/session-facts-projector.js').SessionFactsProjector}
178
+ * — the ADR-0011 eager-projection seam. Threaded through
179
+ * `ShieldModule.forRoot({ sessionFactsProjector })` → `AuthModule.forRoot`, `@Optional()`
180
+ * (bound `null` when absent). When PRESENT, the auth controller calls `project(...)` at
181
+ * login + refresh and caches the returned `CoarseGrants` on the session binding, so gate
182
+ * 5 serves the principal's coarse grants from the request's `cachedFacts` instead of a DB
183
+ * reload every request. ABSENT → the controller omits `cachedFacts` → today's exact
184
+ * behavior (DB reload every request). Purely additive + opt-in. Only consulted under
185
+ * `auth: true`.
186
+ */
187
+ export declare const SHIELD_SESSION_FACTS_PROJECTOR: unique symbol;
188
+ /**
189
+ * The password-login {@link AttemptLimiter} (spec §5 / IdP-hardening H1) — keyed
190
+ * `login:<identityHint>`, default 10 failures / 15-min window. Consumed at the TOP
191
+ * of `POST /auth/login` (`check` → generic 401 when locked) and on the outcome
192
+ * (`recordFailure` on a rejected wrong-password; `reset` on `authenticated` OR
193
+ * `mfa_required` — a correct password that merely needs MFA is NOT a failure).
194
+ */
195
+ export declare const SHIELD_LOGIN_LIMITER: unique symbol;
196
+ /**
197
+ * The recovery-code {@link AttemptLimiter} (spec §5) — keyed `recovery:<principalId>`,
198
+ * default 5 failures / 15-min window (recovery codes are higher-value than a rotating
199
+ * 30-s TOTP). Consumed in the `/auth/mfa` recovery branch: `check` BEFORE `consume`
200
+ * (locked → generic 401 without consulting the store), `recordFailure` on a wrong
201
+ * code, `reset` on success.
202
+ */
203
+ export declare const SHIELD_RECOVERY_LIMITER: unique symbol;
204
+ /**
205
+ * The forgot-password IP {@link AttemptLimiter} (feature #141) — keyed
206
+ * `forgot:ip:<clientIp>`, default 5 per 15-min window. Consumed at the TOP of
207
+ * `POST /auth/forgot-password`. UNLIKE login, forgot-password is ALWAYS 204 (a 429
208
+ * would itself be an enumeration/probing signal), so this is a request-rate cap: the
209
+ * handler counts EVERY forgot request against the IP budget and, once the budget is
210
+ * exceeded, SILENTLY skips the email-send work while STILL returning 204 (the existing
211
+ * always-204 anti-enumeration posture is preserved). Fail-open on a Redis blip. The
212
+ * client IP is read from the TRUSTED `req.ip` (the consumer sets `trust proxy` so
213
+ * X-Forwarded-For can't be spoofed).
214
+ */
215
+ export declare const SHIELD_FORGOT_LIMITER: unique symbol;
216
+ /**
217
+ * The forgot-password PER-HINT {@link AttemptLimiter} (feature #141) — keyed
218
+ * `forgot:<identityHint>`, default 3 per 60-min window (generous). Caps how often ANY
219
+ * caller (across IPs) can trigger a reset email for ONE account, so a single victim
220
+ * can't be email-bombed from a rotating IP pool. Same request-rate-cap + silent-skip-
221
+ * still-204 semantics as {@link SHIELD_FORGOT_LIMITER}; consumed alongside it in
222
+ * `POST /auth/forgot-password`.
223
+ */
224
+ export declare const SHIELD_FORGOT_HINT_LIMITER: unique symbol;
225
+ /**
226
+ * The reset-password IP {@link AttemptLimiter} (feature #141) — keyed
227
+ * `reset:ip:<clientIp>`, default 10 per 15-min window. Consumed at the TOP of
228
+ * `POST /auth/reset-password` (`check` BEFORE token work). Reset is token-gated (a
229
+ * 256-bit single-use token), so this is DoS/abuse throttling, NOT anti-brute-force —
230
+ * and it MAY return a GENERIC 429 on lockout (a token-gated endpoint is not
231
+ * enumeration-sensitive). `recordFailure` on a bad/invalid token (the abuse signal);
232
+ * `reset` on a successful reset (204). Fail-open on a Redis blip. IP is the trusted
233
+ * `req.ip`.
234
+ */
235
+ export declare const SHIELD_RESET_LIMITER: unique symbol;
62
236
  //# sourceMappingURL=tokens.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../../src/auth/tokens.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,sBAAsB,EAAE,MAAM,+CAA+C,CAAC;AAEvF;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,eAA2B,CAAC;AAEvD;;;;GAIG;AACH,eAAO,MAAM,YAAY,eAAyB,CAAC;AAEnD;;;;;;GAMG;AACH,eAAO,MAAM,0BAA0B,eAAuC,CAAC;AAE/E;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,eAAiC,CAAC;AAEnE;;;;;;GAMG;AACH,eAAO,MAAM,wBAAwB,eAAqC,CAAC"}
1
+ {"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../../src/auth/tokens.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,sBAAsB,EAAE,MAAM,+CAA+C,CAAC;AAEvF;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,eAA2B,CAAC;AAEvD;;;;GAIG;AACH,eAAO,MAAM,YAAY,eAAyB,CAAC;AAEnD;;;;;;GAMG;AACH,eAAO,MAAM,0BAA0B,eAAuC,CAAC;AAE/E;;;;;;;GAOG;AACH,eAAO,MAAM,uBAAuB,eAAoC,CAAC;AAEzE;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,eAAiC,CAAC;AAEnE;;;;;;GAMG;AACH,eAAO,MAAM,wBAAwB,eAAqC,CAAC;AAW3E;;;;;;GAMG;AACH,eAAO,MAAM,wBAAwB,eAAqC,CAAC;AAE3E;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,eAAqC,CAAC;AAE3E;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,eAAgC,CAAC;AAEjE;;;GAGG;AACH,eAAO,MAAM,0BAA0B,eAAuC,CAAC;AAE/E;;;GAGG;AACH,eAAO,MAAM,qBAAqB,eAAkC,CAAC;AAErE;;;;;;;;GAQG;AACH,eAAO,MAAM,iBAAiB,eAA8B,CAAC;AAE7D;;;;;;GAMG;AACH,eAAO,MAAM,uBAAuB,eAAoC,CAAC;AAEzE;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,eAAmC,CAAC;AAEvE;;;;;;;GAOG;AACH,eAAO,MAAM,0BAA0B,eAAuC,CAAC;AAE/E;;;;;;;;;;GAUG;AACH,eAAO,MAAM,oBAAoB,eAAiC,CAAC;AAEnE;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,6BAA6B,eAA0C,CAAC;AAErF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,eAAe,eAA4B,CAAC;AAEzD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,8BAA8B,eAA2C,CAAC;AAUvF;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,eAAiC,CAAC;AAEnE;;;;;;GAMG;AACH,eAAO,MAAM,uBAAuB,eAAoC,CAAC;AAEzE;;;;;;;;;;GAUG;AACH,eAAO,MAAM,qBAAqB,eAAkC,CAAC;AAErE;;;;;;;GAOG;AACH,eAAO,MAAM,0BAA0B,eAAuC,CAAC;AAE/E;;;;;;;;;GASG;AACH,eAAO,MAAM,oBAAoB,eAAiC,CAAC"}
@@ -23,7 +23,7 @@
23
23
  * ADR-0003: no framework/DB/crypto imports here — just Symbols (+ one re-export).
24
24
  */
25
25
  Object.defineProperty(exports, "__esModule", { value: true });
26
- exports.SHIELD_SESSION_AUTHORITY = exports.SHIELD_AUTHENTICATOR = exports.SHIELD_CREDENTIAL_RESOLVER = exports.SHIELD_REDIS = exports.SHIELD_DB_POOL = exports.SHIELD_PASSWORD_HASHER = void 0;
26
+ exports.SHIELD_RESET_LIMITER = exports.SHIELD_FORGOT_HINT_LIMITER = exports.SHIELD_FORGOT_LIMITER = exports.SHIELD_RECOVERY_LIMITER = exports.SHIELD_LOGIN_LIMITER = exports.SHIELD_SESSION_FACTS_PROJECTOR = exports.SHIELD_ROOT_IDS = exports.SHIELD_TOTP_VERIFIER_OVERRIDE = exports.SHIELD_TOTP_VERIFIER = exports.SHIELD_RESET_LINK_BASE_URL = exports.SHIELD_SESSION_SWEEPER = exports.SHIELD_MFA_DEVICE_STORE = exports.SHIELD_AUDIT_SINK = exports.SHIELD_PROFILE_WRITER = exports.SHIELD_RECOVERY_CODE_STORE = exports.SHIELD_EMAIL_SENDER = exports.SHIELD_RESET_TOKEN_STORE = exports.SHIELD_CREDENTIAL_WRITER = exports.SHIELD_SESSION_AUTHORITY = exports.SHIELD_AUTHENTICATOR = exports.SHIELD_PROFILE_RESOLVER = exports.SHIELD_CREDENTIAL_RESOLVER = exports.SHIELD_REDIS = exports.SHIELD_DB_POOL = exports.SHIELD_PASSWORD_HASHER = void 0;
27
27
  var argon2id_password_hasher_js_1 = require("../authentication/argon2id-password-hasher.js");
28
28
  Object.defineProperty(exports, "SHIELD_PASSWORD_HASHER", { enumerable: true, get: function () { return argon2id_password_hasher_js_1.SHIELD_PASSWORD_HASHER; } });
29
29
  /**
@@ -48,6 +48,15 @@ exports.SHIELD_REDIS = Symbol('SHIELD_REDIS');
48
48
  * `createAuthenticator({ resolver })`.
49
49
  */
50
50
  exports.SHIELD_CREDENTIAL_RESOLVER = Symbol('SHIELD_CREDENTIAL_RESOLVER');
51
+ /**
52
+ * The `ProfileResolver` (guard port, see `../auth/profile-resolver.ts`) that turns
53
+ * an authenticated `Principal` → the current-user display profile for `GET /auth/me`.
54
+ * OPTIONAL: supplied by the consumer via `ShieldModule.forRoot({ profileResolver })`
55
+ * and threaded through `AuthModule.forRoot`. When ABSENT the `/auth/me` route returns
56
+ * an honest "profile unavailable" (the framework is org-schema-agnostic — see the
57
+ * port header). Consumed by {@link AuthController}'s `me()` handler.
58
+ */
59
+ exports.SHIELD_PROFILE_RESOLVER = Symbol('SHIELD_PROFILE_RESOLVER');
51
60
  /**
52
61
  * The composed `Authenticator` (core engine) — `createAuthenticator({ hasher,
53
62
  * resolver, identityBroker, factorVerifiers:[totp] })`. **Provided by Team B**
@@ -63,3 +72,183 @@ exports.SHIELD_AUTHENTICATOR = Symbol('SHIELD_AUTHENTICATOR');
63
72
  * and `/auth/mfa` (establish after factor).
64
73
  */
65
74
  exports.SHIELD_SESSION_AUTHORITY = Symbol('SHIELD_SESSION_AUTHORITY');
75
+ // ---------------------------------------------------------------------------
76
+ // Account & credential surface seams (forgot/reset, change-password, MFA
77
+ // enrollment, profile edit). All OPTIONAL, consumer-plugged through
78
+ // `ShieldModule.forRoot({...})` → `AuthModule.forRoot` (exactly like
79
+ // {@link SHIELD_PROFILE_RESOLVER}). Each binds to the supplied port impl, or to
80
+ // `null` when absent so the `@Optional()` injection in the routes resolves to "no
81
+ // seam" → the route returns 501. See `../auth/ports/*.ts` for each contract.
82
+ // ---------------------------------------------------------------------------
83
+ /**
84
+ * The `CredentialWriter` (guard port, `../auth/ports/credential-writer.ts`) — the
85
+ * WRITE side of the credential store (set password hash, read the stored hash by
86
+ * principal id). Consumed by change-password + reset-password. Absent (`null`) →
87
+ * those routes return 501. (TOTP secrets no longer live here — the multi-device
88
+ * MFA model stores them in {@link SHIELD_MFA_DEVICE_STORE}.)
89
+ */
90
+ exports.SHIELD_CREDENTIAL_WRITER = Symbol('SHIELD_CREDENTIAL_WRITER');
91
+ /**
92
+ * The `ResetTokenStore` (guard port, `../auth/ports/reset-token-store.ts`) — the
93
+ * single-use, TTL'd password-reset token store (framework hands it a SHA-256 hash,
94
+ * never the raw token). Consumed by forgot-password + reset-password.
95
+ */
96
+ exports.SHIELD_RESET_TOKEN_STORE = Symbol('SHIELD_RESET_TOKEN_STORE');
97
+ /**
98
+ * The `EmailSender` (guard port, `../auth/ports/email-sender.ts`) — the delivery
99
+ * seam (SendGrid/SMTP/test capturer). The framework builds the message; the
100
+ * consumer delivers it. Consumed by forgot-password.
101
+ */
102
+ exports.SHIELD_EMAIL_SENDER = Symbol('SHIELD_EMAIL_SENDER');
103
+ /**
104
+ * The `RecoveryCodeStore` (guard port, `../auth/ports/recovery-code-store.ts`) —
105
+ * persists Argon2-hashed one-time MFA recovery codes. Consumed by MFA enrollment.
106
+ */
107
+ exports.SHIELD_RECOVERY_CODE_STORE = Symbol('SHIELD_RECOVERY_CODE_STORE');
108
+ /**
109
+ * The `ProfileWriter` (guard port, `../auth/ports/profile-writer.ts`) — the
110
+ * self-service profile-edit seam (name/email). Consumed by `PATCH /account/profile`.
111
+ */
112
+ exports.SHIELD_PROFILE_WRITER = Symbol('SHIELD_PROFILE_WRITER');
113
+ /**
114
+ * The `AuditSink` (guard port, `../auth/ports/audit-sink.ts`) — the auth-surface audit
115
+ * seam (spec §6.2). `record(event)` persists a framework-built {@link AuthAuditEvent}
116
+ * (the FIRST auth audit event: the recovery-code login). OPTIONAL, consumer-plugged
117
+ * exactly like {@link SHIELD_PROFILE_RESOLVER}: supplied → bound app-wide; omitted →
118
+ * bound `null`, so the `@Optional()` injection in {@link AuthController} resolves to
119
+ * "no seam" and the best-effort audit call is a no-op (the login still works,
120
+ * un-audited). Consumed by the `/auth/mfa` recovery branch (fire-and-forget).
121
+ */
122
+ exports.SHIELD_AUDIT_SINK = Symbol('SHIELD_AUDIT_SINK');
123
+ /**
124
+ * The `MfaDeviceStore` (guard port, `../auth/ports/mfa-device-store.ts`) — persists
125
+ * a principal's N TOTP authenticator DEVICES (multi-device MFA). Consumed
126
+ * by the `/account/mfa/devices/*` routes (list/begin/confirm/rename/remove) + the
127
+ * consumer's login resolver (which bundles {@link MfaDeviceStore.listConfirmedSecrets}
128
+ * for the OR-match). Absent (`null`) → the device routes return 501.
129
+ */
130
+ exports.SHIELD_MFA_DEVICE_STORE = Symbol('SHIELD_MFA_DEVICE_STORE');
131
+ /**
132
+ * The `SessionSweeper` (guard port, `../auth/ports/session-sweeper.ts`) — revokes a
133
+ * principal's sessions after a credential change (all, or all-but-current). The core
134
+ * has no revoke-all-for-principal, so the consumer implements it. Consumed by
135
+ * change-password (others) + reset-password (all).
136
+ */
137
+ exports.SHIELD_SESSION_SWEEPER = Symbol('SHIELD_SESSION_SWEEPER');
138
+ /**
139
+ * The reset-link base URL (a plain `string` value, NOT a port). The framework
140
+ * composes the emailed reset link as `${base}?token=<rawToken>` so the
141
+ * {@link SHIELD_EMAIL_SENDER} seam stays a dumb delivery channel (no URL/token
142
+ * logic). Bound app-wide by `AuthModule` from the environment
143
+ * (`RESET_LINK_BASE_URL`, default `http://localhost:5173/reset-password`). Consumed
144
+ * by `POST /auth/forgot-password`.
145
+ */
146
+ exports.SHIELD_RESET_LINK_BASE_URL = Symbol('SHIELD_RESET_LINK_BASE_URL');
147
+ /**
148
+ * The core single-secret `FactorVerifier` for TOTP (`createTotpFactorVerifier({
149
+ * hmacSha1 })`), PROVIDED by `AuthModule` (which loads the core factory lazily — the
150
+ * CJS↔ESM invariant, same as {@link SHIELD_AUTHENTICATOR}). It is the SAME base
151
+ * verifier the login default uses, provided pre-built app-wide so the multi-device
152
+ * MFA routes (Phase 3) can check a presented code against a SPECIFIC device's secret
153
+ * without importing `@astram/core` at the controller's top level (that becomes a
154
+ * runtime `require` on the ESM-only core in the built CJS dist). Distinct from the
155
+ * login OR-verifier the consumer plugs through {@link SHIELD_TOTP_VERIFIER_OVERRIDE}
156
+ * (Phase 2): this stays single-secret, for per-device checks.
157
+ */
158
+ exports.SHIELD_TOTP_VERIFIER = Symbol('SHIELD_TOTP_VERIFIER');
159
+ /**
160
+ * OPTIONAL consumer override for the LOGIN `totp` {@link FactorVerifier} — the
161
+ * verifier the composed `Authenticator` uses to satisfy a `RequiredFactor` of
162
+ * factor `'totp'`. Threaded through `ShieldModule.forRoot({ totpVerifier })` →
163
+ * `AuthModule.forRoot`, `@Optional()` (bound `null` when absent). When supplied it
164
+ * REPLACES the framework's default `createTotpFactorVerifier({ hmacSha1 })` in the
165
+ * authenticator's `factorVerifiers` — the seam multi-device MFA needs so a
166
+ * consumer can plug an OR-matching verifier (one required-factor whose `secret` is a
167
+ * bundle of the user's N device secrets; the verifier matches ANY). Default absent →
168
+ * behavior EXACTLY unchanged (single-secret RFC-6238). Distinct from
169
+ * {@link SHIELD_TOTP_VERIFIER} (the pre-built single-secret verifier the account MFA
170
+ * routes use for per-device checks — that one is never overridden).
171
+ */
172
+ exports.SHIELD_TOTP_VERIFIER_OVERRIDE = Symbol('SHIELD_TOTP_VERIFIER_OVERRIDE');
173
+ /**
174
+ * OPTIONAL root allowlist — the server-side, trusted set of canonical
175
+ * principal ids that ARE roots on the framework's Internal identity
176
+ * source. Threaded through `ShieldModule.forRoot({ rootIds })` →
177
+ * `AuthModule.forRoot` and passed to `createIdentityBroker({ internalRootIds })`,
178
+ * `@Optional()` (bound `[]` when absent → nobody is a root, default-deny).
179
+ *
180
+ * ROOT SAFETY (core `internal-adapter.ts`): the `isRoot` flag is set
181
+ * `true` iff the authenticated principal's `id` is in THIS allowlist — it is NEVER read
182
+ * from a caller-supplied claim. So this is the ONLY way a login mints a root, and
183
+ * an attacker who can influence claim VALUES cannot flip it (they'd have to be in this
184
+ * server-side list, which they can't write). Default absent → `[]` → default-deny,
185
+ * exactly the historical behavior (`createIdentityBroker()` with an empty allowlist).
186
+ */
187
+ exports.SHIELD_ROOT_IDS = Symbol('SHIELD_ROOT_IDS');
188
+ /**
189
+ * OPTIONAL {@link import('./ports/session-facts-projector.js').SessionFactsProjector}
190
+ * — the ADR-0011 eager-projection seam. Threaded through
191
+ * `ShieldModule.forRoot({ sessionFactsProjector })` → `AuthModule.forRoot`, `@Optional()`
192
+ * (bound `null` when absent). When PRESENT, the auth controller calls `project(...)` at
193
+ * login + refresh and caches the returned `CoarseGrants` on the session binding, so gate
194
+ * 5 serves the principal's coarse grants from the request's `cachedFacts` instead of a DB
195
+ * reload every request. ABSENT → the controller omits `cachedFacts` → today's exact
196
+ * behavior (DB reload every request). Purely additive + opt-in. Only consulted under
197
+ * `auth: true`.
198
+ */
199
+ exports.SHIELD_SESSION_FACTS_PROJECTOR = Symbol('SHIELD_SESSION_FACTS_PROJECTOR');
200
+ // ---------------------------------------------------------------------------
201
+ // Rate-limit seams (anti-brute-force, spec §5) — ONE `AttemptLimiter` class
202
+ // (Redis-backed), provided as TWO instances with independent thresholds under
203
+ // these tokens. Both are always provided under `auth: true` (they need the live
204
+ // {@link SHIELD_REDIS}); the controller injects each `@Optional()` so an absent
205
+ // limiter degrades to "allow" (never a lock-out from missing wiring).
206
+ // ---------------------------------------------------------------------------
207
+ /**
208
+ * The password-login {@link AttemptLimiter} (spec §5 / IdP-hardening H1) — keyed
209
+ * `login:<identityHint>`, default 10 failures / 15-min window. Consumed at the TOP
210
+ * of `POST /auth/login` (`check` → generic 401 when locked) and on the outcome
211
+ * (`recordFailure` on a rejected wrong-password; `reset` on `authenticated` OR
212
+ * `mfa_required` — a correct password that merely needs MFA is NOT a failure).
213
+ */
214
+ exports.SHIELD_LOGIN_LIMITER = Symbol('SHIELD_LOGIN_LIMITER');
215
+ /**
216
+ * The recovery-code {@link AttemptLimiter} (spec §5) — keyed `recovery:<principalId>`,
217
+ * default 5 failures / 15-min window (recovery codes are higher-value than a rotating
218
+ * 30-s TOTP). Consumed in the `/auth/mfa` recovery branch: `check` BEFORE `consume`
219
+ * (locked → generic 401 without consulting the store), `recordFailure` on a wrong
220
+ * code, `reset` on success.
221
+ */
222
+ exports.SHIELD_RECOVERY_LIMITER = Symbol('SHIELD_RECOVERY_LIMITER');
223
+ /**
224
+ * The forgot-password IP {@link AttemptLimiter} (feature #141) — keyed
225
+ * `forgot:ip:<clientIp>`, default 5 per 15-min window. Consumed at the TOP of
226
+ * `POST /auth/forgot-password`. UNLIKE login, forgot-password is ALWAYS 204 (a 429
227
+ * would itself be an enumeration/probing signal), so this is a request-rate cap: the
228
+ * handler counts EVERY forgot request against the IP budget and, once the budget is
229
+ * exceeded, SILENTLY skips the email-send work while STILL returning 204 (the existing
230
+ * always-204 anti-enumeration posture is preserved). Fail-open on a Redis blip. The
231
+ * client IP is read from the TRUSTED `req.ip` (the consumer sets `trust proxy` so
232
+ * X-Forwarded-For can't be spoofed).
233
+ */
234
+ exports.SHIELD_FORGOT_LIMITER = Symbol('SHIELD_FORGOT_LIMITER');
235
+ /**
236
+ * The forgot-password PER-HINT {@link AttemptLimiter} (feature #141) — keyed
237
+ * `forgot:<identityHint>`, default 3 per 60-min window (generous). Caps how often ANY
238
+ * caller (across IPs) can trigger a reset email for ONE account, so a single victim
239
+ * can't be email-bombed from a rotating IP pool. Same request-rate-cap + silent-skip-
240
+ * still-204 semantics as {@link SHIELD_FORGOT_LIMITER}; consumed alongside it in
241
+ * `POST /auth/forgot-password`.
242
+ */
243
+ exports.SHIELD_FORGOT_HINT_LIMITER = Symbol('SHIELD_FORGOT_HINT_LIMITER');
244
+ /**
245
+ * The reset-password IP {@link AttemptLimiter} (feature #141) — keyed
246
+ * `reset:ip:<clientIp>`, default 10 per 15-min window. Consumed at the TOP of
247
+ * `POST /auth/reset-password` (`check` BEFORE token work). Reset is token-gated (a
248
+ * 256-bit single-use token), so this is DoS/abuse throttling, NOT anti-brute-force —
249
+ * and it MAY return a GENERIC 429 on lockout (a token-gated endpoint is not
250
+ * enumeration-sensitive). `recordFailure` on a bad/invalid token (the abuse signal);
251
+ * `reset` on a successful reset (204). Fail-open on a Redis blip. IP is the trusted
252
+ * `req.ip`.
253
+ */
254
+ exports.SHIELD_RESET_LIMITER = Symbol('SHIELD_RESET_LIMITER');
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Phase 4 MFA-enrollment — the **pure TOTP-enrollment helpers**: generate a fresh
3
+ * base32 TOTP secret, compose the `otpauth://` provisioning URI an authenticator app
4
+ * scans, and mint one-time recovery codes.
5
+ *
6
+ * ── WHY THIS LIVES IN GUARD (not core) ──────────────────────────────────────
7
+ * Core owns the TOTP *verifier* (`createTotpFactorVerifier`, RFC 6238) but is
8
+ * ZERO-dep + node-type-free (ADR-0003), so it cannot mint secrets (needs
9
+ * `node:crypto.randomBytes`) nor build a URI. Enrollment is the *provisioning* side
10
+ * — it produces the secret the verifier later checks against. Guard owns node
11
+ * builtins, so the generation lives here. NO core change: the secret this emits is a
12
+ * plain base32 string, exactly what `createTotpFactorVerifier` decodes.
13
+ *
14
+ * ── SECURITY ─────────────────────────────────────────────────────────────────
15
+ * The secret + recovery codes are credential material: never logged, returned to the
16
+ * user exactly ONCE at enrollment, and (recovery codes) stored only Argon2-hashed.
17
+ * These functions are PURE (no I/O, no storage) — the caller decides what to persist.
18
+ *
19
+ * ADR-0003: guard owns `node:crypto`; this file has NO framework/HTTP/DB coupling.
20
+ */
21
+ /**
22
+ * Generate a fresh, cryptographically-random base32 TOTP secret (160 bits → 32
23
+ * chars). This is what the user's authenticator app is provisioned with and what the
24
+ * core verifier later checks presented codes against.
25
+ */
26
+ export declare function generateTotpSecret(): string;
27
+ /**
28
+ * Build the `otpauth://totp/...` provisioning URI an authenticator app scans (or the
29
+ * user enters manually). `label` identifies the account (shown in the app), `issuer`
30
+ * names the service. All three URI components are percent-encoded so a label/issuer
31
+ * with spaces or `:`/`?`/`&` cannot break the URI. Fixed to the defaults the core
32
+ * verifier uses (SHA-1, 6 digits, 30s period).
33
+ */
34
+ export declare function buildOtpauthUri(secret: string, label: string, issuer: string): string;
35
+ /**
36
+ * Mint `count` unique one-time recovery codes (each `xxxxx-xxxxx`, 10 lowercase hex
37
+ * chars grouped for readability). Returned RAW to the caller ONCE at enrollment; the
38
+ * store persists them Argon2-hashed. Uniqueness is enforced (a hex collision at this
39
+ * entropy is astronomically unlikely, but the loop guarantees `count` distinct codes).
40
+ */
41
+ export declare function generateRecoveryCodes(count?: number): string[];
42
+ //# sourceMappingURL=totp-enrollment.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"totp-enrollment.d.ts","sourceRoot":"","sources":["../../src/auth/totp-enrollment.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AA4CH;;;;GAIG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAOrF;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,GAAE,MAAoC,GAAG,MAAM,EAAE,CAO3F"}