@c9up/warden 0.1.3 → 0.1.5
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/README.md +57 -0
- package/dist/AuthManager.d.ts +98 -0
- package/dist/AuthManager.d.ts.map +1 -0
- package/dist/AuthManager.js +147 -0
- package/dist/AuthManager.js.map +1 -0
- package/dist/AuthRateLimiter.d.ts +29 -0
- package/dist/AuthRateLimiter.d.ts.map +1 -0
- package/dist/AuthRateLimiter.js +88 -0
- package/dist/AuthRateLimiter.js.map +1 -0
- package/dist/Guard.d.ts +35 -0
- package/dist/Guard.d.ts.map +1 -0
- package/dist/Guard.js +68 -0
- package/dist/Guard.js.map +1 -0
- package/dist/RedisBlacklistDriver.d.ts +32 -0
- package/dist/RedisBlacklistDriver.d.ts.map +1 -0
- package/dist/RedisBlacklistDriver.js +37 -0
- package/dist/RedisBlacklistDriver.js.map +1 -0
- package/dist/RefreshTokenStore.d.ts +34 -0
- package/dist/RefreshTokenStore.d.ts.map +1 -0
- package/dist/RefreshTokenStore.js +48 -0
- package/dist/RefreshTokenStore.js.map +1 -0
- package/dist/ResilientBlacklistDriver.d.ts +35 -0
- package/dist/ResilientBlacklistDriver.d.ts.map +1 -0
- package/dist/ResilientBlacklistDriver.js +61 -0
- package/dist/ResilientBlacklistDriver.js.map +1 -0
- package/dist/TokenBlacklist.d.ts +30 -0
- package/dist/TokenBlacklist.d.ts.map +1 -0
- package/dist/TokenBlacklist.js +50 -0
- package/dist/TokenBlacklist.js.map +1 -0
- package/dist/WardenProvider.d.ts +19 -0
- package/dist/WardenProvider.d.ts.map +1 -0
- package/dist/WardenProvider.js +71 -0
- package/dist/WardenProvider.js.map +1 -0
- package/dist/bouncer/AuthorizationResponse.d.ts +25 -0
- package/dist/bouncer/AuthorizationResponse.d.ts.map +1 -0
- package/dist/bouncer/AuthorizationResponse.js +31 -0
- package/dist/bouncer/AuthorizationResponse.js.map +1 -0
- package/dist/bouncer/BasePolicy.d.ts +47 -0
- package/dist/bouncer/BasePolicy.d.ts.map +1 -0
- package/dist/bouncer/BasePolicy.js +36 -0
- package/dist/bouncer/BasePolicy.js.map +1 -0
- package/dist/bouncer/Bouncer.d.ts +47 -0
- package/dist/bouncer/Bouncer.d.ts.map +1 -0
- package/dist/bouncer/Bouncer.js +134 -0
- package/dist/bouncer/Bouncer.js.map +1 -0
- package/dist/bouncer/PolicyAuthorizer.d.ts +24 -0
- package/dist/bouncer/PolicyAuthorizer.d.ts.map +1 -0
- package/dist/bouncer/PolicyAuthorizer.js +92 -0
- package/dist/bouncer/PolicyAuthorizer.js.map +1 -0
- package/dist/bouncer/decorators.d.ts +25 -0
- package/dist/bouncer/decorators.d.ts.map +1 -0
- package/dist/bouncer/decorators.js +36 -0
- package/dist/bouncer/decorators.js.map +1 -0
- package/dist/bouncer/evaluate.d.ts +36 -0
- package/dist/bouncer/evaluate.d.ts.map +1 -0
- package/dist/bouncer/evaluate.js +64 -0
- package/dist/bouncer/evaluate.js.map +1 -0
- package/dist/bouncer/policyContext.d.ts +29 -0
- package/dist/bouncer/policyContext.d.ts.map +1 -0
- package/dist/bouncer/policyContext.js +36 -0
- package/dist/bouncer/policyContext.js.map +1 -0
- package/dist/bouncer/types.d.ts +52 -0
- package/dist/bouncer/types.d.ts.map +1 -0
- package/dist/bouncer/types.js +9 -0
- package/dist/bouncer/types.js.map +1 -0
- package/dist/config.d.ts +51 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +21 -0
- package/dist/config.js.map +1 -0
- package/dist/configure.d.ts +10 -0
- package/dist/configure.d.ts.map +1 -0
- package/dist/configure.js +44 -0
- package/dist/configure.js.map +1 -0
- package/dist/errors.d.ts +18 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +21 -0
- package/dist/errors.js.map +1 -0
- package/dist/firstcontact/FirstContactManager.d.ts +25 -0
- package/dist/firstcontact/FirstContactManager.d.ts.map +1 -0
- package/dist/firstcontact/FirstContactManager.js +38 -0
- package/dist/firstcontact/FirstContactManager.js.map +1 -0
- package/dist/firstcontact/drivers/GitHubDriver.d.ts +14 -0
- package/dist/firstcontact/drivers/GitHubDriver.d.ts.map +1 -0
- package/dist/firstcontact/drivers/GitHubDriver.js +61 -0
- package/dist/firstcontact/drivers/GitHubDriver.js.map +1 -0
- package/dist/firstcontact/drivers/GoogleDriver.d.ts +14 -0
- package/dist/firstcontact/drivers/GoogleDriver.d.ts.map +1 -0
- package/dist/firstcontact/drivers/GoogleDriver.js +64 -0
- package/dist/firstcontact/drivers/GoogleDriver.js.map +1 -0
- package/dist/firstcontact/types.d.ts +34 -0
- package/dist/firstcontact/types.d.ts.map +1 -0
- package/dist/firstcontact/types.js +5 -0
- package/dist/firstcontact/types.js.map +1 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +34 -0
- package/dist/index.js.map +1 -0
- package/dist/mfa/BackupCodesProvider.d.ts +39 -0
- package/dist/mfa/BackupCodesProvider.d.ts.map +1 -0
- package/dist/mfa/BackupCodesProvider.js +87 -0
- package/dist/mfa/BackupCodesProvider.js.map +1 -0
- package/dist/mfa/MfaManager.d.ts +107 -0
- package/dist/mfa/MfaManager.d.ts.map +1 -0
- package/dist/mfa/MfaManager.js +219 -0
- package/dist/mfa/MfaManager.js.map +1 -0
- package/dist/mfa/OtpProvider.d.ts +67 -0
- package/dist/mfa/OtpProvider.d.ts.map +1 -0
- package/dist/mfa/OtpProvider.js +104 -0
- package/dist/mfa/OtpProvider.js.map +1 -0
- package/dist/mfa/TotpProvider.d.ts +52 -0
- package/dist/mfa/TotpProvider.d.ts.map +1 -0
- package/dist/mfa/TotpProvider.js +103 -0
- package/dist/mfa/TotpProvider.js.map +1 -0
- package/dist/mfa/WebauthnProvider.d.ts +175 -0
- package/dist/mfa/WebauthnProvider.d.ts.map +1 -0
- package/dist/mfa/WebauthnProvider.js +239 -0
- package/dist/mfa/WebauthnProvider.js.map +1 -0
- package/dist/mfa/base32.d.ts +13 -0
- package/dist/mfa/base32.d.ts.map +1 -0
- package/dist/mfa/base32.js +52 -0
- package/dist/mfa/base32.js.map +1 -0
- package/dist/mfa/webauthn-codec.d.ts +58 -0
- package/dist/mfa/webauthn-codec.d.ts.map +1 -0
- package/dist/mfa/webauthn-codec.js +221 -0
- package/dist/mfa/webauthn-codec.js.map +1 -0
- package/dist/middleware.d.ts +51 -0
- package/dist/middleware.d.ts.map +1 -0
- package/dist/middleware.js +213 -0
- package/dist/middleware.js.map +1 -0
- package/dist/native.d.ts +14 -0
- package/dist/native.d.ts.map +1 -0
- package/dist/native.js +39 -0
- package/dist/native.js.map +1 -0
- package/dist/rights/MemoryRightsStore.d.ts +29 -0
- package/dist/rights/MemoryRightsStore.d.ts.map +1 -0
- package/dist/rights/MemoryRightsStore.js +70 -0
- package/dist/rights/MemoryRightsStore.js.map +1 -0
- package/dist/rights/RightsResolver.d.ts +29 -0
- package/dist/rights/RightsResolver.d.ts.map +1 -0
- package/dist/rights/RightsResolver.js +73 -0
- package/dist/rights/RightsResolver.js.map +1 -0
- package/dist/rights/types.d.ts +55 -0
- package/dist/rights/types.d.ts.map +1 -0
- package/dist/rights/types.js +18 -0
- package/dist/rights/types.js.map +1 -0
- package/dist/services/main.d.ts +19 -0
- package/dist/services/main.d.ts.map +1 -0
- package/dist/services/main.js +32 -0
- package/dist/services/main.js.map +1 -0
- package/dist/standalone.d.ts +37 -0
- package/dist/standalone.d.ts.map +1 -0
- package/dist/standalone.js +92 -0
- package/dist/standalone.js.map +1 -0
- package/dist/strategies/ApiKeyStrategy.d.ts +28 -0
- package/dist/strategies/ApiKeyStrategy.d.ts.map +1 -0
- package/dist/strategies/ApiKeyStrategy.js +36 -0
- package/dist/strategies/ApiKeyStrategy.js.map +1 -0
- package/dist/strategies/JwtStrategy.d.ts +51 -0
- package/dist/strategies/JwtStrategy.d.ts.map +1 -0
- package/dist/strategies/JwtStrategy.js +150 -0
- package/dist/strategies/JwtStrategy.js.map +1 -0
- package/dist/strategies/SessionStrategy.d.ts +58 -0
- package/dist/strategies/SessionStrategy.d.ts.map +1 -0
- package/dist/strategies/SessionStrategy.js +70 -0
- package/dist/strategies/SessionStrategy.js.map +1 -0
- package/index.darwin-arm64.node +0 -0
- package/index.darwin-x64.node +0 -0
- package/index.linux-arm64-gnu.node +0 -0
- package/index.linux-x64-gnu.node +0 -0
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +1 -1
- package/src/Guard.ts +21 -0
- package/src/RedisBlacklistDriver.ts +60 -0
- package/src/ResilientBlacklistDriver.ts +84 -0
- package/src/WardenProvider.ts +11 -0
- package/src/config.ts +7 -0
- package/src/index.ts +58 -0
- package/src/mfa/BackupCodesProvider.ts +125 -0
- package/src/mfa/MfaManager.ts +307 -0
- package/src/mfa/OtpProvider.ts +177 -0
- package/src/mfa/TotpProvider.ts +140 -0
- package/src/mfa/WebauthnProvider.ts +416 -0
- package/src/mfa/base32.ts +54 -0
- package/src/mfa/webauthn-codec.ts +264 -0
- package/src/middleware.ts +14 -0
package/README.md
CHANGED
|
@@ -29,6 +29,63 @@ class OrderController {
|
|
|
29
29
|
- RBAC: `hasRole`, `hasPermission`, `hasAllPermissions`
|
|
30
30
|
- Strategy exception safety (catch → AuthResult)
|
|
31
31
|
- Runtime strategy registration
|
|
32
|
+
- **Multi-factor authentication** — TOTP, backup codes, email/SMS OTP, WebAuthn/passkeys, all dependency-free
|
|
33
|
+
- JWT revocation blacklist (Memory / Redis-KeyDB / resilient fallback drivers)
|
|
34
|
+
|
|
35
|
+
## Multi-factor authentication
|
|
36
|
+
|
|
37
|
+
Four providers, all implemented on `node:crypto` with **zero third-party dependency**
|
|
38
|
+
(WebAuthn included). Every provider and the manager take pluggable stores, defaulting
|
|
39
|
+
to in-memory — supply persistent stores (Atlas, KeyDB…) in production.
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
import {
|
|
43
|
+
MfaManager,
|
|
44
|
+
TotpProvider,
|
|
45
|
+
BackupCodesProvider,
|
|
46
|
+
RequireMfa,
|
|
47
|
+
} from '@c9up/warden'
|
|
48
|
+
|
|
49
|
+
const mfa = new MfaManager({
|
|
50
|
+
issuer: 'Fluveo',
|
|
51
|
+
totp: new TotpProvider(),
|
|
52
|
+
backupCodes: new BackupCodesProvider(),
|
|
53
|
+
// store: new AtlasMfaFactorStore(db), // persistent factors in production
|
|
54
|
+
rateLimit: { maxAttempts: 5, windowSeconds: 900 }, // per-user brute-force lock
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
// Enroll (returns the otpauth:// URI to render as a QR code)
|
|
58
|
+
const { factorId, uri } = await mfa.enrollTotp({ id: user.id, name: user.email })
|
|
59
|
+
await mfa.confirmTotp(factorId, codeFromAuthenticatorApp) // → true once confirmed
|
|
60
|
+
const recoveryCodes = await mfa.createBackupCodes(user.id) // show once
|
|
61
|
+
|
|
62
|
+
// At sign-in step-up: TOTP code OR a backup code (rate-limited)
|
|
63
|
+
if (await mfa.verify(user.id, submittedCode)) {
|
|
64
|
+
// issue a JWT carrying `mfa: true`
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Gate sensitive routes — the middleware returns `403 MFA_REQUIRED` unless the
|
|
69
|
+
authenticated user's payload carries a truthy `mfa` claim:
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
class TransferController {
|
|
73
|
+
@Guard('jwt')
|
|
74
|
+
@RequireMfa()
|
|
75
|
+
async transfer() { /* only reachable after MFA step-up */ }
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Wire the manager into the container via config:
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
// config/auth.ts
|
|
83
|
+
import { defineConfig } from '@c9up/warden/config'
|
|
84
|
+
export default defineConfig({ jwt: { /* … */ }, mfa: { manager: mfa } })
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Email/SMS OTP (`OtpProvider`) and WebAuthn/passkeys (`WebauthnProvider`) are used
|
|
88
|
+
directly — OTP needs a delivery channel, WebAuthn persists passkeys in its own store.
|
|
32
89
|
|
|
33
90
|
## License
|
|
34
91
|
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AuthManager — manages multiple authentication strategies.
|
|
3
|
+
*
|
|
4
|
+
* @implements FR48, FR50, FR51
|
|
5
|
+
*/
|
|
6
|
+
import { RightsResolver } from "./rights/RightsResolver.js";
|
|
7
|
+
import type { EffectivePermissions, Scope } from "./rights/types.js";
|
|
8
|
+
export interface UserPayload {
|
|
9
|
+
id: string;
|
|
10
|
+
roles?: string[];
|
|
11
|
+
permissions?: string[];
|
|
12
|
+
[key: string]: unknown;
|
|
13
|
+
}
|
|
14
|
+
export interface AuthResult {
|
|
15
|
+
authenticated: boolean;
|
|
16
|
+
user?: UserPayload;
|
|
17
|
+
error?: string;
|
|
18
|
+
/**
|
|
19
|
+
* @internal Set when the strategy's `verify()` / `authenticate()` THREW
|
|
20
|
+
* a generic error (vs returned a deliberate `{ authenticated: false }`
|
|
21
|
+
* rejection). The middleware uses this to distinguish credential
|
|
22
|
+
* failures (→ 401) from strategy crashes (→ 500). Not part of the
|
|
23
|
+
* public consumer-facing API.
|
|
24
|
+
*/
|
|
25
|
+
strategyCrash?: true;
|
|
26
|
+
}
|
|
27
|
+
export interface AuthStrategy {
|
|
28
|
+
name: string;
|
|
29
|
+
authenticate(credentials: Record<string, unknown>): Promise<AuthResult>;
|
|
30
|
+
verify(token: string, context?: Record<string, unknown>): Promise<AuthResult>;
|
|
31
|
+
}
|
|
32
|
+
export interface AuthConfig {
|
|
33
|
+
defaultStrategy: string;
|
|
34
|
+
strategies: Record<string, AuthStrategy>;
|
|
35
|
+
/**
|
|
36
|
+
* The rights resolver backing the coarse RBAC helpers (Epic 56). When
|
|
37
|
+
* absent, a default `RightsResolver(new MemoryRightsStore())` is used so
|
|
38
|
+
* `new AuthManager({ defaultStrategy, strategies })` keeps working — with
|
|
39
|
+
* an empty store, payload roles still fold in (D2) and permissions are
|
|
40
|
+
* empty (token `user.permissions` is not an input — D1).
|
|
41
|
+
*/
|
|
42
|
+
rights?: RightsResolver;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Manages authentication strategies and provides guard/permission checks.
|
|
46
|
+
*/
|
|
47
|
+
export declare class AuthManager {
|
|
48
|
+
private strategies;
|
|
49
|
+
private defaultStrategy;
|
|
50
|
+
private readonly rights;
|
|
51
|
+
constructor(config: AuthConfig);
|
|
52
|
+
/** Authenticate with credentials using a specific or default strategy. */
|
|
53
|
+
authenticate(credentials: Record<string, unknown>, strategyName?: string): Promise<AuthResult>;
|
|
54
|
+
/** Verify a token (JWT, session, API key). */
|
|
55
|
+
verify(token: string, strategyName?: string, context?: Record<string, unknown>): Promise<AuthResult>;
|
|
56
|
+
/**
|
|
57
|
+
* Resolve a user's effective permissions for a scope (Epic 56). The single
|
|
58
|
+
* resolution entry the coarse helpers wrap — `hasRole`/`hasPermission`/
|
|
59
|
+
* `hasAllPermissions` read this set, never the token payload directly. The
|
|
60
|
+
* Bouncer policy path (`this.permissions`, 56.3) consults the SAME resolver,
|
|
61
|
+
* so a coarse question and a policy question return the same answer for the
|
|
62
|
+
* same `(user, scope)` (the single unification point — AC-E3).
|
|
63
|
+
*/
|
|
64
|
+
resolvePermissions(user: UserPayload, scope?: Scope): Promise<EffectivePermissions>;
|
|
65
|
+
/**
|
|
66
|
+
* Check if a user has a specific role within a scope (default `"global"`).
|
|
67
|
+
* Reflects payload roles ∪ store roles (D2) — a token-carried role still
|
|
68
|
+
* satisfies it with no store config.
|
|
69
|
+
*/
|
|
70
|
+
hasRole(user: UserPayload, role: string, scope?: Scope): Promise<boolean>;
|
|
71
|
+
/**
|
|
72
|
+
* Check if a user has a specific permission within a scope (default
|
|
73
|
+
* `"global"`). Derived from roles + store grants ONLY — token
|
|
74
|
+
* `user.permissions` is NOT read (D1, cerebrum 459).
|
|
75
|
+
*/
|
|
76
|
+
hasPermission(user: UserPayload, permission: string, scope?: Scope): Promise<boolean>;
|
|
77
|
+
/**
|
|
78
|
+
* Check if a user has ALL required permissions within a scope (default
|
|
79
|
+
* `"global"`). Empty list ⇒ vacuously true.
|
|
80
|
+
*/
|
|
81
|
+
hasAllPermissions(user: UserPayload, permissions: string[], scope?: Scope): Promise<boolean>;
|
|
82
|
+
/** Get a registered strategy by name. */
|
|
83
|
+
getStrategy(name?: string): AuthStrategy;
|
|
84
|
+
/** Register a new strategy at runtime. */
|
|
85
|
+
registerStrategy(name: string, strategy: AuthStrategy): void;
|
|
86
|
+
/** Get all registered strategy names. */
|
|
87
|
+
getStrategyNames(): string[];
|
|
88
|
+
}
|
|
89
|
+
/** Strip dangerous prototype-pollution keys from user payload. */
|
|
90
|
+
/**
|
|
91
|
+
* Strip prototype-pollution keys from a user payload before it's
|
|
92
|
+
* attached to the request. Exported so the middleware can apply it to
|
|
93
|
+
* the session path too — session auth goes through `verifyWithContext`
|
|
94
|
+
* directly (not `AuthManager.verify`), so without this it would skip
|
|
95
|
+
* the guard JWT / api-key users get.
|
|
96
|
+
*/
|
|
97
|
+
export declare function sanitizePayload(user: UserPayload): void;
|
|
98
|
+
//# sourceMappingURL=AuthManager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AuthManager.d.ts","sourceRoot":"","sources":["../src/AuthManager.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAC5D,OAAO,KAAK,EAAE,oBAAoB,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAErE,MAAM,WAAW,WAAW;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACvB;AAED,MAAM,WAAW,UAAU;IAC1B,aAAa,EAAE,OAAO,CAAC;IACvB,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;;;OAMG;IACH,aAAa,CAAC,EAAE,IAAI,CAAC;CACrB;AAED,MAAM,WAAW,YAAY;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,CAAC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IACxE,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;CAC9E;AAED,MAAM,WAAW,UAAU;IAC1B,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IACzC;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,cAAc,CAAC;CACxB;AAED;;GAEG;AACH,qBAAa,WAAW;IACvB,OAAO,CAAC,UAAU,CAAwC;IAC1D,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAiB;gBAE5B,MAAM,EAAE,UAAU;IA0B9B,0EAA0E;IACpE,YAAY,CACjB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACpC,YAAY,CAAC,EAAE,MAAM,GACnB,OAAO,CAAC,UAAU,CAAC;IAmBtB,8CAA8C;IACxC,MAAM,CACX,KAAK,EAAE,MAAM,EACb,YAAY,CAAC,EAAE,MAAM,EACrB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC/B,OAAO,CAAC,UAAU,CAAC;IAoBtB;;;;;;;OAOG;IACH,kBAAkB,CACjB,IAAI,EAAE,WAAW,EACjB,KAAK,GAAE,KAAgB,GACrB,OAAO,CAAC,oBAAoB,CAAC;IAIhC;;;;OAIG;IACG,OAAO,CACZ,IAAI,EAAE,WAAW,EACjB,IAAI,EAAE,MAAM,EACZ,KAAK,GAAE,KAAgB,GACrB,OAAO,CAAC,OAAO,CAAC;IAInB;;;;OAIG;IACG,aAAa,CAClB,IAAI,EAAE,WAAW,EACjB,UAAU,EAAE,MAAM,EAClB,KAAK,GAAE,KAAgB,GACrB,OAAO,CAAC,OAAO,CAAC;IAInB;;;OAGG;IACG,iBAAiB,CACtB,IAAI,EAAE,WAAW,EACjB,WAAW,EAAE,MAAM,EAAE,EACrB,KAAK,GAAE,KAAgB,GACrB,OAAO,CAAC,OAAO,CAAC;IAInB,yCAAyC;IACzC,WAAW,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,YAAY;IAexC,0CAA0C;IAC1C,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,YAAY,GAAG,IAAI;IAI5D,yCAAyC;IACzC,gBAAgB,IAAI,MAAM,EAAE;CAG5B;AAED,kEAAkE;AAClE;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,CAMvD"}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AuthManager — manages multiple authentication strategies.
|
|
3
|
+
*
|
|
4
|
+
* @implements FR48, FR50, FR51
|
|
5
|
+
*/
|
|
6
|
+
import { WardenError } from "./errors.js";
|
|
7
|
+
import { MemoryRightsStore } from "./rights/MemoryRightsStore.js";
|
|
8
|
+
import { RightsResolver } from "./rights/RightsResolver.js";
|
|
9
|
+
/**
|
|
10
|
+
* Manages authentication strategies and provides guard/permission checks.
|
|
11
|
+
*/
|
|
12
|
+
export class AuthManager {
|
|
13
|
+
strategies = new Map();
|
|
14
|
+
defaultStrategy;
|
|
15
|
+
rights;
|
|
16
|
+
constructor(config) {
|
|
17
|
+
this.defaultStrategy = config.defaultStrategy;
|
|
18
|
+
this.rights = config.rights ?? new RightsResolver(new MemoryRightsStore());
|
|
19
|
+
for (const [name, strategy] of Object.entries(config.strategies)) {
|
|
20
|
+
this.strategies.set(name, strategy);
|
|
21
|
+
}
|
|
22
|
+
// Fail-fast at construction: an AuthManager with zero strategies
|
|
23
|
+
// is a configuration bug. Previously, an empty `strategies: {}`
|
|
24
|
+
// passed the constructor cleanly and the first protected request
|
|
25
|
+
// crashed at runtime when `getStrategy('jwt')` threw — opaque 401
|
|
26
|
+
// or 500 instead of a boot-time INVALID_CONFIG that points the
|
|
27
|
+
// operator at the missing `config.auth.jwt` (or other strategy).
|
|
28
|
+
if (Object.keys(config.strategies).length === 0) {
|
|
29
|
+
throw new WardenError("INVALID_CONFIG", `AuthManager: no authentication strategies registered. Configure at least one strategy (e.g. config.warden.auth.jwt) before booting WardenProvider.`);
|
|
30
|
+
}
|
|
31
|
+
if (!this.strategies.has(config.defaultStrategy)) {
|
|
32
|
+
throw new WardenError("INVALID_CONFIG", `defaultStrategy '${config.defaultStrategy}' is not present in strategies`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** Authenticate with credentials using a specific or default strategy. */
|
|
36
|
+
async authenticate(credentials, strategyName) {
|
|
37
|
+
const strategy = this.getStrategy(strategyName);
|
|
38
|
+
try {
|
|
39
|
+
const result = await strategy.authenticate(credentials);
|
|
40
|
+
if (result.user)
|
|
41
|
+
sanitizePayload(result.user);
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
// Re-throw structured WardenError sentinels (e.g. SessionStrategy's
|
|
46
|
+
// USE_LOGIN throw) so callers see the design-boundary signal instead
|
|
47
|
+
// of a soft `{ authenticated: false }`. Generic errors stay soft.
|
|
48
|
+
if (err instanceof WardenError)
|
|
49
|
+
throw err;
|
|
50
|
+
return {
|
|
51
|
+
authenticated: false,
|
|
52
|
+
error: err instanceof Error ? err.message : "Unknown authentication error",
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** Verify a token (JWT, session, API key). */
|
|
57
|
+
async verify(token, strategyName, context) {
|
|
58
|
+
const strategy = this.getStrategy(strategyName);
|
|
59
|
+
try {
|
|
60
|
+
const result = await strategy.verify(token, context);
|
|
61
|
+
if (result.user)
|
|
62
|
+
sanitizePayload(result.user);
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
// Mirror authenticate(): rethrow structured WardenError sentinels;
|
|
67
|
+
// soft-fail generic throws but TAG them as strategy crashes so the
|
|
68
|
+
// middleware can flip 401 → 500 when every attempted strategy bombed.
|
|
69
|
+
if (err instanceof WardenError)
|
|
70
|
+
throw err;
|
|
71
|
+
return {
|
|
72
|
+
authenticated: false,
|
|
73
|
+
error: err instanceof Error ? err.message : "Unknown verification error",
|
|
74
|
+
strategyCrash: true,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Resolve a user's effective permissions for a scope (Epic 56). The single
|
|
80
|
+
* resolution entry the coarse helpers wrap — `hasRole`/`hasPermission`/
|
|
81
|
+
* `hasAllPermissions` read this set, never the token payload directly. The
|
|
82
|
+
* Bouncer policy path (`this.permissions`, 56.3) consults the SAME resolver,
|
|
83
|
+
* so a coarse question and a policy question return the same answer for the
|
|
84
|
+
* same `(user, scope)` (the single unification point — AC-E3).
|
|
85
|
+
*/
|
|
86
|
+
resolvePermissions(user, scope = "global") {
|
|
87
|
+
return this.rights.resolve(user, scope);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Check if a user has a specific role within a scope (default `"global"`).
|
|
91
|
+
* Reflects payload roles ∪ store roles (D2) — a token-carried role still
|
|
92
|
+
* satisfies it with no store config.
|
|
93
|
+
*/
|
|
94
|
+
async hasRole(user, role, scope = "global") {
|
|
95
|
+
return (await this.resolvePermissions(user, scope)).roles.has(role);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Check if a user has a specific permission within a scope (default
|
|
99
|
+
* `"global"`). Derived from roles + store grants ONLY — token
|
|
100
|
+
* `user.permissions` is NOT read (D1, cerebrum 459).
|
|
101
|
+
*/
|
|
102
|
+
async hasPermission(user, permission, scope = "global") {
|
|
103
|
+
return (await this.resolvePermissions(user, scope)).has(permission);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Check if a user has ALL required permissions within a scope (default
|
|
107
|
+
* `"global"`). Empty list ⇒ vacuously true.
|
|
108
|
+
*/
|
|
109
|
+
async hasAllPermissions(user, permissions, scope = "global") {
|
|
110
|
+
return (await this.resolvePermissions(user, scope)).hasAll(permissions);
|
|
111
|
+
}
|
|
112
|
+
/** Get a registered strategy by name. */
|
|
113
|
+
getStrategy(name) {
|
|
114
|
+
const strategyName = name ?? this.defaultStrategy;
|
|
115
|
+
const strategy = this.strategies.get(strategyName);
|
|
116
|
+
if (!strategy) {
|
|
117
|
+
throw new WardenError("STRATEGY_NOT_FOUND", `Auth strategy '${strategyName}' not registered`, {
|
|
118
|
+
hint: "Call registerStrategy() before using this strategy name.",
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return strategy;
|
|
122
|
+
}
|
|
123
|
+
/** Register a new strategy at runtime. */
|
|
124
|
+
registerStrategy(name, strategy) {
|
|
125
|
+
this.strategies.set(name, strategy);
|
|
126
|
+
}
|
|
127
|
+
/** Get all registered strategy names. */
|
|
128
|
+
getStrategyNames() {
|
|
129
|
+
return [...this.strategies.keys()];
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/** Strip dangerous prototype-pollution keys from user payload. */
|
|
133
|
+
/**
|
|
134
|
+
* Strip prototype-pollution keys from a user payload before it's
|
|
135
|
+
* attached to the request. Exported so the middleware can apply it to
|
|
136
|
+
* the session path too — session auth goes through `verifyWithContext`
|
|
137
|
+
* directly (not `AuthManager.verify`), so without this it would skip
|
|
138
|
+
* the guard JWT / api-key users get.
|
|
139
|
+
*/
|
|
140
|
+
export function sanitizePayload(user) {
|
|
141
|
+
for (const key of ["__proto__", "constructor", "prototype"]) {
|
|
142
|
+
if (key in user) {
|
|
143
|
+
delete user[key];
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
//# sourceMappingURL=AuthManager.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AuthManager.js","sourceRoot":"","sources":["../src/AuthManager.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AuthRateLimiter — brute force protection for login endpoints.
|
|
3
|
+
*
|
|
4
|
+
* Dual-key rate limiting: IP + email to prevent distributed attacks.
|
|
5
|
+
*
|
|
6
|
+
* @implements MISS-26
|
|
7
|
+
*/
|
|
8
|
+
export interface AuthRateLimiterConfig {
|
|
9
|
+
maxAttempts?: number;
|
|
10
|
+
windowSeconds?: number;
|
|
11
|
+
}
|
|
12
|
+
export declare class AuthRateLimiter {
|
|
13
|
+
private maxAttempts;
|
|
14
|
+
private window;
|
|
15
|
+
private store;
|
|
16
|
+
private maxStoreSize;
|
|
17
|
+
constructor(config?: AuthRateLimiterConfig);
|
|
18
|
+
/** Check if an attempt is allowed. Returns false if rate limited. */
|
|
19
|
+
check(ip: string, identifier: string): boolean;
|
|
20
|
+
/** Record a failed attempt. */
|
|
21
|
+
recordFailure(ip: string, identifier: string): void;
|
|
22
|
+
/** Reset counters on successful login. */
|
|
23
|
+
recordSuccess(ip: string, identifier: string): void;
|
|
24
|
+
/** Get remaining attempts for an identifier. */
|
|
25
|
+
remaining(ip: string, identifier: string): number;
|
|
26
|
+
private checkKey;
|
|
27
|
+
private increment;
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=AuthRateLimiter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AuthRateLimiter.d.ts","sourceRoot":"","sources":["../src/AuthRateLimiter.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,WAAW,qBAAqB;IACrC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;CACvB;AAOD,qBAAa,eAAe;IAC3B,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,KAAK,CAAwC;IACrD,OAAO,CAAC,YAAY,CAAW;gBAEnB,MAAM,CAAC,EAAE,qBAAqB;IAO1C,qEAAqE;IACrE,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO;IAW9C,+BAA+B;IAC/B,aAAa,CAAC,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI;IAQnD,0CAA0C;IAC1C,aAAa,CAAC,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI;IAOnD,gDAAgD;IAChD,SAAS,CAAC,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM;IAcjD,OAAO,CAAC,QAAQ;IAMhB,OAAO,CAAC,SAAS;CAejB"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AuthRateLimiter — brute force protection for login endpoints.
|
|
3
|
+
*
|
|
4
|
+
* Dual-key rate limiting: IP + email to prevent distributed attacks.
|
|
5
|
+
*
|
|
6
|
+
* @implements MISS-26
|
|
7
|
+
*/
|
|
8
|
+
export class AuthRateLimiter {
|
|
9
|
+
maxAttempts;
|
|
10
|
+
window;
|
|
11
|
+
store = new Map();
|
|
12
|
+
maxStoreSize = 100_000;
|
|
13
|
+
constructor(config) {
|
|
14
|
+
this.maxAttempts = config?.maxAttempts ?? 5;
|
|
15
|
+
if (this.maxAttempts <= 0)
|
|
16
|
+
this.maxAttempts = 1;
|
|
17
|
+
this.window = (config?.windowSeconds ?? 900) * 1000;
|
|
18
|
+
if (this.window <= 0)
|
|
19
|
+
this.window = 900_000; // prevent bypass via 0/negative
|
|
20
|
+
}
|
|
21
|
+
/** Check if an attempt is allowed. Returns false if rate limited. */
|
|
22
|
+
check(ip, identifier) {
|
|
23
|
+
const now = Date.now();
|
|
24
|
+
const ipNorm = normalizeIp(ip);
|
|
25
|
+
const identifierNorm = normalizeIdentifier(identifier);
|
|
26
|
+
// Dual key: limit both by IP and by identifier (email)
|
|
27
|
+
const ipKey = `ip:${ipNorm}`;
|
|
28
|
+
const idKey = `id:${identifierNorm}`;
|
|
29
|
+
return this.checkKey(ipKey, now) && this.checkKey(idKey, now);
|
|
30
|
+
}
|
|
31
|
+
/** Record a failed attempt. */
|
|
32
|
+
recordFailure(ip, identifier) {
|
|
33
|
+
const now = Date.now();
|
|
34
|
+
const ipNorm = normalizeIp(ip);
|
|
35
|
+
const identifierNorm = normalizeIdentifier(identifier);
|
|
36
|
+
this.increment(`ip:${ipNorm}`, now);
|
|
37
|
+
this.increment(`id:${identifierNorm}`, now);
|
|
38
|
+
}
|
|
39
|
+
/** Reset counters on successful login. */
|
|
40
|
+
recordSuccess(ip, identifier) {
|
|
41
|
+
const ipNorm = normalizeIp(ip);
|
|
42
|
+
const identifierNorm = normalizeIdentifier(identifier);
|
|
43
|
+
this.store.delete(`ip:${ipNorm}`);
|
|
44
|
+
this.store.delete(`id:${identifierNorm}`);
|
|
45
|
+
}
|
|
46
|
+
/** Get remaining attempts for an identifier. */
|
|
47
|
+
remaining(ip, identifier) {
|
|
48
|
+
const ipNorm = normalizeIp(ip);
|
|
49
|
+
const identifierNorm = normalizeIdentifier(identifier);
|
|
50
|
+
const ipEntry = this.store.get(`ip:${ipNorm}`);
|
|
51
|
+
const idEntry = this.store.get(`id:${identifierNorm}`);
|
|
52
|
+
const ipRemaining = ipEntry
|
|
53
|
+
? Math.max(0, this.maxAttempts - ipEntry.count)
|
|
54
|
+
: this.maxAttempts;
|
|
55
|
+
const idRemaining = idEntry
|
|
56
|
+
? Math.max(0, this.maxAttempts - idEntry.count)
|
|
57
|
+
: this.maxAttempts;
|
|
58
|
+
return Math.min(ipRemaining, idRemaining);
|
|
59
|
+
}
|
|
60
|
+
checkKey(key, now) {
|
|
61
|
+
const entry = this.store.get(key);
|
|
62
|
+
if (!entry || entry.resetAt < now)
|
|
63
|
+
return true;
|
|
64
|
+
return entry.count < this.maxAttempts;
|
|
65
|
+
}
|
|
66
|
+
increment(key, now) {
|
|
67
|
+
// Evict expired entries when store grows too large (prevent OOM)
|
|
68
|
+
if (this.store.size > this.maxStoreSize) {
|
|
69
|
+
for (const [k, v] of this.store) {
|
|
70
|
+
if (v.resetAt < now)
|
|
71
|
+
this.store.delete(k);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
let entry = this.store.get(key);
|
|
75
|
+
if (!entry || entry.resetAt < now) {
|
|
76
|
+
entry = { count: 0, resetAt: now + this.window };
|
|
77
|
+
this.store.set(key, entry);
|
|
78
|
+
}
|
|
79
|
+
entry.count++;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function normalizeIdentifier(identifier) {
|
|
83
|
+
return identifier.trim().toLowerCase();
|
|
84
|
+
}
|
|
85
|
+
function normalizeIp(ip) {
|
|
86
|
+
return ip.trim();
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=AuthRateLimiter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AuthRateLimiter.js","sourceRoot":"","sources":["../src/AuthRateLimiter.ts"],"names":[],"mappings":"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"}
|
package/dist/Guard.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @Guard() decorator — protects route handlers with authentication.
|
|
3
|
+
*
|
|
4
|
+
* @implements FR50, FR51
|
|
5
|
+
*/
|
|
6
|
+
import "reflect-metadata";
|
|
7
|
+
/**
|
|
8
|
+
* @Guard('jwt') — require authentication via the named strategy.
|
|
9
|
+
* At least one strategy name is required.
|
|
10
|
+
*/
|
|
11
|
+
export declare function Guard(first: string, ...rest: string[]): MethodDecorator;
|
|
12
|
+
/**
|
|
13
|
+
* @Permission('orders.create') — require specific permissions.
|
|
14
|
+
*/
|
|
15
|
+
export declare function Permission(...permissions: string[]): MethodDecorator;
|
|
16
|
+
/**
|
|
17
|
+
* @Role('admin') — require specific roles.
|
|
18
|
+
*/
|
|
19
|
+
export declare function Role(...roles: string[]): MethodDecorator;
|
|
20
|
+
/** Get guard metadata. */
|
|
21
|
+
export declare function getGuardMetadata(target: object, propertyKey: string | symbol): string[];
|
|
22
|
+
/** Get permission metadata. */
|
|
23
|
+
export declare function getPermissionMetadata(target: object, propertyKey: string | symbol): string[];
|
|
24
|
+
/** Get role metadata. */
|
|
25
|
+
export declare function getRoleMetadata(target: object, propertyKey: string | symbol): string[];
|
|
26
|
+
/**
|
|
27
|
+
* @RequireMfa() — require the authenticated user to have completed multi-factor
|
|
28
|
+
* authentication. The auth middleware rejects with 403 `MFA_REQUIRED` when the
|
|
29
|
+
* user payload lacks a truthy `mfa` claim (set by your MFA step-up flow once
|
|
30
|
+
* `MfaManager.verify()` succeeds).
|
|
31
|
+
*/
|
|
32
|
+
export declare function RequireMfa(): MethodDecorator;
|
|
33
|
+
/** Whether the route requires completed MFA. */
|
|
34
|
+
export declare function getRequireMfaMetadata(target: object, propertyKey: string | symbol): boolean;
|
|
35
|
+
//# sourceMappingURL=Guard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Guard.d.ts","sourceRoot":"","sources":["../src/Guard.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,OAAO,kBAAkB,CAAC;AAQ1B;;;GAGG;AACH,wBAAgB,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,GAAG,eAAe,CAKvE;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,GAAG,WAAW,EAAE,MAAM,EAAE,GAAG,eAAe,CAIpE;AAED;;GAEG;AACH,wBAAgB,IAAI,CAAC,GAAG,KAAK,EAAE,MAAM,EAAE,GAAG,eAAe,CAIxD;AAED,0BAA0B;AAC1B,wBAAgB,gBAAgB,CAC/B,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,GAAG,MAAM,GAC1B,MAAM,EAAE,CAEV;AAED,+BAA+B;AAC/B,wBAAgB,qBAAqB,CACpC,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,GAAG,MAAM,GAC1B,MAAM,EAAE,CAEV;AAED,yBAAyB;AACzB,wBAAgB,eAAe,CAC9B,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,GAAG,MAAM,GAC1B,MAAM,EAAE,CAEV;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,IAAI,eAAe,CAI5C;AAED,gDAAgD;AAChD,wBAAgB,qBAAqB,CACpC,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,GAAG,MAAM,GAC1B,OAAO,CAET"}
|
package/dist/Guard.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @Guard() decorator — protects route handlers with authentication.
|
|
3
|
+
*
|
|
4
|
+
* @implements FR50, FR51
|
|
5
|
+
*/
|
|
6
|
+
// Side-effect import: registers the `Reflect.defineMetadata` / `getOwnMetadata`
|
|
7
|
+
// methods used below. Pulled in here (not transitively via the framework)
|
|
8
|
+
// so warden remains self-sufficient when published / consumed standalone.
|
|
9
|
+
import "reflect-metadata";
|
|
10
|
+
/** Guard decorator metadata key — Symbol.for ensures cross-module accessibility. */
|
|
11
|
+
const GUARD_KEY = Symbol.for("warden:guard");
|
|
12
|
+
const PERMISSION_KEY = Symbol.for("warden:permission");
|
|
13
|
+
const ROLE_KEY = Symbol.for("warden:role");
|
|
14
|
+
const MFA_KEY = Symbol.for("warden:mfa");
|
|
15
|
+
/**
|
|
16
|
+
* @Guard('jwt') — require authentication via the named strategy.
|
|
17
|
+
* At least one strategy name is required.
|
|
18
|
+
*/
|
|
19
|
+
export function Guard(first, ...rest) {
|
|
20
|
+
const strategies = [first, ...rest];
|
|
21
|
+
return (target, propertyKey) => {
|
|
22
|
+
Reflect.defineMetadata(GUARD_KEY, strategies, target, propertyKey);
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* @Permission('orders.create') — require specific permissions.
|
|
27
|
+
*/
|
|
28
|
+
export function Permission(...permissions) {
|
|
29
|
+
return (target, propertyKey) => {
|
|
30
|
+
Reflect.defineMetadata(PERMISSION_KEY, permissions, target, propertyKey);
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* @Role('admin') — require specific roles.
|
|
35
|
+
*/
|
|
36
|
+
export function Role(...roles) {
|
|
37
|
+
return (target, propertyKey) => {
|
|
38
|
+
Reflect.defineMetadata(ROLE_KEY, roles, target, propertyKey);
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/** Get guard metadata. */
|
|
42
|
+
export function getGuardMetadata(target, propertyKey) {
|
|
43
|
+
return Reflect.getOwnMetadata(GUARD_KEY, target, propertyKey) ?? [];
|
|
44
|
+
}
|
|
45
|
+
/** Get permission metadata. */
|
|
46
|
+
export function getPermissionMetadata(target, propertyKey) {
|
|
47
|
+
return Reflect.getOwnMetadata(PERMISSION_KEY, target, propertyKey) ?? [];
|
|
48
|
+
}
|
|
49
|
+
/** Get role metadata. */
|
|
50
|
+
export function getRoleMetadata(target, propertyKey) {
|
|
51
|
+
return Reflect.getOwnMetadata(ROLE_KEY, target, propertyKey) ?? [];
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* @RequireMfa() — require the authenticated user to have completed multi-factor
|
|
55
|
+
* authentication. The auth middleware rejects with 403 `MFA_REQUIRED` when the
|
|
56
|
+
* user payload lacks a truthy `mfa` claim (set by your MFA step-up flow once
|
|
57
|
+
* `MfaManager.verify()` succeeds).
|
|
58
|
+
*/
|
|
59
|
+
export function RequireMfa() {
|
|
60
|
+
return (target, propertyKey) => {
|
|
61
|
+
Reflect.defineMetadata(MFA_KEY, true, target, propertyKey);
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/** Whether the route requires completed MFA. */
|
|
65
|
+
export function getRequireMfaMetadata(target, propertyKey) {
|
|
66
|
+
return Reflect.getOwnMetadata(MFA_KEY, target, propertyKey) ?? false;
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=Guard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Guard.js","sourceRoot":"","sources":["../src/Guard.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,gFAAgF;AAChF,0EAA0E;AAC1E,0EAA0E;AAC1E,OAAO,kBAAkB,CAAC;AAE1B,oFAAoF;AACpF,MAAM,SAAS,GAAG,MAAM,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;AAC7C,MAAM,cAAc,GAAG,MAAM,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC;AACvD,MAAM,QAAQ,GAAG,MAAM,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;AAC3C,MAAM,OAAO,GAAG,MAAM,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;AAEzC;;;GAGG;AACH,MAAM,UAAU,KAAK,CAAC,KAAa,EAAE,GAAG,IAAc;IACrD,MAAM,UAAU,GAAG,CAAC,KAAK,EAAE,GAAG,IAAI,CAAC,CAAC;IACpC,OAAO,CAAC,MAAM,EAAE,WAAW,EAAE,EAAE;QAC9B,OAAO,CAAC,cAAc,CAAC,SAAS,EAAE,UAAU,EAAE,MAAM,EAAE,WAAW,CAAC,CAAC;IACpE,CAAC,CAAC;AACH,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,UAAU,CAAC,GAAG,WAAqB;IAClD,OAAO,CAAC,MAAM,EAAE,WAAW,EAAE,EAAE;QAC9B,OAAO,CAAC,cAAc,CAAC,cAAc,EAAE,WAAW,EAAE,MAAM,EAAE,WAAW,CAAC,CAAC;IAC1E,CAAC,CAAC;AACH,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,IAAI,CAAC,GAAG,KAAe;IACtC,OAAO,CAAC,MAAM,EAAE,WAAW,EAAE,EAAE;QAC9B,OAAO,CAAC,cAAc,CAAC,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,CAAC,CAAC;IAC9D,CAAC,CAAC;AACH,CAAC;AAED,0BAA0B;AAC1B,MAAM,UAAU,gBAAgB,CAC/B,MAAc,EACd,WAA4B;IAE5B,OAAO,OAAO,CAAC,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,WAAW,CAAC,IAAI,EAAE,CAAC;AACrE,CAAC;AAED,+BAA+B;AAC/B,MAAM,UAAU,qBAAqB,CACpC,MAAc,EACd,WAA4B;IAE5B,OAAO,OAAO,CAAC,cAAc,CAAC,cAAc,EAAE,MAAM,EAAE,WAAW,CAAC,IAAI,EAAE,CAAC;AAC1E,CAAC;AAED,yBAAyB;AACzB,MAAM,UAAU,eAAe,CAC9B,MAAc,EACd,WAA4B;IAE5B,OAAO,OAAO,CAAC,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,CAAC,IAAI,EAAE,CAAC;AACpE,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,UAAU;IACzB,OAAO,CAAC,MAAM,EAAE,WAAW,EAAE,EAAE;QAC9B,OAAO,CAAC,cAAc,CAAC,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,CAAC,CAAC;IAC5D,CAAC,CAAC;AACH,CAAC;AAED,gDAAgD;AAChD,MAAM,UAAU,qBAAqB,CACpC,MAAc,EACd,WAA4B;IAE5B,OAAO,OAAO,CAAC,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,CAAC,IAAI,KAAK,CAAC;AACtE,CAAC"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RedisBlacklistDriver — a persistent JWT blacklist backed by Redis / KeyDB,
|
|
3
|
+
* so logout revocation survives a process restart (unlike MemoryBlacklistDriver).
|
|
4
|
+
*
|
|
5
|
+
* Agnostic by construction: it depends on no Redis package. The caller injects
|
|
6
|
+
* any ioredis-compatible client (KeyDB, Redis, a mock…) that exposes `set` with
|
|
7
|
+
* an `"EX"` ttl and `exists`. Native key expiry does the housekeeping, so
|
|
8
|
+
* `cleanup()` is a no-op.
|
|
9
|
+
*/
|
|
10
|
+
import type { BlacklistDriver } from "./TokenBlacklist.js";
|
|
11
|
+
/** The minimal ioredis-shaped surface this driver needs. */
|
|
12
|
+
export interface RedisLikeClient {
|
|
13
|
+
set(key: string, value: string, mode: "EX", ttlSeconds: number): Promise<unknown>;
|
|
14
|
+
exists(key: string): Promise<number>;
|
|
15
|
+
}
|
|
16
|
+
export interface RedisBlacklistConfig {
|
|
17
|
+
/** Key prefix for blacklist entries. Default `warden:blacklist:`. */
|
|
18
|
+
prefix?: string;
|
|
19
|
+
}
|
|
20
|
+
export declare class RedisBlacklistDriver implements BlacklistDriver {
|
|
21
|
+
#private;
|
|
22
|
+
constructor(client: RedisLikeClient, config?: RedisBlacklistConfig);
|
|
23
|
+
/**
|
|
24
|
+
* Blacklist a JTI until its natural expiry. Tokens already past `expiresAt`
|
|
25
|
+
* are skipped — there is nothing left to revoke.
|
|
26
|
+
*/
|
|
27
|
+
add(jti: string, expiresAt: number): Promise<void>;
|
|
28
|
+
has(jti: string): Promise<boolean>;
|
|
29
|
+
/** No-op: Redis/KeyDB evicts expired keys on its own. */
|
|
30
|
+
cleanup(): Promise<void>;
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=RedisBlacklistDriver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RedisBlacklistDriver.d.ts","sourceRoot":"","sources":["../src/RedisBlacklistDriver.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAE3D,4DAA4D;AAC5D,MAAM,WAAW,eAAe;IAC/B,GAAG,CACF,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,IAAI,EACV,UAAU,EAAE,MAAM,GAChB,OAAO,CAAC,OAAO,CAAC,CAAC;IACpB,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CACrC;AAED,MAAM,WAAW,oBAAoB;IACpC,qEAAqE;IACrE,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,qBAAa,oBAAqB,YAAW,eAAe;;gBAI/C,MAAM,EAAE,eAAe,EAAE,MAAM,GAAE,oBAAyB;IAKtE;;;OAGG;IACG,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQlD,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAIxC,yDAAyD;IACnD,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;CAK9B"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RedisBlacklistDriver — a persistent JWT blacklist backed by Redis / KeyDB,
|
|
3
|
+
* so logout revocation survives a process restart (unlike MemoryBlacklistDriver).
|
|
4
|
+
*
|
|
5
|
+
* Agnostic by construction: it depends on no Redis package. The caller injects
|
|
6
|
+
* any ioredis-compatible client (KeyDB, Redis, a mock…) that exposes `set` with
|
|
7
|
+
* an `"EX"` ttl and `exists`. Native key expiry does the housekeeping, so
|
|
8
|
+
* `cleanup()` is a no-op.
|
|
9
|
+
*/
|
|
10
|
+
export class RedisBlacklistDriver {
|
|
11
|
+
#client;
|
|
12
|
+
#prefix;
|
|
13
|
+
constructor(client, config = {}) {
|
|
14
|
+
this.#client = client;
|
|
15
|
+
this.#prefix = config.prefix ?? "warden:blacklist:";
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Blacklist a JTI until its natural expiry. Tokens already past `expiresAt`
|
|
19
|
+
* are skipped — there is nothing left to revoke.
|
|
20
|
+
*/
|
|
21
|
+
async add(jti, expiresAt) {
|
|
22
|
+
const ttlSeconds = Math.ceil((expiresAt - Date.now()) / 1000);
|
|
23
|
+
if (ttlSeconds <= 0) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
await this.#client.set(this.#key(jti), "1", "EX", ttlSeconds);
|
|
27
|
+
}
|
|
28
|
+
async has(jti) {
|
|
29
|
+
return (await this.#client.exists(this.#key(jti))) > 0;
|
|
30
|
+
}
|
|
31
|
+
/** No-op: Redis/KeyDB evicts expired keys on its own. */
|
|
32
|
+
async cleanup() { }
|
|
33
|
+
#key(jti) {
|
|
34
|
+
return `${this.#prefix}${jti}`;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=RedisBlacklistDriver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RedisBlacklistDriver.js","sourceRoot":"","sources":["../src/RedisBlacklistDriver.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAoBH,MAAM,OAAO,oBAAoB;IACvB,OAAO,CAAkB;IACzB,OAAO,CAAS;IAEzB,YAAY,MAAuB,EAAE,SAA+B,EAAE;QACrE,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC;QACtB,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC,MAAM,IAAI,mBAAmB,CAAC;IACrD,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,GAAG,CAAC,GAAW,EAAE,SAAiB;QACvC,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,GAAG,IAAI,CAAC,CAAC;QAC9D,IAAI,UAAU,IAAI,CAAC,EAAE,CAAC;YACrB,OAAO;QACR,CAAC;QACD,MAAM,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,GAAG,EAAE,IAAI,EAAE,UAAU,CAAC,CAAC;IAC/D,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,GAAW;QACpB,OAAO,CAAC,MAAM,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IACxD,CAAC;IAED,yDAAyD;IACzD,KAAK,CAAC,OAAO,KAAmB,CAAC;IAEjC,IAAI,CAAC,GAAW;QACf,OAAO,GAAG,IAAI,CAAC,OAAO,GAAG,GAAG,EAAE,CAAC;IAChC,CAAC;CACD"}
|