@astram/guard 0.10.0 → 0.10.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.
@@ -1,9 +1,12 @@
1
1
  /**
2
- * `AttemptLimiter` — a guard-owned, Redis-backed sliding-window attempt counter for
3
- * anti-brute-force on the auth surface (spec §5). ONE class, used at TWO call sites
4
- * with independent thresholds: `/auth/login` (H1 password-login lockout, default
5
- * 10/15min, keyed `login:<identityHint>`) and the `/auth/mfa` recovery branch
6
- * (default 5/15min, keyed `recovery:<principalId>`).
2
+ * `AttemptLimiter` — a guard-owned, Redis-backed FIXED-WINDOW attempt counter for
3
+ * anti-brute-force on the auth surface (spec §5). The window starts at the first counted
4
+ * attempt (`PEXPIRE windowMs`) and is not extended by later ones; the counter resets when
5
+ * it expires or on a success. One class, one instance per DI token with its own threshold:
6
+ * `SHIELD_LOGIN_LIMITER` (`login:pid:<id>`, shared by `/auth/login` and change-password),
7
+ * the per-IP login limiter, `SHIELD_MFA_LIMITER` (`mfa:pid:<id>`, `/auth/mfa` TOTP and the
8
+ * code-gated account MFA routes), `SHIELD_RECOVERY_LIMITER` (`/auth/mfa` recovery codes),
9
+ * and the forgot/reset-password limiters.
7
10
  *
8
11
  * ── CONTRACT ─────────────────────────────────────────────────────────────────
9
12
  * consume(key) → Phase 9 #10, the gate for credential-verifying routes (login,
@@ -18,7 +21,7 @@
18
21
  * `false` if AT/over it (locked out for the window).
19
22
  * recordFailure(key) → increment the counter; on the FIRST failure in a window,
20
23
  * set the window TTL (`PEXPIRE windowMs`) so the counter
21
- * auto-expires (a sliding window that resets on inactivity).
24
+ * auto-expires (a fixed window from the first failure).
22
25
  * RETURNS the new count so a caller can detect the LOCKOUT
23
26
  * TRANSITION (count === maxAttempts is the exact edge where
24
27
  * the NEXT check() flips to `false`) and act ONCE on it — e.g.
@@ -41,9 +44,12 @@
41
44
  * count we couldn't write just means the window is a touch more lenient — never a
42
45
  * thrown error into the request path).
43
46
  *
44
- * ── ANTI-ENUMERATION ── the limiter itself is silent; the caller maps a `false`
45
- * from `check()` to the SAME generic 401 as a wrong password / wrong code, so a
46
- * locked-out account is indistinguishable from a bad credential.
47
+ * ── WHAT A LOCK ANSWERS ── the limiter itself is silent; the caller decides. On the
48
+ * unauthenticated `/auth/*` routes a lock is the SAME generic 401 as a wrong password /
49
+ * wrong code, so a locked-out account is indistinguishable from a bad credential. On the
50
+ * authenticated `/account/*` routes (change-password, MFA device remove, recovery-code
51
+ * regenerate) the caller already holds a session, so a lock answers 429 (Phase 8 #1,
52
+ * Phase 9 #3).
47
53
  *
48
54
  * ADR-0023: guard owns the infra adapter (ioredis); the class is a thin, injectable
49
55
  * collaborator the controller consumes via a DI token (`SHIELD_LOGIN_LIMITER` /
@@ -73,7 +79,7 @@ export interface AttemptVerdict {
73
79
  export declare class AttemptLimiterUnavailableError extends Error {
74
80
  constructor();
75
81
  }
76
- /** Per-limiter tuning: the failure ceiling and the sliding-window length (ms). */
82
+ /** Per-limiter tuning: the failure ceiling and the fixed-window length (ms). */
77
83
  export interface AttemptLimiterConfig {
78
84
  /** Number of failures within the window that trips the lockout (check → false). */
79
85
  readonly maxAttempts: number;
@@ -1 +1 @@
1
- {"version":3,"file":"attempt-limiter.d.ts","sourceRoot":"","sources":["../../../src/auth/rate-limit/attempt-limiter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkDG;AAGH,OAAO,KAAK,KAAK,MAAM,SAAS,CAAC;AAEjC;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,yJAI1B,CAAC;AAEV,0FAA0F;AAC1F,eAAO,MAAM,qBAAqB,4JAGzB,CAAC;AAEV,qDAAqD;AACrD,MAAM,WAAW,cAAc;IAC7B,6EAA6E;IAC7E,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,uEAAuE;IACvE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED;;;GAGG;AACH,qBAAa,8BAA+B,SAAQ,KAAK;;CAKxD;AAED,kFAAkF;AAClF,MAAM,WAAW,oBAAoB;IACnC,mFAAmF;IACnF,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,8EAA8E;IAC9E,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED,qBAAa,cAAc;IAKvB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,GAAG;IALtB,oFAAoF;IACpF,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAmC;gBAG9C,KAAK,EAAE,KAAK,EACZ,GAAG,EAAE,oBAAoB;IAG5C;;;;;OAKG;IACH,IAAI,WAAW,IAAI,MAAM,CAExB;IAED,iFAAiF;IACjF,OAAO,CAAC,CAAC;IAIT;;;;;OAKG;IACG,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAc1C;;;;;;;;;;;;;;;OAeG;IACG,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;IAmBnD;;;;;OAKG;IACG,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAUxC;;;;;;;;;;;OAWG;IACG,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAmBjD;;;;OAIG;IACG,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAOxC"}
1
+ {"version":3,"file":"attempt-limiter.d.ts","sourceRoot":"","sources":["../../../src/auth/rate-limit/attempt-limiter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwDG;AAGH,OAAO,KAAK,KAAK,MAAM,SAAS,CAAC;AAEjC;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,yJAI1B,CAAC;AAEV,0FAA0F;AAC1F,eAAO,MAAM,qBAAqB,4JAGzB,CAAC;AAEV,qDAAqD;AACrD,MAAM,WAAW,cAAc;IAC7B,6EAA6E;IAC7E,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,uEAAuE;IACvE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED;;;GAGG;AACH,qBAAa,8BAA+B,SAAQ,KAAK;;CAKxD;AAED,gFAAgF;AAChF,MAAM,WAAW,oBAAoB;IACnC,mFAAmF;IACnF,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,8EAA8E;IAC9E,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED,qBAAa,cAAc;IAKvB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,GAAG;IALtB,oFAAoF;IACpF,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAmC;gBAG9C,KAAK,EAAE,KAAK,EACZ,GAAG,EAAE,oBAAoB;IAG5C;;;;;OAKG;IACH,IAAI,WAAW,IAAI,MAAM,CAExB;IAED,iFAAiF;IACjF,OAAO,CAAC,CAAC;IAIT;;;;;OAKG;IACG,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAc1C;;;;;;;;;;;;;;;OAeG;IACG,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;IAmBnD;;;;;OAKG;IACG,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAUxC;;;;;;;;;;;OAWG;IACG,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAmBjD;;;;OAIG;IACG,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAOxC"}
@@ -1,10 +1,13 @@
1
1
  "use strict";
2
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>`).
3
+ * `AttemptLimiter` — a guard-owned, Redis-backed FIXED-WINDOW attempt counter for
4
+ * anti-brute-force on the auth surface (spec §5). The window starts at the first counted
5
+ * attempt (`PEXPIRE windowMs`) and is not extended by later ones; the counter resets when
6
+ * it expires or on a success. One class, one instance per DI token with its own threshold:
7
+ * `SHIELD_LOGIN_LIMITER` (`login:pid:<id>`, shared by `/auth/login` and change-password),
8
+ * the per-IP login limiter, `SHIELD_MFA_LIMITER` (`mfa:pid:<id>`, `/auth/mfa` TOTP and the
9
+ * code-gated account MFA routes), `SHIELD_RECOVERY_LIMITER` (`/auth/mfa` recovery codes),
10
+ * and the forgot/reset-password limiters.
8
11
  *
9
12
  * ── CONTRACT ─────────────────────────────────────────────────────────────────
10
13
  * consume(key) → Phase 9 #10, the gate for credential-verifying routes (login,
@@ -19,7 +22,7 @@
19
22
  * `false` if AT/over it (locked out for the window).
20
23
  * recordFailure(key) → increment the counter; on the FIRST failure in a window,
21
24
  * set the window TTL (`PEXPIRE windowMs`) so the counter
22
- * auto-expires (a sliding window that resets on inactivity).
25
+ * auto-expires (a fixed window from the first failure).
23
26
  * RETURNS the new count so a caller can detect the LOCKOUT
24
27
  * TRANSITION (count === maxAttempts is the exact edge where
25
28
  * the NEXT check() flips to `false`) and act ONCE on it — e.g.
@@ -42,9 +45,12 @@
42
45
  * count we couldn't write just means the window is a touch more lenient — never a
43
46
  * thrown error into the request path).
44
47
  *
45
- * ── ANTI-ENUMERATION ── the limiter itself is silent; the caller maps a `false`
46
- * from `check()` to the SAME generic 401 as a wrong password / wrong code, so a
47
- * locked-out account is indistinguishable from a bad credential.
48
+ * ── WHAT A LOCK ANSWERS ── the limiter itself is silent; the caller decides. On the
49
+ * unauthenticated `/auth/*` routes a lock is the SAME generic 401 as a wrong password /
50
+ * wrong code, so a locked-out account is indistinguishable from a bad credential. On the
51
+ * authenticated `/account/*` routes (change-password, MFA device remove, recovery-code
52
+ * regenerate) the caller already holds a session, so a lock answers 429 (Phase 8 #1,
53
+ * Phase 9 #3).
48
54
  *
49
55
  * ADR-0023: guard owns the infra adapter (ioredis); the class is a thin, injectable
50
56
  * collaborator the controller consumes via a DI token (`SHIELD_LOGIN_LIMITER` /
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astram/guard",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
4
4
  "license": "MIT",
5
5
  "description": "Astram framework — the enforcement guard + real-world adapters (the 'hands'). One global deny-by-default gate every request passes through (ADR-0002), classification decorators, boot-pool assertion (ADR-0018), the forRoot() registration seam, and the DB/Redis/crypto adapters that connect the sealed core to a real backend. Depends on @astram/core (ADR-0003, ADR-0023).",
6
6
  "type": "commonjs",