@astram/guard 0.9.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.
- package/dist/auth/account.controller.d.ts +21 -13
- package/dist/auth/account.controller.d.ts.map +1 -1
- package/dist/auth/account.controller.js +91 -38
- package/dist/auth/auth.controller.d.ts +6 -5
- package/dist/auth/auth.controller.d.ts.map +1 -1
- package/dist/auth/auth.controller.js +127 -75
- package/dist/auth/auth.module.d.ts.map +1 -1
- package/dist/auth/auth.module.js +10 -7
- package/dist/auth/connections.d.ts.map +1 -1
- package/dist/auth/connections.js +3 -1
- package/dist/auth/rate-limit/attempt-limiter.d.ts +75 -11
- package/dist/auth/rate-limit/attempt-limiter.d.ts.map +1 -1
- package/dist/auth/rate-limit/attempt-limiter.js +100 -11
- package/dist/auth/tokens.d.ts +13 -10
- package/dist/auth/tokens.d.ts.map +1 -1
- package/dist/auth/tokens.js +13 -10
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -3
- package/dist/module/shield-tokens.d.ts +12 -0
- package/dist/module/shield-tokens.d.ts.map +1 -1
- package/dist/module/shield-tokens.js +13 -1
- package/dist/module/shield.module.d.ts +6 -2
- package/dist/module/shield.module.d.ts.map +1 -1
- package/dist/module/shield.module.js +40 -18
- package/package.json +3 -3
|
@@ -1,17 +1,28 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* `AttemptLimiter` — a guard-owned, Redis-backed
|
|
4
|
-
* anti-brute-force on the auth surface (spec §5).
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* (
|
|
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 ─────────────────────────────────────────────────────────────────
|
|
13
|
+
* consume(key) → Phase 9 #10, the gate for credential-verifying routes (login,
|
|
14
|
+
* /auth/mfa, change-password, the code-gated account MFA routes):
|
|
15
|
+
* atomically COUNTS this attempt first and returns
|
|
16
|
+
* `{ allowed: count <= maxAttempts, count }`. Parallel guesses get
|
|
17
|
+
* distinct counts, so at most `maxAttempts` reach the verifier.
|
|
18
|
+
* FAILS CLOSED: a Redis error throws (caller → 503).
|
|
19
|
+
* refund(key) → give back one consumed attempt that reached no verdict
|
|
20
|
+
* (transient outage; a step that is not a failure). Best-effort.
|
|
10
21
|
* check(key) → `true` if the caller is UNDER the threshold (allowed);
|
|
11
22
|
* `false` if AT/over it (locked out for the window).
|
|
12
23
|
* recordFailure(key) → increment the counter; on the FIRST failure in a window,
|
|
13
24
|
* set the window TTL (`PEXPIRE windowMs`) so the counter
|
|
14
|
-
* auto-expires (a
|
|
25
|
+
* auto-expires (a fixed window from the first failure).
|
|
15
26
|
* RETURNS the new count so a caller can detect the LOCKOUT
|
|
16
27
|
* TRANSITION (count === maxAttempts is the exact edge where
|
|
17
28
|
* the NEXT check() flips to `false`) and act ONCE on it — e.g.
|
|
@@ -22,7 +33,10 @@
|
|
|
22
33
|
* reset(key) → clear the counter (a SUCCESS resets — a legitimate user is
|
|
23
34
|
* never penalized for their earlier fat-fingers).
|
|
24
35
|
*
|
|
25
|
-
* ── FAIL-OPEN-WITH-LOG (spec §5
|
|
36
|
+
* ── FAIL-OPEN-WITH-LOG (spec §5) — the read-only `check()` path only ──────────
|
|
37
|
+
* (`consume()` fails CLOSED instead — Phase 9 #10; the routes that use it also need
|
|
38
|
+
* Redis to bind or resolve a session, so failing closed locks no one out who could
|
|
39
|
+
* otherwise have signed in.) `check()` remains for forgot/reset-password, where:
|
|
26
40
|
* A limiter differs from the credential path: a Redis outage must NOT lock out
|
|
27
41
|
* every legitimate login/recovery. So `check()` FAILS OPEN on a Redis error —
|
|
28
42
|
* it returns `true` (allowed) and LOGS the blip. This never grants access on its
|
|
@@ -31,17 +45,47 @@
|
|
|
31
45
|
* count we couldn't write just means the window is a touch more lenient — never a
|
|
32
46
|
* thrown error into the request path).
|
|
33
47
|
*
|
|
34
|
-
* ──
|
|
35
|
-
*
|
|
36
|
-
* 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).
|
|
37
54
|
*
|
|
38
55
|
* ADR-0023: guard owns the infra adapter (ioredis); the class is a thin, injectable
|
|
39
56
|
* collaborator the controller consumes via a DI token (`SHIELD_LOGIN_LIMITER` /
|
|
40
57
|
* `SHIELD_RECOVERY_LIMITER`), each provided with the live Redis + its config.
|
|
41
58
|
*/
|
|
42
59
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
43
|
-
exports.AttemptLimiter = void 0;
|
|
60
|
+
exports.AttemptLimiter = exports.AttemptLimiterUnavailableError = exports.ATTEMPT_REFUND_SCRIPT = exports.ATTEMPT_CONSUME_SCRIPT = void 0;
|
|
44
61
|
const common_1 = require("@nestjs/common");
|
|
62
|
+
/**
|
|
63
|
+
* Phase 9 #10 — the atomic increment-first step: count the attempt and arm the window TTL
|
|
64
|
+
* in one script, so the counter can never be left without an expiry between two commands.
|
|
65
|
+
* The TTL is also re-armed when the key somehow has none (`PTTL == -1`), so a counter
|
|
66
|
+
* left persistent by an older non-atomic write heals instead of locking forever.
|
|
67
|
+
*/
|
|
68
|
+
exports.ATTEMPT_CONSUME_SCRIPT = `local n = redis.call('INCR', KEYS[1])
|
|
69
|
+
if n == 1 or redis.call('PTTL', KEYS[1]) == -1 then
|
|
70
|
+
redis.call('PEXPIRE', KEYS[1], ARGV[1])
|
|
71
|
+
end
|
|
72
|
+
return n`;
|
|
73
|
+
/** Phase 9 #10 — give back one consumed attempt; deletes the key when it reaches zero. */
|
|
74
|
+
exports.ATTEMPT_REFUND_SCRIPT = `if redis.call('EXISTS', KEYS[1]) == 0 then return 0 end
|
|
75
|
+
local n = redis.call('DECR', KEYS[1])
|
|
76
|
+
if n <= 0 then redis.call('DEL', KEYS[1]) end
|
|
77
|
+
return n`;
|
|
78
|
+
/**
|
|
79
|
+
* Thrown by {@link AttemptLimiter.consume} when Redis cannot answer: the gate fails CLOSED
|
|
80
|
+
* and the caller answers a retryable 503. Carries no key or Redis detail.
|
|
81
|
+
*/
|
|
82
|
+
class AttemptLimiterUnavailableError extends Error {
|
|
83
|
+
constructor() {
|
|
84
|
+
super('attempt limiter unavailable');
|
|
85
|
+
this.name = 'AttemptLimiterUnavailableError';
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
exports.AttemptLimiterUnavailableError = AttemptLimiterUnavailableError;
|
|
45
89
|
class AttemptLimiter {
|
|
46
90
|
redis;
|
|
47
91
|
cfg;
|
|
@@ -82,6 +126,51 @@ class AttemptLimiter {
|
|
|
82
126
|
return true;
|
|
83
127
|
}
|
|
84
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* INCREMENT-FIRST attempt gate (Phase 9 #10). Atomically counts THIS attempt (one Lua
|
|
131
|
+
* `INCR`, arming the window TTL on the first hit) and reports whether it is within the
|
|
132
|
+
* budget (`count <= maxAttempts`). Call it BEFORE verifying the credential: N parallel
|
|
133
|
+
* guesses each get a distinct count, so at most `maxAttempts` of them ever reach the
|
|
134
|
+
* verifier (the old check → verify → recordFailure shape let every in-flight guess pass
|
|
135
|
+
* the check). The attempt stays counted when the credential is wrong; on success the
|
|
136
|
+
* caller calls {@link reset}; when the attempt never reached a verdict (a transient
|
|
137
|
+
* outage, or a step that is not a failure) the caller calls {@link refund}.
|
|
138
|
+
*
|
|
139
|
+
* `count === maxAttempts` on a FAILED attempt is the lockout edge (audit-on-the-edge).
|
|
140
|
+
*
|
|
141
|
+
* FAILS CLOSED: a Redis error throws {@link AttemptLimiterUnavailableError} (the caller
|
|
142
|
+
* answers a retryable 503). Unlike {@link check}, an outage never lets an unthrottled
|
|
143
|
+
* guess through.
|
|
144
|
+
*/
|
|
145
|
+
async consume(key) {
|
|
146
|
+
let n;
|
|
147
|
+
try {
|
|
148
|
+
n = Number(await this.redis.eval(exports.ATTEMPT_CONSUME_SCRIPT, 1, this.k(key), String(this.cfg.windowMs)));
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
AttemptLimiter.logger.warn(`attempt-limiter consume failed closed (denying) — Redis unavailable: ${describeError(err)}`);
|
|
152
|
+
throw new AttemptLimiterUnavailableError();
|
|
153
|
+
}
|
|
154
|
+
if (!Number.isFinite(n) || n < 1) {
|
|
155
|
+
// An answer that is not a count cannot prove the attempt is within budget.
|
|
156
|
+
throw new AttemptLimiterUnavailableError();
|
|
157
|
+
}
|
|
158
|
+
return { allowed: n <= this.cfg.maxAttempts, count: n };
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Give back one attempt taken by {@link consume} that never reached a verdict (a
|
|
162
|
+
* transient dependency failure, or a step that is neither failure nor success, e.g. a
|
|
163
|
+
* password step that now needs a second factor). Removes the key when it reaches zero,
|
|
164
|
+
* so a refunded first attempt leaves no counter behind. Best-effort, never throws.
|
|
165
|
+
*/
|
|
166
|
+
async refund(key) {
|
|
167
|
+
try {
|
|
168
|
+
await this.redis.eval(exports.ATTEMPT_REFUND_SCRIPT, 1, this.k(key));
|
|
169
|
+
}
|
|
170
|
+
catch (err) {
|
|
171
|
+
AttemptLimiter.logger.warn(`attempt-limiter refund best-effort miss — Redis unavailable: ${describeError(err)}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
85
174
|
/**
|
|
86
175
|
* Increment the failure counter; set the window TTL on the FIRST failure so the
|
|
87
176
|
* counter auto-expires after `windowMs` of inactivity. Best-effort — a write we
|
package/dist/auth/tokens.d.ts
CHANGED
|
@@ -216,9 +216,11 @@ export declare const SHIELD_MEMBERSHIP_CHECKER: unique symbol;
|
|
|
216
216
|
* typed hint (the DB collation decides which spellings are one account), else
|
|
217
217
|
* `login:<sha256(canonical hint)>` for an unknown hint — no PII in the key; see
|
|
218
218
|
* `rate-limit/canonical-hint.ts`. Default 10 failures / 15-min window.
|
|
219
|
-
*
|
|
220
|
-
*
|
|
221
|
-
*
|
|
219
|
+
* Phase 9 #10: every attempt is counted at the TOP of `POST /auth/login` (`consume`, atomic
|
|
220
|
+
* increment-first → generic 401 when over budget, 503 when Redis cannot answer); a rejected
|
|
221
|
+
* wrong-password stays counted, `authenticated` → `reset`, `mfa_required` or a transient
|
|
222
|
+
* outage → `refund` (a correct password that merely needs MFA is NOT a failure, nor a
|
|
223
|
+
* success until `/auth/mfa` completes). Shared with `POST /account/change-password`.
|
|
222
224
|
*/
|
|
223
225
|
export declare const SHIELD_LOGIN_LIMITER: unique symbol;
|
|
224
226
|
/**
|
|
@@ -227,8 +229,8 @@ export declare const SHIELD_LOGIN_LIMITER: unique symbol;
|
|
|
227
229
|
* `LOGIN_IP_MAX_FAILURES` / `LOGIN_IP_WINDOW_MS`, see `AppConfig.loginIpLimit`). Caps
|
|
228
230
|
* password spraying across MANY distinct hints from one client. Counts wrong-password
|
|
229
231
|
* FAILURES only and is NEVER reset by a success (an attacker's own valid login must not
|
|
230
|
-
* wipe the budget).
|
|
231
|
-
*
|
|
232
|
+
* wipe the budget; a non-failure gives its attempt back). Counted before the account lookup
|
|
233
|
+
* and the authenticator (Phase 9 #10); fails closed (503) on a Redis error. ENFORCED ONLY when the app set Express
|
|
232
234
|
* `trust proxy` explicitly (0 = no proxy, else the hop count): otherwise every client
|
|
233
235
|
* behind a proxy shares one `req.ip`, so the limiter is switched OFF at boot with a
|
|
234
236
|
* warning (`TrustProxyBootCheck`) rather than becoming a global login DoS.
|
|
@@ -237,16 +239,17 @@ export declare const SHIELD_LOGIN_IP_LIMITER: unique symbol;
|
|
|
237
239
|
/**
|
|
238
240
|
* The recovery-code {@link AttemptLimiter} (spec §5) — keyed `recovery:<principalId>`,
|
|
239
241
|
* default 5 failures / 15-min window (recovery codes are higher-value than a rotating
|
|
240
|
-
* 30-s TOTP). Consumed in the `/auth/mfa` recovery branch:
|
|
241
|
-
* (
|
|
242
|
-
* code, `reset` on success.
|
|
242
|
+
* 30-s TOTP). Consumed in the `/auth/mfa` recovery branch: the attempt is counted
|
|
243
|
+
* (`consume`, Phase 9 #10) BEFORE the store is consulted (over budget → generic 401;
|
|
244
|
+
* Redis error → 503), a wrong code stays counted, `reset` on success.
|
|
243
245
|
*/
|
|
244
246
|
export declare const SHIELD_RECOVERY_LIMITER: unique symbol;
|
|
245
247
|
/**
|
|
246
248
|
* The TOTP-step {@link AttemptLimiter} (security hardening #9) — keyed per account,
|
|
247
249
|
* `mfa:pid:<principalId>`, default 5 failures / 15-min window. Consumed in the `/auth/mfa`
|
|
248
|
-
* TOTP branch
|
|
249
|
-
*
|
|
250
|
+
* TOTP branch and the code-gated `/account/mfa/*` routes: the attempt is counted
|
|
251
|
+
* (`consume`, Phase 9 #10) BEFORE the factor is verified (over budget → generic 401 /
|
|
252
|
+
* 429; Redis error → 503), a rejected factor stays counted (the threshold edge audits
|
|
250
253
|
* `auth.mfa.locked` once), `reset` on success.
|
|
251
254
|
*/
|
|
252
255
|
export declare const SHIELD_MFA_LIMITER: unique symbol;
|
|
@@ -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;;;;;;;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;AAEvF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,6BAA6B,eAA0C,CAAC;AAErF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,yBAAyB,eAAsC,CAAC;AAU7E
|
|
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;AAEvF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,6BAA6B,eAA0C,CAAC;AAErF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,yBAAyB,eAAsC,CAAC;AAU7E;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,oBAAoB,eAAiC,CAAC;AAEnE;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,uBAAuB,eAAoC,CAAC;AAEzE;;;;;;GAMG;AACH,eAAO,MAAM,uBAAuB,eAAoC,CAAC;AAEzE;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB,eAA+B,CAAC;AAE/D;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB,eAA+B,CAAC;AAE/D;;;;;GAKG;AACH,eAAO,MAAM,0BAA0B,eAAuC,CAAC;AAE/E;;;;;;;;;;GAUG;AACH,eAAO,MAAM,qBAAqB,eAAkC,CAAC;AAErE;;;;;;;;;GASG;AACH,eAAO,MAAM,0BAA0B,eAAuC,CAAC;AAE/E;;;;;;;;;;GAUG;AACH,eAAO,MAAM,oBAAoB,eAAiC,CAAC;AAEnE;;;;;;;GAOG;AACH,eAAO,MAAM,uBAAuB,eAAoC,CAAC;AAEzE;;;;;;;GAOG;AACH,eAAO,MAAM,+BAA+B,eAA4C,CAAC;AAEzF;;;;;;GAMG;AACH,eAAO,MAAM,2BAA2B,eAAwC,CAAC"}
|
package/dist/auth/tokens.js
CHANGED
|
@@ -235,9 +235,11 @@ exports.SHIELD_MEMBERSHIP_CHECKER = Symbol('SHIELD_MEMBERSHIP_CHECKER');
|
|
|
235
235
|
* typed hint (the DB collation decides which spellings are one account), else
|
|
236
236
|
* `login:<sha256(canonical hint)>` for an unknown hint — no PII in the key; see
|
|
237
237
|
* `rate-limit/canonical-hint.ts`. Default 10 failures / 15-min window.
|
|
238
|
-
*
|
|
239
|
-
*
|
|
240
|
-
*
|
|
238
|
+
* Phase 9 #10: every attempt is counted at the TOP of `POST /auth/login` (`consume`, atomic
|
|
239
|
+
* increment-first → generic 401 when over budget, 503 when Redis cannot answer); a rejected
|
|
240
|
+
* wrong-password stays counted, `authenticated` → `reset`, `mfa_required` or a transient
|
|
241
|
+
* outage → `refund` (a correct password that merely needs MFA is NOT a failure, nor a
|
|
242
|
+
* success until `/auth/mfa` completes). Shared with `POST /account/change-password`.
|
|
241
243
|
*/
|
|
242
244
|
exports.SHIELD_LOGIN_LIMITER = Symbol('SHIELD_LOGIN_LIMITER');
|
|
243
245
|
/**
|
|
@@ -246,8 +248,8 @@ exports.SHIELD_LOGIN_LIMITER = Symbol('SHIELD_LOGIN_LIMITER');
|
|
|
246
248
|
* `LOGIN_IP_MAX_FAILURES` / `LOGIN_IP_WINDOW_MS`, see `AppConfig.loginIpLimit`). Caps
|
|
247
249
|
* password spraying across MANY distinct hints from one client. Counts wrong-password
|
|
248
250
|
* FAILURES only and is NEVER reset by a success (an attacker's own valid login must not
|
|
249
|
-
* wipe the budget).
|
|
250
|
-
*
|
|
251
|
+
* wipe the budget; a non-failure gives its attempt back). Counted before the account lookup
|
|
252
|
+
* and the authenticator (Phase 9 #10); fails closed (503) on a Redis error. ENFORCED ONLY when the app set Express
|
|
251
253
|
* `trust proxy` explicitly (0 = no proxy, else the hop count): otherwise every client
|
|
252
254
|
* behind a proxy shares one `req.ip`, so the limiter is switched OFF at boot with a
|
|
253
255
|
* warning (`TrustProxyBootCheck`) rather than becoming a global login DoS.
|
|
@@ -256,16 +258,17 @@ exports.SHIELD_LOGIN_IP_LIMITER = Symbol('SHIELD_LOGIN_IP_LIMITER');
|
|
|
256
258
|
/**
|
|
257
259
|
* The recovery-code {@link AttemptLimiter} (spec §5) — keyed `recovery:<principalId>`,
|
|
258
260
|
* default 5 failures / 15-min window (recovery codes are higher-value than a rotating
|
|
259
|
-
* 30-s TOTP). Consumed in the `/auth/mfa` recovery branch:
|
|
260
|
-
* (
|
|
261
|
-
* code, `reset` on success.
|
|
261
|
+
* 30-s TOTP). Consumed in the `/auth/mfa` recovery branch: the attempt is counted
|
|
262
|
+
* (`consume`, Phase 9 #10) BEFORE the store is consulted (over budget → generic 401;
|
|
263
|
+
* Redis error → 503), a wrong code stays counted, `reset` on success.
|
|
262
264
|
*/
|
|
263
265
|
exports.SHIELD_RECOVERY_LIMITER = Symbol('SHIELD_RECOVERY_LIMITER');
|
|
264
266
|
/**
|
|
265
267
|
* The TOTP-step {@link AttemptLimiter} (security hardening #9) — keyed per account,
|
|
266
268
|
* `mfa:pid:<principalId>`, default 5 failures / 15-min window. Consumed in the `/auth/mfa`
|
|
267
|
-
* TOTP branch
|
|
268
|
-
*
|
|
269
|
+
* TOTP branch and the code-gated `/account/mfa/*` routes: the attempt is counted
|
|
270
|
+
* (`consume`, Phase 9 #10) BEFORE the factor is verified (over budget → generic 401 /
|
|
271
|
+
* 429; Redis error → 503), a rejected factor stays counted (the threshold edge audits
|
|
269
272
|
* `auth.mfa.locked` once), `reset` on success.
|
|
270
273
|
*/
|
|
271
274
|
exports.SHIELD_MFA_LIMITER = Symbol('SHIELD_MFA_LIMITER');
|
package/dist/index.d.ts
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
export { guardMigrationsDir } from './migrations-dir.js';
|
|
18
18
|
export { buildIdentityModuleRegistry } from './identity/module-registry.bridge.js';
|
|
19
19
|
export { MODULE_KEY, IDENTITY_ACTIONS, actionsFor } from './identity/identity-actions.js';
|
|
20
|
-
export { ShieldModule, SHIELD_DECISION_AUTHORITY, type ShieldModuleOptions, type ShieldModuleAsyncOptions, type RecordAuthorizationInputs, } from './module/shield.module.js';
|
|
20
|
+
export { ShieldModule, SHIELD_DECISION_AUTHORITY, SHIELD_RECORD_AUTHORIZATION, type ShieldModuleOptions, type ShieldModuleAsyncOptions, type RecordAuthorizationInputs, } from './module/shield.module.js';
|
|
21
21
|
export { Public, Classified, CLASSIFICATION_METADATA, DEFAULT_CLASSIFIED_ACTION, normalizeClassification, } from './enforcement/classification.decorator.js';
|
|
22
22
|
export type { Classification } from './enforcement/classification.decorator.js';
|
|
23
23
|
export { OperationInventoryScanner } from './completeness/operation-inventory-scanner.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAIH,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AASzD,OAAO,EAAE,2BAA2B,EAAE,MAAM,sCAAsC,CAAC;AACnF,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,gCAAgC,CAAC;AAG1F,OAAO,EACL,YAAY,EACZ,yBAAyB,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -16,9 +16,9 @@
|
|
|
16
16
|
* `@nestjs/core`, `reflect-metadata`, `rxjs` so the host owns one copy of each.
|
|
17
17
|
*/
|
|
18
18
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
-
exports.
|
|
20
|
-
exports.
|
|
21
|
-
exports.ScopeUnknownParentError = exports.ScopeDepthError = exports.ScopeCycleError = exports.buildClosureRows = exports.AccountController = exports.generateRecoveryCodes = exports.buildOtpauthUri = exports.generateTotpSecret = exports.SHIELD_LOGIN_TOTP_VERIFIER = exports.SHIELD_MFA_STEP_UP = exports.SHIELD_MFA_LIMITER = exports.challengeMatches = exports.credentialFingerprint = exports.CredentialMfaStepUp = exports.RedisPendingMfaStore = exports.PendingMfaStore = exports.AuthController = exports.SHIELD_SCOPE_SUBTREE_RESOLVER = exports.SHIELD_SESSION_FACTS_PROJECTOR = exports.hmacSha1 = exports.resolveTotpVerifier = exports.makeDefaultTotpVerifier = exports.AuthModule = exports.MariaDbCredentialResolver = exports.SHIELD_TOTP_VERIFIER_OVERRIDE = exports.SHIELD_MFA_DEVICE_STORE = exports.SHIELD_AUDIT_SINK = exports.SHIELD_SESSION_SWEEPER = 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.isDevOrTestNodeEnv = void 0;
|
|
19
|
+
exports.ROTATE_GRACE_MS = exports.SHIELD_SESSION_LIFETIMES = exports.HotRevokingSessionRecordStore = exports.SHIELD_CLOCK = exports.SHIELD_REFRESH_HASHER = exports.SHIELD_TOKEN_GENERATOR = exports.systemClockProvider = exports.refreshHasherProvider = exports.cryptoTokenGeneratorProvider = exports.SystemClock = exports.Sha256RefreshTokenHasher = exports.CryptoTokenGenerator = exports.SHIELD_HOT_SESSION_STORE = exports.RedisHotSessionStore = exports.SHIELD_SESSION_RECORD_STORE = exports.MariaDbSessionRecordStore = exports.EncryptedFileSecretsProvider = exports.LocalKekCrypto = exports.SHIELD_PASSWORD_HASHER = exports.argon2idHasherProvider = exports.Argon2idPasswordHasher = exports.stubDecisionAuthority = exports.GlobalEnforcementGuard = exports.NO_SESSION_FACTS = exports.SHIELD_SESSION_FACTS_ALS = exports.MariaDbApiKeyStore = exports.isApiKey = exports.extractBearerToken = exports.principalFromClaims = exports.resolveRequestAuth = exports.resolveRequestPrincipal = exports.SessionTokenBinder = exports.SHIELD_SERVICE_AUTHENTICATOR = exports.SHIELD_SESSION_TOKEN_BINDER = exports.CatalogGatabilityError = exports.CatalogReconciler = exports.OperationInventoryScanner = exports.normalizeClassification = exports.DEFAULT_CLASSIFIED_ACTION = exports.CLASSIFICATION_METADATA = exports.Classified = exports.Public = exports.SHIELD_RECORD_AUTHORIZATION = exports.SHIELD_DECISION_AUTHORITY = exports.ShieldModule = exports.actionsFor = exports.IDENTITY_ACTIONS = exports.MODULE_KEY = exports.buildIdentityModuleRegistry = exports.guardMigrationsDir = void 0;
|
|
20
|
+
exports.loadLoginIpLimit = exports.DEFAULT_REDIS_URL = exports.DEFAULT_DB_CONFIG = exports.assertRefreshHmacKey = exports.loadRefreshHmacKey = exports.loadSessionLifetimes = exports.loadRedisConfig = exports.loadDbConfig = exports.SCHEMA_SVC_POOL_HARD_CAP = exports.DEFAULT_POOL_BUDGET = exports.evaluatePoolBudget = exports.loadPoolBudget = exports.loadAppConfig = exports.APP_CONFIG = exports.assertPoolBudget = exports.BootSequence = exports.buildListScopeFn = exports.MariaDbSessionRestrictionStore = exports.SHIELD_SESSION_RESTRICTIONS = exports.SHIELD_PERMISSION_VERSION_CACHE = exports.DEFAULT_PERMISSION_VERSION_TTL_MS = exports.PERMISSION_VERSION_KEY_PREFIX = exports.RedisPermissionVersionCache = exports.withRedisPermissionVersion = exports.buildRecordAuthorization = exports.SHIELD_OPERATION_AUTHORITY_PORT = exports.ManifestOperationAuthorityAdapter = exports.GrantStoreUnavailableError = exports.InvalidGrantError = exports.GrantNotPermittedError = exports.SHIELD_CROSS_TENANT_GRANT_WRITER = exports.CrossTenantGrantWriter = exports.SHIELD_CROSS_TENANT_GRANT_PORT = exports.MariaDbCrossTenantGrantAdapter = exports.makeShareDeleteCascade = exports.ShareStoreUnavailableError = exports.ShareNotPermittedError = exports.SHIELD_RECORD_SHARE_WRITER = exports.RecordShareWriter = exports.SHIELD_RECORD_SHARE_PORT = exports.MariaDbRecordShareAdapter = exports.UnknownScopeFieldError = exports.translateListScope = exports.SHIELD_LIST_SCOPE_PORT = exports.MariaDbListScopeAdapter = exports.SHIELD_POLICY_STORE = exports.MariaDbPolicyStore = exports.SHIELD_RECORD_RULE_PORT = exports.MariaDbRecordPermissionRuleAdapter = exports.accessBindingTtlMs = void 0;
|
|
21
|
+
exports.ScopeUnknownParentError = exports.ScopeDepthError = exports.ScopeCycleError = exports.buildClosureRows = exports.AccountController = exports.generateRecoveryCodes = exports.buildOtpauthUri = exports.generateTotpSecret = exports.SHIELD_LOGIN_TOTP_VERIFIER = exports.SHIELD_MFA_STEP_UP = exports.SHIELD_MFA_LIMITER = exports.challengeMatches = exports.credentialFingerprint = exports.CredentialMfaStepUp = exports.RedisPendingMfaStore = exports.PendingMfaStore = exports.AuthController = exports.SHIELD_SCOPE_SUBTREE_RESOLVER = exports.SHIELD_SESSION_FACTS_PROJECTOR = exports.hmacSha1 = exports.resolveTotpVerifier = exports.makeDefaultTotpVerifier = exports.AuthModule = exports.MariaDbCredentialResolver = exports.SHIELD_TOTP_VERIFIER_OVERRIDE = exports.SHIELD_MFA_DEVICE_STORE = exports.SHIELD_AUDIT_SINK = exports.SHIELD_SESSION_SWEEPER = 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.isDevOrTestNodeEnv = exports.DEFAULT_LOGIN_IP_LIMIT = void 0;
|
|
22
22
|
// Framework authz migrations (shield_authz DDL — permission / role_permission /
|
|
23
23
|
// authz_permission_version). Absolute path a consumer's `@astram/migrate` setup merges in.
|
|
24
24
|
var migrations_dir_js_1 = require("./migrations-dir.js");
|
|
@@ -40,6 +40,9 @@ Object.defineProperty(exports, "actionsFor", { enumerable: true, get: function (
|
|
|
40
40
|
var shield_module_js_1 = require("./module/shield.module.js");
|
|
41
41
|
Object.defineProperty(exports, "ShieldModule", { enumerable: true, get: function () { return shield_module_js_1.ShieldModule; } });
|
|
42
42
|
Object.defineProperty(exports, "SHIELD_DECISION_AUTHORITY", { enumerable: true, get: function () { return shield_module_js_1.SHIELD_DECISION_AUTHORITY; } });
|
|
43
|
+
// Phase 9 #7: the record-authorization inputs a feature module (IdentityFeatureModule)
|
|
44
|
+
// provides for ShieldModule to build the request-path authority from (one source).
|
|
45
|
+
Object.defineProperty(exports, "SHIELD_RECORD_AUTHORIZATION", { enumerable: true, get: function () { return shield_module_js_1.SHIELD_RECORD_AUTHORIZATION; } });
|
|
43
46
|
// Route classification (the consumer authors these on its controllers).
|
|
44
47
|
var classification_decorator_js_1 = require("./enforcement/classification.decorator.js");
|
|
45
48
|
Object.defineProperty(exports, "Public", { enumerable: true, get: function () { return classification_decorator_js_1.Public; } });
|
|
@@ -17,4 +17,16 @@
|
|
|
17
17
|
* now import the ONE symbol from core, so it stays a single DI identity across the app.
|
|
18
18
|
*/
|
|
19
19
|
export { SHIELD_DECISION_AUTHORITY } from '@astram/core';
|
|
20
|
+
/**
|
|
21
|
+
* The record-authorization inputs (`RecordAuthorizationInputs`: pool + app ports + registry) that
|
|
22
|
+
* a FEATURE module provides app-wide for `ShieldModule` to build the request-path decision
|
|
23
|
+
* authority from (security hardening Phase 9 #7). `@astram/identity`'s `IdentityFeatureModule`
|
|
24
|
+
* provides it, built from the same `extraObjects` as its own authorities, so the request path and
|
|
25
|
+
* the identity module can never judge with different registries. Guard never imports identity:
|
|
26
|
+
* this token is the seam.
|
|
27
|
+
*
|
|
28
|
+
* Exactly one source per app: this token, `ShieldModule.forRoot({ recordAuthorization })` or
|
|
29
|
+
* `ShieldModule.forRoot({ decisionAuthority })`. Two of them fail the boot.
|
|
30
|
+
*/
|
|
31
|
+
export declare const SHIELD_RECORD_AUTHORIZATION: unique symbol;
|
|
20
32
|
//# sourceMappingURL=shield-tokens.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shield-tokens.d.ts","sourceRoot":"","sources":["../../src/module/shield-tokens.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;;;;;;;;GAUG;AACH,OAAO,EAAE,yBAAyB,EAAE,MAAM,cAAc,CAAC"}
|
|
1
|
+
{"version":3,"file":"shield-tokens.d.ts","sourceRoot":"","sources":["../../src/module/shield-tokens.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;;;;;;;;GAUG;AACH,OAAO,EAAE,yBAAyB,EAAE,MAAM,cAAc,CAAC;AAEzD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,2BAA2B,eAAwC,CAAC"}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* with no cycle. `shield.module.ts` re-exports these for the public barrel.
|
|
8
8
|
*/
|
|
9
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
-
exports.SHIELD_DECISION_AUTHORITY = void 0;
|
|
10
|
+
exports.SHIELD_RECORD_AUTHORIZATION = exports.SHIELD_DECISION_AUTHORITY = void 0;
|
|
11
11
|
/**
|
|
12
12
|
* The Core decision authority (`@astram/core`'s `DecisionAuthority`) — the single
|
|
13
13
|
* ADR-0001 decision entry both the global enforcement guard and the engine's
|
|
@@ -21,3 +21,15 @@ exports.SHIELD_DECISION_AUTHORITY = void 0;
|
|
|
21
21
|
*/
|
|
22
22
|
var core_1 = require("@astram/core");
|
|
23
23
|
Object.defineProperty(exports, "SHIELD_DECISION_AUTHORITY", { enumerable: true, get: function () { return core_1.SHIELD_DECISION_AUTHORITY; } });
|
|
24
|
+
/**
|
|
25
|
+
* The record-authorization inputs (`RecordAuthorizationInputs`: pool + app ports + registry) that
|
|
26
|
+
* a FEATURE module provides app-wide for `ShieldModule` to build the request-path decision
|
|
27
|
+
* authority from (security hardening Phase 9 #7). `@astram/identity`'s `IdentityFeatureModule`
|
|
28
|
+
* provides it, built from the same `extraObjects` as its own authorities, so the request path and
|
|
29
|
+
* the identity module can never judge with different registries. Guard never imports identity:
|
|
30
|
+
* this token is the seam.
|
|
31
|
+
*
|
|
32
|
+
* Exactly one source per app: this token, `ShieldModule.forRoot({ recordAuthorization })` or
|
|
33
|
+
* `ShieldModule.forRoot({ decisionAuthority })`. Two of them fail the boot.
|
|
34
|
+
*/
|
|
35
|
+
exports.SHIELD_RECORD_AUTHORIZATION = Symbol('SHIELD_RECORD_AUTHORIZATION');
|
|
@@ -15,8 +15,8 @@ import type { ScopeSubtreeResolver } from '../auth/ports/scope-subtree-resolver.
|
|
|
15
15
|
import type { MembershipChecker } from '../auth/ports/membership-checker.js';
|
|
16
16
|
import type { CoreDecisionAuthority } from '../enforcement/core-seam.js';
|
|
17
17
|
import { type AppConfig } from '../config/app-config.js';
|
|
18
|
-
import { SHIELD_DECISION_AUTHORITY } from './shield-tokens.js';
|
|
19
|
-
export { SHIELD_DECISION_AUTHORITY };
|
|
18
|
+
import { SHIELD_DECISION_AUTHORITY, SHIELD_RECORD_AUTHORIZATION } from './shield-tokens.js';
|
|
19
|
+
export { SHIELD_DECISION_AUTHORITY, SHIELD_RECORD_AUTHORIZATION };
|
|
20
20
|
export type { RecordAuthorizationInputs } from '../authz/wire-record-authorization.js';
|
|
21
21
|
/**
|
|
22
22
|
* Options for {@link ShieldModule.forRoot}. Intentionally minimal (ADR-0023 §7 —
|
|
@@ -49,6 +49,10 @@ export interface ShieldModuleOptions {
|
|
|
49
49
|
*
|
|
50
50
|
* MUTUALLY EXCLUSIVE with {@link decisionAuthority} — supplying BOTH is a misconfig and
|
|
51
51
|
* throws at registration. Supplying NEITHER binds the fail-closed stub (unchanged).
|
|
52
|
+
*
|
|
53
|
+
* An app that imports `IdentityFeatureModule` (`@astram/identity` ≥ 0.7) passes NEITHER: the
|
|
54
|
+
* identity module provides the inputs under {@link SHIELD_RECORD_AUTHORIZATION} (Phase 9 #7),
|
|
55
|
+
* and this option next to it fails the boot (one source of `extraObjects`).
|
|
52
56
|
*/
|
|
53
57
|
readonly recordAuthorization?: RecordAuthorizationInputs;
|
|
54
58
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shield.module.d.ts","sourceRoot":"","sources":["../../src/module/shield.module.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,KAAK,aAAa,EAAiB,MAAM,gBAAgB,CAAC;AAOnF,OAAO,KAAK,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAQvE,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,uCAAuC,CAAC;AACvF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AACnE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oCAAoC,CAAC;AAC3E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oCAAoC,CAAC;AAC1E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAC;AACjE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sCAAsC,CAAC;AAC9E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AACrE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kCAAkC,CAAC;AACvE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mCAAmC,CAAC;AACxE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,6BAA6B,CAAC;AAC7D,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,0CAA0C,CAAC;AACtF,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,yCAAyC,CAAC;AACpF,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qCAAqC,CAAC;AAC7E,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,6BAA6B,CAAC;AAKzE,OAAO,EAAiB,KAAK,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAgBxE,OAAO,EAAE,yBAAyB,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"shield.module.d.ts","sourceRoot":"","sources":["../../src/module/shield.module.ts"],"names":[],"mappings":"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"}
|
|
@@ -40,7 +40,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
40
40
|
})();
|
|
41
41
|
var ShieldModule_1;
|
|
42
42
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
43
|
-
exports.ShieldModule = exports.SHIELD_DECISION_AUTHORITY = void 0;
|
|
43
|
+
exports.ShieldModule = exports.SHIELD_RECORD_AUTHORIZATION = exports.SHIELD_DECISION_AUTHORITY = void 0;
|
|
44
44
|
const common_1 = require("@nestjs/common");
|
|
45
45
|
const node_url_1 = require("node:url");
|
|
46
46
|
const core_seam_js_1 = require("../enforcement/core-seam.js");
|
|
@@ -63,6 +63,7 @@ const tokens_js_1 = require("../auth/tokens.js");
|
|
|
63
63
|
// EnforcementModule → guard). Re-exported here to keep the public import path stable.
|
|
64
64
|
const shield_tokens_js_1 = require("./shield-tokens.js");
|
|
65
65
|
Object.defineProperty(exports, "SHIELD_DECISION_AUTHORITY", { enumerable: true, get: function () { return shield_tokens_js_1.SHIELD_DECISION_AUTHORITY; } });
|
|
66
|
+
Object.defineProperty(exports, "SHIELD_RECORD_AUTHORIZATION", { enumerable: true, get: function () { return shield_tokens_js_1.SHIELD_RECORD_AUTHORIZATION; } });
|
|
66
67
|
/**
|
|
67
68
|
* A `Function`-wrapped dynamic `import()` that survives the CommonJS **build** — the same
|
|
68
69
|
* shim as auth.module.ts's `evalImport`. Under `module: commonjs`, `tsc` would rewrite a
|
|
@@ -105,33 +106,45 @@ async function loadBuildRecordAuthorization() {
|
|
|
105
106
|
}
|
|
106
107
|
/**
|
|
107
108
|
* Resolve the `SHIELD_DECISION_AUTHORITY` VALUE from the caller's options (the ONE seam,
|
|
108
|
-
* ADR-0023 — only how its value is PRODUCED changes here).
|
|
109
|
+
* ADR-0023 — only how its value is PRODUCED changes here). Exactly ONE source, else boot fails:
|
|
109
110
|
*
|
|
110
|
-
* -
|
|
111
|
-
*
|
|
112
|
-
* Phase
|
|
111
|
+
* - {@link SHIELD_RECORD_AUTHORIZATION} PROVIDED (by a feature module — `IdentityFeatureModule`
|
|
112
|
+
* from `@astram/identity`, built from its own `extraObjects`) → the framework builds the REAL
|
|
113
|
+
* wired authority from those inputs (Phase 9 #7: one registry for the request path and the
|
|
114
|
+
* identity module's own authorities).
|
|
115
|
+
* - `recordAuthorization` PRESENT → the same, from inputs the app passes (an app without the
|
|
116
|
+
* identity module). The four record-tier adapters are constructed inside guard either way,
|
|
117
|
+
* un-overridable.
|
|
113
118
|
* - `decisionAuthority` PRESENT (legacy passthrough) → bind the caller's pre-built
|
|
114
119
|
* authority verbatim (kept for other consumers / back-compat).
|
|
115
|
-
* -
|
|
116
|
-
* -
|
|
120
|
+
* - MORE THAN ONE → MISCONFIG: throw a clear error (two sources could disagree).
|
|
121
|
+
* - NONE → the fail-closed {@link stubDecisionAuthority} (enforcement-only apps).
|
|
117
122
|
*
|
|
118
|
-
* Async because the
|
|
123
|
+
* Async because the record-authorization path lazy-loads the (core-touching) builder.
|
|
119
124
|
*/
|
|
120
125
|
async function resolveDecisionAuthority(options, extras = {}) {
|
|
126
|
+
const provided = extras.providedRecordAuthorization ?? undefined;
|
|
127
|
+
if (provided && (options.recordAuthorization || options.decisionAuthority)) {
|
|
128
|
+
throw new Error('ShieldModule: a record authorization is already provided under SHIELD_RECORD_AUTHORIZATION ' +
|
|
129
|
+
'(IdentityFeatureModule.forRoot builds it from its own extraObjects), so ShieldModule.forRoot ' +
|
|
130
|
+
'must not also receive `recordAuthorization` or `decisionAuthority`. Remove that option ' +
|
|
131
|
+
'(since @astram/identity 0.7 the identity module is the one source).');
|
|
132
|
+
}
|
|
121
133
|
if (options.recordAuthorization && options.decisionAuthority) {
|
|
122
134
|
throw new Error('ShieldModule: `recordAuthorization` and `decisionAuthority` are mutually exclusive — ' +
|
|
123
135
|
'supply the record-authorization inputs (pool + app ports + registry) so the framework ' +
|
|
124
136
|
'builds the decision authority, OR a pre-built `decisionAuthority`, but not both.');
|
|
125
137
|
}
|
|
126
|
-
|
|
138
|
+
const recordAuthorization = provided ?? options.recordAuthorization;
|
|
139
|
+
if (recordAuthorization) {
|
|
127
140
|
const api = await loadBuildRecordAuthorization();
|
|
128
141
|
const versionCache = extras.versionCache ?? undefined;
|
|
129
142
|
// ADR-0035 default projector: cache the SAME facts the authority decides on (version-cache
|
|
130
143
|
// wrapped, so a login projection also warms the Redis version).
|
|
131
144
|
extras.defaultProjector?.attach(versionCache !== undefined
|
|
132
|
-
? api.withRedisPermissionVersion(
|
|
133
|
-
:
|
|
134
|
-
return api.buildRecordAuthorization(
|
|
145
|
+
? api.withRedisPermissionVersion(recordAuthorization.permissionFacts, versionCache)
|
|
146
|
+
: recordAuthorization.permissionFacts);
|
|
147
|
+
return api.buildRecordAuthorization(recordAuthorization, versionCache !== undefined ? { versionCache } : {});
|
|
135
148
|
}
|
|
136
149
|
return options.decisionAuthority ?? core_seam_js_1.stubDecisionAuthority;
|
|
137
150
|
}
|
|
@@ -144,6 +157,14 @@ function defaultProjectorFor(auth, explicit) {
|
|
|
144
157
|
}
|
|
145
158
|
/** Inject the Redis version cache when AuthModule provides it (`auth: true`); absent otherwise. */
|
|
146
159
|
const OPTIONAL_VERSION_CACHE = { token: tokens_js_1.SHIELD_PERMISSION_VERSION_CACHE, optional: true };
|
|
160
|
+
/**
|
|
161
|
+
* Phase 9 #7: the record-authorization inputs a feature module provides app-wide (global), e.g.
|
|
162
|
+
* `IdentityFeatureModule`. Optional: an enforcement-only app provides none.
|
|
163
|
+
*/
|
|
164
|
+
const OPTIONAL_RECORD_AUTHORIZATION = {
|
|
165
|
+
token: shield_tokens_js_1.SHIELD_RECORD_AUTHORIZATION,
|
|
166
|
+
optional: true,
|
|
167
|
+
};
|
|
147
168
|
/**
|
|
148
169
|
* `ShieldModule` — the SINGLE registration seam (ADR-0023 §4).
|
|
149
170
|
*
|
|
@@ -238,16 +259,17 @@ let ShieldModule = ShieldModule_1 = class ShieldModule {
|
|
|
238
259
|
// stub paths resolve synchronously but flow through the same one seam. Both
|
|
239
260
|
// supplied → throws (mutual exclusion).
|
|
240
261
|
provide: shield_tokens_js_1.SHIELD_DECISION_AUTHORITY,
|
|
241
|
-
useFactory: (versionCache) => resolveDecisionAuthority({
|
|
262
|
+
useFactory: (versionCache, providedRecordAuthorization) => resolveDecisionAuthority({
|
|
242
263
|
...(options.recordAuthorization
|
|
243
264
|
? { recordAuthorization: options.recordAuthorization }
|
|
244
265
|
: {}),
|
|
245
266
|
...(options.decisionAuthority
|
|
246
267
|
? { decisionAuthority: options.decisionAuthority }
|
|
247
268
|
: {}),
|
|
248
|
-
}, { versionCache, defaultProjector }),
|
|
269
|
+
}, { versionCache, defaultProjector, providedRecordAuthorization }),
|
|
249
270
|
// ADR-0035: the Redis version cache AuthModule provides under `auth: true`.
|
|
250
|
-
|
|
271
|
+
// Phase 9 #7: the record authorization a feature module provides (IdentityFeatureModule).
|
|
272
|
+
inject: [OPTIONAL_VERSION_CACHE, OPTIONAL_RECORD_AUTHORIZATION],
|
|
251
273
|
},
|
|
252
274
|
],
|
|
253
275
|
// Re-export the composed AuthModule app-wide ONLY when auth is opted in — its
|
|
@@ -289,11 +311,11 @@ let ShieldModule = ShieldModule_1 = class ShieldModule {
|
|
|
289
311
|
// in its useFactory from the injected registrations) has the framework build the
|
|
290
312
|
// real wired authority; `decisionAuthority` is the legacy passthrough; both →
|
|
291
313
|
// throws; neither → stub. Async because the record path lazy-loads the builder.
|
|
292
|
-
useFactory: (opts, versionCache) => resolveDecisionAuthority({
|
|
314
|
+
useFactory: (opts, versionCache, providedRecordAuthorization) => resolveDecisionAuthority({
|
|
293
315
|
...(opts.recordAuthorization ? { recordAuthorization: opts.recordAuthorization } : {}),
|
|
294
316
|
...(opts.decisionAuthority ? { decisionAuthority: opts.decisionAuthority } : {}),
|
|
295
|
-
}, { versionCache, defaultProjector }),
|
|
296
|
-
inject: [optionsToken, OPTIONAL_VERSION_CACHE],
|
|
317
|
+
}, { versionCache, defaultProjector, providedRecordAuthorization }),
|
|
318
|
+
inject: [optionsToken, OPTIONAL_VERSION_CACHE, OPTIONAL_RECORD_AUTHORIZATION],
|
|
297
319
|
};
|
|
298
320
|
// Same one-instance opt-in AuthModule as forRoot (imported + re-exported).
|
|
299
321
|
// `credentialResolver` is STATIC on the async options (like `auth`) — a
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astram/guard",
|
|
3
|
-
"version": "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",
|
|
@@ -34,9 +34,9 @@
|
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"argon2": "^0.41.1",
|
|
36
36
|
"ioredis": "^5.4.1",
|
|
37
|
-
"mariadb": "^3.4
|
|
37
|
+
"mariadb": "^3.5.4",
|
|
38
38
|
"@astram/core": "1.4.0",
|
|
39
|
-
"@astram/engine": "0.5.
|
|
39
|
+
"@astram/engine": "0.5.1"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
42
|
"@nestjs/common": "^11",
|