@astram/guard 0.1.1 → 0.3.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.
- package/dist/auth/account.controller.d.ts +245 -0
- package/dist/auth/account.controller.d.ts.map +1 -0
- package/dist/auth/account.controller.js +673 -0
- package/dist/auth/account.controller.js.map +1 -0
- package/dist/auth/auth.controller.d.ts +316 -0
- package/dist/auth/auth.controller.d.ts.map +1 -0
- package/dist/auth/auth.controller.js +1091 -0
- package/dist/auth/auth.controller.js.map +1 -0
- package/dist/auth/auth.module.d.ts +202 -0
- package/dist/auth/auth.module.d.ts.map +1 -0
- package/dist/auth/auth.module.js +610 -0
- package/dist/auth/auth.module.js.map +1 -0
- package/dist/auth/connections.d.ts +61 -0
- package/dist/auth/connections.d.ts.map +1 -0
- package/dist/auth/connections.js +116 -0
- package/dist/auth/connections.js.map +1 -0
- package/dist/auth/dto.d.ts +154 -0
- package/dist/auth/dto.d.ts.map +1 -0
- package/dist/auth/dto.js +29 -0
- package/dist/auth/dto.js.map +1 -0
- package/dist/auth/pending-mfa-store.d.ts +72 -0
- package/dist/auth/pending-mfa-store.d.ts.map +1 -0
- package/dist/auth/pending-mfa-store.js +106 -0
- package/dist/auth/pending-mfa-store.js.map +1 -0
- package/dist/auth/persistence.module.d.ts +42 -0
- package/dist/auth/persistence.module.d.ts.map +1 -0
- package/dist/auth/persistence.module.js +94 -0
- package/dist/auth/persistence.module.js.map +1 -0
- package/dist/auth/ports/audit-sink.d.ts +47 -0
- package/dist/auth/ports/audit-sink.d.ts.map +1 -0
- package/dist/auth/ports/audit-sink.js +20 -0
- package/dist/auth/ports/audit-sink.js.map +1 -0
- package/dist/auth/ports/credential-writer.d.ts +23 -0
- package/dist/auth/ports/credential-writer.d.ts.map +1 -0
- package/dist/auth/ports/credential-writer.js +2 -0
- package/dist/auth/ports/credential-writer.js.map +1 -0
- package/dist/auth/ports/email-sender.d.ts +14 -0
- package/dist/auth/ports/email-sender.d.ts.map +1 -0
- package/dist/auth/ports/email-sender.js +2 -0
- package/dist/auth/ports/email-sender.js.map +1 -0
- package/dist/auth/ports/mfa-device-store.d.ts +45 -0
- package/dist/auth/ports/mfa-device-store.d.ts.map +1 -0
- package/dist/auth/ports/mfa-device-store.js +13 -0
- package/dist/auth/ports/mfa-device-store.js.map +1 -0
- package/dist/auth/ports/profile-writer.d.ts +12 -0
- package/dist/auth/ports/profile-writer.d.ts.map +1 -0
- package/dist/auth/ports/profile-writer.js +2 -0
- package/dist/auth/ports/profile-writer.js.map +1 -0
- package/dist/auth/ports/recovery-code-store.d.ts +10 -0
- package/dist/auth/ports/recovery-code-store.d.ts.map +1 -0
- package/dist/auth/ports/recovery-code-store.js +2 -0
- package/dist/auth/ports/recovery-code-store.js.map +1 -0
- package/dist/auth/ports/reset-token-store.d.ts +12 -0
- package/dist/auth/ports/reset-token-store.d.ts.map +1 -0
- package/dist/auth/ports/reset-token-store.js +2 -0
- package/dist/auth/ports/reset-token-store.js.map +1 -0
- package/dist/auth/ports/session-facts-projector.d.ts +44 -0
- package/dist/auth/ports/session-facts-projector.d.ts.map +1 -0
- package/dist/auth/ports/session-facts-projector.js +30 -0
- package/dist/auth/ports/session-facts-projector.js.map +1 -0
- package/dist/auth/ports/session-sweeper.d.ts +12 -0
- package/dist/auth/ports/session-sweeper.d.ts.map +1 -0
- package/dist/auth/ports/session-sweeper.js +2 -0
- package/dist/auth/ports/session-sweeper.js.map +1 -0
- package/dist/auth/profile-resolver.d.ts +73 -0
- package/dist/auth/profile-resolver.d.ts.map +1 -0
- package/dist/auth/profile-resolver.js +28 -0
- package/dist/auth/profile-resolver.js.map +1 -0
- package/dist/auth/rate-limit/attempt-limiter.d.ts +91 -0
- package/dist/auth/rate-limit/attempt-limiter.d.ts.map +1 -0
- package/dist/auth/rate-limit/attempt-limiter.js +133 -0
- package/dist/auth/rate-limit/attempt-limiter.js.map +1 -0
- package/dist/auth/tokens.d.ts +236 -0
- package/dist/auth/tokens.d.ts.map +1 -0
- package/dist/auth/tokens.js +254 -0
- package/dist/auth/tokens.js.map +1 -0
- package/dist/auth/totp-enrollment.d.ts +42 -0
- package/dist/auth/totp-enrollment.d.ts.map +1 -0
- package/dist/auth/totp-enrollment.js +96 -0
- package/dist/auth/totp-enrollment.js.map +1 -0
- package/dist/authentication/argon2id-password-hasher.js.map +1 -0
- package/dist/authentication/mariadb-credential-resolver.d.ts +110 -0
- package/dist/authentication/mariadb-credential-resolver.d.ts.map +1 -0
- package/dist/authentication/mariadb-credential-resolver.js +170 -0
- package/dist/authentication/mariadb-credential-resolver.js.map +1 -0
- package/dist/authz/cross-tenant-grant-writer.d.ts +259 -0
- package/dist/authz/cross-tenant-grant-writer.d.ts.map +1 -0
- package/dist/authz/cross-tenant-grant-writer.js +397 -0
- package/dist/authz/cross-tenant-grant-writer.js.map +1 -0
- package/dist/authz/manifest-operation-authority-adapter.d.ts +65 -0
- package/dist/authz/manifest-operation-authority-adapter.d.ts.map +1 -0
- package/dist/authz/manifest-operation-authority-adapter.js +108 -0
- package/dist/authz/manifest-operation-authority-adapter.js.map +1 -0
- package/dist/authz/mariadb-cross-tenant-grant-adapter.d.ts +90 -0
- package/dist/authz/mariadb-cross-tenant-grant-adapter.d.ts.map +1 -0
- package/dist/authz/mariadb-cross-tenant-grant-adapter.js +211 -0
- package/dist/authz/mariadb-cross-tenant-grant-adapter.js.map +1 -0
- package/dist/authz/mariadb-record-rule-adapter.d.ts +60 -0
- package/dist/authz/mariadb-record-rule-adapter.d.ts.map +1 -0
- package/dist/authz/mariadb-record-rule-adapter.js +154 -0
- package/dist/authz/mariadb-record-rule-adapter.js.map +1 -0
- package/dist/authz/mariadb-record-share-adapter.d.ts +75 -0
- package/dist/authz/mariadb-record-share-adapter.d.ts.map +1 -0
- package/dist/authz/mariadb-record-share-adapter.js +106 -0
- package/dist/authz/mariadb-record-share-adapter.js.map +1 -0
- package/dist/authz/record-share-cascade.d.ts +52 -0
- package/dist/authz/record-share-cascade.d.ts.map +1 -0
- package/dist/authz/record-share-cascade.js +49 -0
- package/dist/authz/record-share-cascade.js.map +1 -0
- package/dist/authz/record-share-writer.d.ts +175 -0
- package/dist/authz/record-share-writer.d.ts.map +1 -0
- package/dist/authz/record-share-writer.js +225 -0
- package/dist/authz/record-share-writer.js.map +1 -0
- package/dist/authz/wire-record-authorization.d.ts +66 -0
- package/dist/authz/wire-record-authorization.d.ts.map +1 -0
- package/dist/authz/wire-record-authorization.js +34 -0
- package/dist/authz/wire-record-authorization.js.map +1 -0
- package/dist/boot/boot-sequence.js.map +1 -0
- package/dist/boot/boot.module.js.map +1 -0
- package/dist/completeness/catalog-reconciler.d.ts +21 -0
- package/dist/completeness/catalog-reconciler.d.ts.map +1 -0
- package/dist/completeness/catalog-reconciler.js +74 -0
- package/dist/completeness/catalog-reconciler.js.map +1 -0
- package/dist/completeness/operation-inventory-scanner.d.ts +17 -0
- package/dist/completeness/operation-inventory-scanner.d.ts.map +1 -0
- package/dist/completeness/operation-inventory-scanner.js +79 -0
- package/dist/completeness/operation-inventory-scanner.js.map +1 -0
- package/dist/completeness/ports/catalog-store.d.ts +33 -0
- package/dist/completeness/ports/catalog-store.d.ts.map +1 -0
- package/dist/completeness/ports/catalog-store.js +2 -0
- package/dist/completeness/ports/catalog-store.js.map +1 -0
- package/dist/config/app-config.d.ts +134 -1
- package/dist/config/app-config.d.ts.map +1 -1
- package/dist/config/app-config.js +144 -2
- package/dist/config/app-config.js.map +1 -0
- package/dist/config/config.module.js.map +1 -0
- package/dist/enforcement/classification.decorator.d.ts +33 -14
- package/dist/enforcement/classification.decorator.d.ts.map +1 -1
- package/dist/enforcement/classification.decorator.js +34 -14
- package/dist/enforcement/classification.decorator.js.map +1 -0
- package/dist/enforcement/core-seam.js.map +1 -0
- package/dist/enforcement/enforcement.module.js.map +1 -0
- package/dist/enforcement/global-enforcement.guard.d.ts +29 -19
- package/dist/enforcement/global-enforcement.guard.d.ts.map +1 -1
- package/dist/enforcement/global-enforcement.guard.js +108 -25
- package/dist/enforcement/global-enforcement.guard.js.map +1 -0
- package/dist/enforcement/reenroll-allowlist.d.ts +49 -0
- package/dist/enforcement/reenroll-allowlist.d.ts.map +1 -0
- package/dist/enforcement/reenroll-allowlist.js +77 -0
- package/dist/enforcement/reenroll-allowlist.js.map +1 -0
- package/dist/index.d.ts +43 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +198 -1
- package/dist/index.js.map +1 -0
- package/dist/module/shield-tokens.d.ts +15 -0
- package/dist/module/shield-tokens.d.ts.map +1 -0
- package/dist/module/shield-tokens.js +17 -0
- package/dist/module/shield-tokens.js.map +1 -0
- package/dist/module/shield.module.d.ts +203 -8
- package/dist/module/shield.module.d.ts.map +1 -1
- package/dist/module/shield.module.js +232 -14
- package/dist/module/shield.module.js.map +1 -0
- package/dist/request-auth/request-principal.d.ts +42 -0
- package/dist/request-auth/request-principal.d.ts.map +1 -0
- package/dist/request-auth/request-principal.js +87 -0
- package/dist/request-auth/request-principal.js.map +1 -0
- package/dist/request-auth/session-facts-als.d.ts +65 -0
- package/dist/request-auth/session-facts-als.d.ts.map +1 -0
- package/dist/request-auth/session-facts-als.js +57 -0
- package/dist/request-auth/session-facts-als.js.map +1 -0
- package/dist/request-auth/session-token-binder.d.ts +130 -0
- package/dist/request-auth/session-token-binder.d.ts.map +1 -0
- package/dist/request-auth/session-token-binder.js +277 -0
- package/dist/request-auth/session-token-binder.js.map +1 -0
- package/dist/request-auth/tokens.d.ts +14 -0
- package/dist/request-auth/tokens.d.ts.map +1 -0
- package/dist/request-auth/tokens.js +16 -0
- package/dist/request-auth/tokens.js.map +1 -0
- package/dist/session/crypto-adapters.js.map +1 -0
- package/dist/session/index.js.map +1 -0
- package/dist/session/mariadb-session-record-store.js.map +1 -0
- package/dist/session/redis-hot-session-store.js.map +1 -0
- package/package.json +2 -2
|
@@ -0,0 +1,91 @@
|
|
|
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>`).
|
|
7
|
+
*
|
|
8
|
+
* ── CONTRACT ─────────────────────────────────────────────────────────────────
|
|
9
|
+
* check(key) → `true` if the caller is UNDER the threshold (allowed);
|
|
10
|
+
* `false` if AT/over it (locked out for the window).
|
|
11
|
+
* recordFailure(key) → increment the counter; on the FIRST failure in a window,
|
|
12
|
+
* set the window TTL (`PEXPIRE windowMs`) so the counter
|
|
13
|
+
* auto-expires (a sliding window that resets on inactivity).
|
|
14
|
+
* RETURNS the new count so a caller can detect the LOCKOUT
|
|
15
|
+
* TRANSITION (count === maxAttempts is the exact edge where
|
|
16
|
+
* the NEXT check() flips to `false`) and act ONCE on it — e.g.
|
|
17
|
+
* emit a single lockout audit event, never one per attempt
|
|
18
|
+
* (feature #140's audit-on-the-edge, anti-flood). On a Redis
|
|
19
|
+
* best-effort miss it returns `0` (an unknown count → the
|
|
20
|
+
* caller simply doesn't treat the miss as a transition).
|
|
21
|
+
* reset(key) → clear the counter (a SUCCESS resets — a legitimate user is
|
|
22
|
+
* never penalized for their earlier fat-fingers).
|
|
23
|
+
*
|
|
24
|
+
* ── FAIL-OPEN-WITH-LOG (spec §5, the deliberate posture) ──────────────────────
|
|
25
|
+
* A limiter differs from the credential path: a Redis outage must NOT lock out
|
|
26
|
+
* every legitimate login/recovery. So `check()` FAILS OPEN on a Redis error —
|
|
27
|
+
* it returns `true` (allowed) and LOGS the blip. This never grants access on its
|
|
28
|
+
* own (the password / recovery code still gates); it only transiently LOOSENS the
|
|
29
|
+
* brute-force ceiling. `recordFailure`/`reset` are best-effort and swallow (a
|
|
30
|
+
* count we couldn't write just means the window is a touch more lenient — never a
|
|
31
|
+
* thrown error into the request path).
|
|
32
|
+
*
|
|
33
|
+
* ── ANTI-ENUMERATION ── the limiter itself is silent; the caller maps a `false`
|
|
34
|
+
* from `check()` to the SAME generic 401 as a wrong password / wrong code, so a
|
|
35
|
+
* locked-out account is indistinguishable from a bad credential.
|
|
36
|
+
*
|
|
37
|
+
* ADR-0023: guard owns the infra adapter (ioredis); the class is a thin, injectable
|
|
38
|
+
* collaborator the controller consumes via a DI token (`SHIELD_LOGIN_LIMITER` /
|
|
39
|
+
* `SHIELD_RECOVERY_LIMITER`), each provided with the live Redis + its config.
|
|
40
|
+
*/
|
|
41
|
+
import type Redis from 'ioredis';
|
|
42
|
+
/** Per-limiter tuning: the failure ceiling and the sliding-window length (ms). */
|
|
43
|
+
export interface AttemptLimiterConfig {
|
|
44
|
+
/** Number of failures within the window that trips the lockout (check → false). */
|
|
45
|
+
readonly maxAttempts: number;
|
|
46
|
+
/** The window length in ms; the counter auto-expires after this idle span. */
|
|
47
|
+
readonly windowMs: number;
|
|
48
|
+
}
|
|
49
|
+
export declare class AttemptLimiter {
|
|
50
|
+
private readonly redis;
|
|
51
|
+
private readonly cfg;
|
|
52
|
+
/** One shared logger — the fail-open path warns so a Redis outage is observable. */
|
|
53
|
+
private static readonly logger;
|
|
54
|
+
constructor(redis: Redis, cfg: AttemptLimiterConfig);
|
|
55
|
+
/**
|
|
56
|
+
* The failure ceiling for this limiter (read-only). A caller uses it to detect the
|
|
57
|
+
* lockout TRANSITION: {@link recordFailure} returns the new count, and the caller
|
|
58
|
+
* acts ONCE when that count first equals `maxAttempts` (the edge where the next
|
|
59
|
+
* {@link check} flips to `false`) — the audit-on-the-edge / anti-flood contract.
|
|
60
|
+
*/
|
|
61
|
+
get maxAttempts(): number;
|
|
62
|
+
/** Namespace every key so the two limiters + other Redis users never collide. */
|
|
63
|
+
private k;
|
|
64
|
+
/**
|
|
65
|
+
* `true` iff the caller is UNDER the threshold (allowed). FAILS OPEN on a Redis
|
|
66
|
+
* error — returns `true` and logs — so a Redis blip never locks out every login
|
|
67
|
+
* (spec §5). The credential/recovery check still gates access; this only bounds
|
|
68
|
+
* brute-force.
|
|
69
|
+
*/
|
|
70
|
+
check(key: string): Promise<boolean>;
|
|
71
|
+
/**
|
|
72
|
+
* Increment the failure counter; set the window TTL on the FIRST failure so the
|
|
73
|
+
* counter auto-expires after `windowMs` of inactivity. Best-effort — a write we
|
|
74
|
+
* can't land just makes the window slightly more lenient (never a thrown error).
|
|
75
|
+
*
|
|
76
|
+
* RETURNS the new failure count so the caller can detect the lockout TRANSITION
|
|
77
|
+
* (count === {@link maxAttempts} is the exact edge where the next {@link check}
|
|
78
|
+
* flips to `false`) and act on it exactly ONCE — feature #140's audit-on-the-edge
|
|
79
|
+
* (never one audit event per attempt past the threshold). On a Redis best-effort
|
|
80
|
+
* miss it returns `0`: an unknown count the caller must NOT treat as a transition
|
|
81
|
+
* (so a Redis blip never spuriously fires — or suppresses — the lockout audit).
|
|
82
|
+
*/
|
|
83
|
+
recordFailure(key: string): Promise<number>;
|
|
84
|
+
/**
|
|
85
|
+
* Clear the counter — a SUCCESS resets the window (the user proved themselves; do
|
|
86
|
+
* not carry their earlier failures forward). Best-effort; a swallowed error just
|
|
87
|
+
* leaves the (soon-to-expire) counter in place.
|
|
88
|
+
*/
|
|
89
|
+
reset(key: string): Promise<void>;
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=attempt-limiter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attempt-limiter.d.ts","sourceRoot":"","sources":["../../../src/auth/rate-limit/attempt-limiter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAGH,OAAO,KAAK,KAAK,MAAM,SAAS,CAAC;AAEjC,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;;;;;;;;;;;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"}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attempt-limiter.js","sourceRoot":"","sources":["../../../src/auth/rate-limit/attempt-limiter.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;;;AAEH,2CAAwC;AAWxC,MAAa,cAAc;IAKN;IACA;IALnB,oFAAoF;IAC5E,MAAM,CAAU,MAAM,GAAG,IAAI,eAAM,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC;IAEjE,YACmB,KAAY,EACZ,GAAyB;QADzB,UAAK,GAAL,KAAK,CAAO;QACZ,QAAG,GAAH,GAAG,CAAsB;IACzC,CAAC;IAEJ;;;;;OAKG;IACH,IAAI,WAAW;QACb,OAAO,IAAI,CAAC,GAAG,CAAC,WAAW,CAAC;IAC9B,CAAC;IAED,iFAAiF;IACzE,CAAC,CAAC,GAAW;QACnB,OAAO,mBAAmB,GAAG,EAAE,CAAC;IAClC,CAAC;IAED;;;;;OAKG;IACH,KAAK,CAAC,KAAK,CAAC,GAAW;QACrB,IAAI,CAAC;YACH,MAAM,CAAC,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;YAC5C,OAAO,CAAC,KAAK,IAAI,IAAI,MAAM,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,WAAW,CAAC;QACxD,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,6EAA6E;YAC7E,+EAA+E;YAC/E,cAAc,CAAC,MAAM,CAAC,IAAI,CACxB,+EAA+E,aAAa,CAAC,GAAG,CAAC,EAAE,CACpG,CAAC;YACF,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAED;;;;;;;;;;;OAWG;IACH,KAAK,CAAC,aAAa,CAAC,GAAW;QAC7B,IAAI,CAAC;YACH,MAAM,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;YACtB,MAAM,CAAC,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YACnC,+EAA+E;YAC/E,IAAI,CAAC,KAAK,CAAC;gBAAE,MAAM,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;YAC5D,OAAO,CAAC,CAAC;QACX,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,+EAA+E;YAC/E,6EAA6E;YAC7E,gFAAgF;YAChF,8DAA8D;YAC9D,cAAc,CAAC,MAAM,CAAC,IAAI,CACxB,uEAAuE,aAAa,CAAC,GAAG,CAAC,EAAE,CAC5F,CAAC;YACF,OAAO,CAAC,CAAC;QACX,CAAC;IACH,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,KAAK,CAAC,GAAW;QACrB,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;QACpC,CAAC;QAAC,MAAM,CAAC;YACP,6EAA6E;QAC/E,CAAC;IACH,CAAC;;AAtFH,wCAuFC;AAED,sFAAsF;AACtF,SAAS,aAAa,CAAC,GAAY;IACjC,OAAO,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;AAC1D,CAAC"}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 0b Auth-surface **DI provider tokens** — the shared injection seams the
|
|
3
|
+
* three parallel teams bind/consume so they never collide (ADR-0023: composition
|
|
4
|
+
* is the framework's job; these are the framework's internal wiring points).
|
|
5
|
+
*
|
|
6
|
+
* ⛔ FROZEN by this prelude. Team B (composition) PROVIDES these; Team A
|
|
7
|
+
* (credential store) provides the resolver behind {@link SHIELD_CREDENTIAL_RESOLVER};
|
|
8
|
+
* Team C (`/auth` controllers) CONSUMES {@link SHIELD_AUTHENTICATOR} +
|
|
9
|
+
* {@link SHIELD_SESSION_AUTHORITY}. Do not rename these Symbols — a rename is a
|
|
10
|
+
* breaking change to every team's wiring.
|
|
11
|
+
*
|
|
12
|
+
* NOTE — some session/hasher adapters already ship their OWN tokens in
|
|
13
|
+
* `@astram/guard` (`SHIELD_PASSWORD_HASHER`, `SHIELD_SESSION_RECORD_STORE`,
|
|
14
|
+
* `SHIELD_HOT_SESSION_STORE`, `SHIELD_TOKEN_GENERATOR`, `SHIELD_REFRESH_HASHER`,
|
|
15
|
+
* `SHIELD_CLOCK`). Those stay the canonical tokens for those adapters. This file
|
|
16
|
+
* adds ONLY the NEW seams Phase 0b introduces: the live connections
|
|
17
|
+
* ({@link SHIELD_DB_POOL} / {@link SHIELD_REDIS}), the credential resolver, and the
|
|
18
|
+
* two COMPOSED engines ({@link SHIELD_AUTHENTICATOR} / {@link SHIELD_SESSION_AUTHORITY}).
|
|
19
|
+
* {@link SHIELD_PASSWORD_HASHER} is re-exported from the argon2 adapter for
|
|
20
|
+
* convenience so a team can import every auth-relevant token from one module.
|
|
21
|
+
*
|
|
22
|
+
* ADR-0003: no framework/DB/crypto imports here — just Symbols (+ one re-export).
|
|
23
|
+
*/
|
|
24
|
+
export { SHIELD_PASSWORD_HASHER } from '../authentication/argon2id-password-hasher.js';
|
|
25
|
+
/**
|
|
26
|
+
* The ONE live `mariadb` `Pool` (the `shield_core` account — DML on
|
|
27
|
+
* `shield_identity`, ADR-0013/0014). **Provided by Team B** (the persistence
|
|
28
|
+
* factory — the single place a live DB connection is created). **Consumed by**
|
|
29
|
+
* the {@link SHIELD_CREDENTIAL_RESOLVER} factory and the
|
|
30
|
+
* {@link SHIELD_SESSION_RECORD_STORE} factory. Disposed on app shutdown.
|
|
31
|
+
*/
|
|
32
|
+
export declare const SHIELD_DB_POOL: unique symbol;
|
|
33
|
+
/**
|
|
34
|
+
* The ONE live `ioredis` client (the hot session tier, ADR-0013). **Provided by
|
|
35
|
+
* Team B** (the persistence factory). **Consumed by** the
|
|
36
|
+
* {@link SHIELD_HOT_SESSION_STORE} factory. Disposed on app shutdown.
|
|
37
|
+
*/
|
|
38
|
+
export declare const SHIELD_REDIS: unique symbol;
|
|
39
|
+
/**
|
|
40
|
+
* The `CredentialResolver` (core port) that resolves an identity hint →
|
|
41
|
+
* `ResolvedCredential | null`. **Provided by Team A** (the
|
|
42
|
+
* `MariaDbCredentialResolver`, injected with {@link SHIELD_DB_POOL}). **Consumed
|
|
43
|
+
* by** the {@link SHIELD_AUTHENTICATOR} factory (Team B) as
|
|
44
|
+
* `createAuthenticator({ resolver })`.
|
|
45
|
+
*/
|
|
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;
|
|
56
|
+
/**
|
|
57
|
+
* The composed `Authenticator` (core engine) — `createAuthenticator({ hasher,
|
|
58
|
+
* resolver, identityBroker, factorVerifiers:[totp] })`. **Provided by Team B**
|
|
59
|
+
* (the AuthModule factory). **Consumed by** Team C's `/auth/login` +
|
|
60
|
+
* `/auth/mfa` controllers.
|
|
61
|
+
*/
|
|
62
|
+
export declare const SHIELD_AUTHENTICATOR: unique symbol;
|
|
63
|
+
/**
|
|
64
|
+
* The composed `SessionAuthority` (core engine) — `createSessionAuthority({
|
|
65
|
+
* recordStore, hotStore, tokenGenerator, refreshHasher, clock, lifetimes })`.
|
|
66
|
+
* **Provided by Team B** (the AuthModule factory). **Consumed by** Team C's
|
|
67
|
+
* `/auth/login` (establish), `/auth/refresh` (rotate), `/auth/logout` (revoke),
|
|
68
|
+
* and `/auth/mfa` (establish after factor).
|
|
69
|
+
*/
|
|
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;
|
|
236
|
+
//# sourceMappingURL=tokens.d.ts.map
|
|
@@ -0,0 +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;;;;;;;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"}
|