@astram/guard 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/auth.controller.d.ts +219 -5
- package/dist/auth/auth.controller.d.ts.map +1 -1
- package/dist/auth/auth.controller.js +790 -12
- package/dist/auth/auth.module.d.ts +101 -3
- package/dist/auth/auth.module.d.ts.map +1 -1
- package/dist/auth/auth.module.js +300 -8
- package/dist/auth/dto.d.ts +16 -6
- package/dist/auth/dto.d.ts.map +1 -1
- 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/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/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/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/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/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/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/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-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/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/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/tokens.d.ts +174 -0
- package/dist/auth/tokens.d.ts.map +1 -1
- package/dist/auth/tokens.js +190 -1
- 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/authentication/mariadb-credential-resolver.d.ts +38 -24
- package/dist/authentication/mariadb-credential-resolver.d.ts.map +1 -1
- package/dist/authentication/mariadb-credential-resolver.js +42 -28
- 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/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/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-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-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/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-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/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/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/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/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/config/app-config.d.ts +18 -16
- package/dist/config/app-config.d.ts.map +1 -1
- package/dist/config/app-config.js +68 -35
- 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/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/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/index.d.ts +35 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +159 -7
- 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.module.d.ts +154 -10
- package/dist/module/shield.module.d.ts.map +1 -1
- package/dist/module/shield.module.js +181 -24
- 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/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-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/tokens.d.ts +14 -0
- package/dist/request-auth/tokens.d.ts.map +1 -0
- package/dist/request-auth/tokens.js +16 -0
- package/package.json +2 -2
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
* factorVerifiers: [ createTotpFactorVerifier({ hmacSha1 }) ] }). The TOTP
|
|
35
35
|
* verifier is wired so MFA works; `hmacSha1` is the {@link hmacSha1} node:crypto
|
|
36
36
|
* one-liner (guard owns node:crypto, core does not). `createIdentityBroker()`
|
|
37
|
-
* with no args registers the default Internal adapter with an EMPTY
|
|
37
|
+
* with no args registers the default Internal adapter with an EMPTY root
|
|
38
38
|
* allowlist (default-deny).
|
|
39
39
|
* ─────────────────────────────────────────────────────────────────────────────
|
|
40
40
|
*
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
*
|
|
55
55
|
* An enforcement-only app that does NOT opt in never imports AuthModule → never
|
|
56
56
|
* opens a pool/redis → never calls `assertRefreshHmacKey` → keeps the no-`.env`
|
|
57
|
-
* boot. An auth-enabled app fails LOUD at boot if `
|
|
57
|
+
* boot. An auth-enabled app fails LOUD at boot if `REFRESH_HMAC_KEY` is
|
|
58
58
|
* blank — auth can never compose unkeyed. `APP_CONFIG` is already `@Global` from
|
|
59
59
|
* `ShieldModule`, so AuthModule's config-injecting factories resolve. See DONE.md.
|
|
60
60
|
*
|
|
@@ -63,7 +63,17 @@
|
|
|
63
63
|
* node:crypto HMAC live in guard. Core stays zero-dep + unchanged.
|
|
64
64
|
*/
|
|
65
65
|
import { type DynamicModule } from '@nestjs/common';
|
|
66
|
-
import type { CredentialResolver, HmacSha1 } from '@astram/core';
|
|
66
|
+
import type { CredentialResolver, HmacSha1, FactorVerifier } from '@astram/core';
|
|
67
|
+
import type { SessionFactsProjector } from './ports/session-facts-projector.js';
|
|
68
|
+
import type { ProfileResolver } from './profile-resolver.js';
|
|
69
|
+
import type { CredentialWriter } from './ports/credential-writer.js';
|
|
70
|
+
import type { ResetTokenStore } from './ports/reset-token-store.js';
|
|
71
|
+
import type { EmailSender } from './ports/email-sender.js';
|
|
72
|
+
import type { RecoveryCodeStore } from './ports/recovery-code-store.js';
|
|
73
|
+
import type { ProfileWriter } from './ports/profile-writer.js';
|
|
74
|
+
import type { SessionSweeper } from './ports/session-sweeper.js';
|
|
75
|
+
import type { MfaDeviceStore } from './ports/mfa-device-store.js';
|
|
76
|
+
import type { AuditSink } from './ports/audit-sink.js';
|
|
67
77
|
/**
|
|
68
78
|
* The injected HMAC-SHA1 primitive the core TOTP verifier needs (Core is zero-dep
|
|
69
79
|
* + node-type-free, so it cannot import `node:crypto`). Guard owns node:crypto and
|
|
@@ -71,6 +81,29 @@ import type { CredentialResolver, HmacSha1 } from '@astram/core';
|
|
|
71
81
|
* digest. Never logs the key or message.
|
|
72
82
|
*/
|
|
73
83
|
export declare const hmacSha1: HmacSha1;
|
|
84
|
+
/**
|
|
85
|
+
* Resolve the login `totp` {@link FactorVerifier}: the consumer's `override` when
|
|
86
|
+
* supplied, else the framework's `defaultVerifier`. A pure, infra-free `??` — the
|
|
87
|
+
* ONE decision point of the Phase-2 verifier seam (override wins; absent → the
|
|
88
|
+
* default RFC-6238 single-secret verify, behavior unchanged).
|
|
89
|
+
*
|
|
90
|
+
* The default is passed IN (rather than built here) because the core factory
|
|
91
|
+
* `createTotpFactorVerifier` is loaded LAZILY via {@link loadCore} (the CJS↔ESM
|
|
92
|
+
* invariant, see the file header) — only reachable inside an async provider factory.
|
|
93
|
+
* So the authenticator's async `useFactory` awaits {@link loadCore}, builds the
|
|
94
|
+
* default, and hands it here alongside the injected override.
|
|
95
|
+
*/
|
|
96
|
+
export declare function resolveTotpVerifier(override: FactorVerifier | null | undefined, defaultVerifier: FactorVerifier): FactorVerifier;
|
|
97
|
+
/**
|
|
98
|
+
* Build the framework's DEFAULT single-secret TOTP {@link FactorVerifier}
|
|
99
|
+
* (`createTotpFactorVerifier({ hmacSha1 })`), loading the core factory lazily (the
|
|
100
|
+
* CJS↔ESM invariant, see the file header). Exported so a CONSUMER can obtain the
|
|
101
|
+
* framework's base per-secret verifier to WRAP (e.g. an OR-matching multi-device
|
|
102
|
+
* verifier delegates to it per device secret) WITHOUT importing `@astram/core`
|
|
103
|
+
* directly — which would become a runtime `require` on the ESM-only core in the
|
|
104
|
+
* consumer's built CJS dist. Async because of {@link loadCore}.
|
|
105
|
+
*/
|
|
106
|
+
export declare function makeDefaultTotpVerifier(): Promise<FactorVerifier>;
|
|
74
107
|
/**
|
|
75
108
|
* Options for {@link AuthModule.forRoot} — registration-time collaborators the
|
|
76
109
|
* consumer plugs through {@link ShieldModule.forRoot}.
|
|
@@ -84,6 +117,71 @@ export interface AuthModuleOptions {
|
|
|
84
117
|
* schema. Only meaningful here (AuthModule is only imported under `auth: true`).
|
|
85
118
|
*/
|
|
86
119
|
readonly credentialResolver?: CredentialResolver;
|
|
120
|
+
/**
|
|
121
|
+
* OPTIONAL current-user profile resolver backing `GET /auth/me` (the
|
|
122
|
+
* {@link ProfileResolver} port). Omit → `/auth/me` returns an honest 501 (the
|
|
123
|
+
* framework is org-schema-agnostic; it cannot name the user/company/roles without
|
|
124
|
+
* the consumer's org tables). Supply an implementation to return the signed-in
|
|
125
|
+
* user's display profile (names, never UUIDs). Only meaningful under `auth: true`.
|
|
126
|
+
*/
|
|
127
|
+
readonly profileResolver?: ProfileResolver;
|
|
128
|
+
/**
|
|
129
|
+
* OPTIONAL account/credential-surface seams (forgot/reset, change-password, MFA
|
|
130
|
+
* enrollment, profile edit). Each is threaded through exactly like
|
|
131
|
+
* {@link profileResolver}: supplied → bound app-wide under its token; omitted →
|
|
132
|
+
* bound `null`, so the route that needs it returns an honest 501. Only meaningful
|
|
133
|
+
* under `auth: true`. See `./ports/*.ts` for each contract.
|
|
134
|
+
*/
|
|
135
|
+
readonly credentialWriter?: CredentialWriter;
|
|
136
|
+
readonly resetTokenStore?: ResetTokenStore;
|
|
137
|
+
readonly emailSender?: EmailSender;
|
|
138
|
+
readonly recoveryCodeStore?: RecoveryCodeStore;
|
|
139
|
+
readonly profileWriter?: ProfileWriter;
|
|
140
|
+
readonly sessionSweeper?: SessionSweeper;
|
|
141
|
+
/**
|
|
142
|
+
* OPTIONAL auth-surface audit sink (the {@link AuditSink} port) — persists the
|
|
143
|
+
* FIRST auth audit event (the recovery-code login, spec §6.2). Threaded exactly like
|
|
144
|
+
* the other seams: supplied → bound app-wide under its token; omitted → bound `null`,
|
|
145
|
+
* so the recovery branch's best-effort audit call is a no-op (login still works,
|
|
146
|
+
* un-audited). Only meaningful under `auth: true`.
|
|
147
|
+
*/
|
|
148
|
+
readonly auditSink?: AuditSink;
|
|
149
|
+
/**
|
|
150
|
+
* OPTIONAL multi-device MFA store (the {@link MfaDeviceStore} port) — persists the
|
|
151
|
+
* caller's N TOTP authenticator devices. Threaded exactly like the other seams:
|
|
152
|
+
* supplied → bound app-wide under its token; omitted → bound `null`, so the device
|
|
153
|
+
* routes return an honest 501. Only meaningful under `auth: true`.
|
|
154
|
+
*/
|
|
155
|
+
readonly mfaDeviceStore?: MfaDeviceStore;
|
|
156
|
+
/**
|
|
157
|
+
* OPTIONAL login `totp` {@link FactorVerifier} override (the Phase-2 multi-device
|
|
158
|
+
* seam). Supplied → REPLACES the framework's default single-secret
|
|
159
|
+
* `createTotpFactorVerifier({ hmacSha1 })` in the composed authenticator's
|
|
160
|
+
* `factorVerifiers`; omitted → the default (behavior unchanged). A consumer plugs
|
|
161
|
+
* an OR-matching verifier here so login matches ANY enrolled device (the resolver
|
|
162
|
+
* bundles the N device secrets into one required-factor). Only meaningful under
|
|
163
|
+
* `auth: true`.
|
|
164
|
+
*/
|
|
165
|
+
readonly totpVerifier?: FactorVerifier;
|
|
166
|
+
/**
|
|
167
|
+
* OPTIONAL root allowlist — the server-side, trusted set of canonical
|
|
168
|
+
* principal ids that ARE roots (passed to
|
|
169
|
+
* `createIdentityBroker({ internalRootIds })`). ROOT SAFETY: this is the
|
|
170
|
+
* ONLY way a login mints a root — the Internal adapter keys the trusted signal
|
|
171
|
+
* off THIS list, never off a claim (an attacker-controllable `isPlatformSuperuser`
|
|
172
|
+
* claim is ignored). Omit → `[]` → default-deny (nobody is a root, historical
|
|
173
|
+
* behavior unchanged). Only meaningful under `auth: true`.
|
|
174
|
+
*/
|
|
175
|
+
readonly rootIds?: readonly string[];
|
|
176
|
+
/**
|
|
177
|
+
* OPTIONAL eager session-facts projector (ADR-0011, the {@link SessionFactsProjector}
|
|
178
|
+
* port). Threaded exactly like the other seams: supplied → bound app-wide under
|
|
179
|
+
* {@link SHIELD_SESSION_FACTS_PROJECTOR}; omitted → bound `null`, so the auth controller
|
|
180
|
+
* omits `cachedFacts` and gate 5 DB-reloads every request (today's behavior). When
|
|
181
|
+
* supplied, login/refresh cache the principal's coarse grants on the session binding so
|
|
182
|
+
* gate 5 serves them from the request's `cachedFacts`. Only meaningful under `auth: true`.
|
|
183
|
+
*/
|
|
184
|
+
readonly sessionFactsProjector?: SessionFactsProjector;
|
|
87
185
|
}
|
|
88
186
|
/**
|
|
89
187
|
* The auth composition module. Imports {@link PersistenceModule} (the live
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.module.d.ts","sourceRoot":"","sources":["../../src/auth/auth.module.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+DG;AAEH,OAAO,EAAU,KAAK,aAAa,EAAiB,MAAM,gBAAgB,CAAC;AAa3E,OAAO,KAAK,EAGV,kBAAkB,EAOlB,QAAQ,
|
|
1
|
+
{"version":3,"file":"auth.module.d.ts","sourceRoot":"","sources":["../../src/auth/auth.module.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+DG;AAEH,OAAO,EAAU,KAAK,aAAa,EAAiB,MAAM,gBAAgB,CAAC;AAa3E,OAAO,KAAK,EAGV,kBAAkB,EAOlB,QAAQ,EACR,cAAc,EACf,MAAM,cAAc,CAAC;AAkDtB,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,oCAAoC,CAAC;AAEhF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AACrE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AACpE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AACxE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAClE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAIvD;;;;;GAKG;AACH,eAAO,MAAM,QAAQ,EAAE,QACqE,CAAC;AAE7F;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,cAAc,GAAG,IAAI,GAAG,SAAS,EAC3C,eAAe,EAAE,cAAc,GAC9B,cAAc,CAEhB;AAED;;;;;;;;GAQG;AACH,wBAAsB,uBAAuB,IAAI,OAAO,CAAC,cAAc,CAAC,CAGvE;AAyUD;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC;;;;;;OAMG;IACH,QAAQ,CAAC,kBAAkB,CAAC,EAAE,kBAAkB,CAAC;IACjD;;;;;;OAMG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,eAAe,CAAC;IAC3C;;;;;;OAMG;IACH,QAAQ,CAAC,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;IAC7C,QAAQ,CAAC,eAAe,CAAC,EAAE,eAAe,CAAC;IAC3C,QAAQ,CAAC,WAAW,CAAC,EAAE,WAAW,CAAC;IACnC,QAAQ,CAAC,iBAAiB,CAAC,EAAE,iBAAiB,CAAC;IAC/C,QAAQ,CAAC,aAAa,CAAC,EAAE,aAAa,CAAC;IACvC,QAAQ,CAAC,cAAc,CAAC,EAAE,cAAc,CAAC;IACzC;;;;;;OAMG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC;IAC/B;;;;;OAKG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,cAAc,CAAC;IACzC;;;;;;;;OAQG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,cAAc,CAAC;IACvC;;;;;;;;OAQG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC;;;;;;;OAOG;IACH,QAAQ,CAAC,qBAAqB,CAAC,EAAE,qBAAqB,CAAC;CACxD;AAgED;;;;;;GAMG;AACH,qBACa,UAAU;IACrB;;;;;OAKG;IACH,MAAM,CAAC,OAAO,CAAC,OAAO,GAAE,iBAAsB,GAAG,aAAa;CA6F/D"}
|
package/dist/auth/auth.module.js
CHANGED
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
* factorVerifiers: [ createTotpFactorVerifier({ hmacSha1 }) ] }). The TOTP
|
|
36
36
|
* verifier is wired so MFA works; `hmacSha1` is the {@link hmacSha1} node:crypto
|
|
37
37
|
* one-liner (guard owns node:crypto, core does not). `createIdentityBroker()`
|
|
38
|
-
* with no args registers the default Internal adapter with an EMPTY
|
|
38
|
+
* with no args registers the default Internal adapter with an EMPTY root
|
|
39
39
|
* allowlist (default-deny).
|
|
40
40
|
* ─────────────────────────────────────────────────────────────────────────────
|
|
41
41
|
*
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
*
|
|
56
56
|
* An enforcement-only app that does NOT opt in never imports AuthModule → never
|
|
57
57
|
* opens a pool/redis → never calls `assertRefreshHmacKey` → keeps the no-`.env`
|
|
58
|
-
* boot. An auth-enabled app fails LOUD at boot if `
|
|
58
|
+
* boot. An auth-enabled app fails LOUD at boot if `REFRESH_HMAC_KEY` is
|
|
59
59
|
* blank — auth can never compose unkeyed. `APP_CONFIG` is already `@Global` from
|
|
60
60
|
* `ShieldModule`, so AuthModule's config-injecting factories resolve. See DONE.md.
|
|
61
61
|
*
|
|
@@ -105,6 +105,8 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
105
105
|
var AuthModule_1;
|
|
106
106
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
107
107
|
exports.AuthModule = exports.hmacSha1 = void 0;
|
|
108
|
+
exports.resolveTotpVerifier = resolveTotpVerifier;
|
|
109
|
+
exports.makeDefaultTotpVerifier = makeDefaultTotpVerifier;
|
|
108
110
|
const common_1 = require("@nestjs/common");
|
|
109
111
|
const node_crypto_1 = require("node:crypto");
|
|
110
112
|
const app_config_js_1 = require("../config/app-config.js");
|
|
@@ -114,8 +116,12 @@ const mariadb_credential_resolver_js_1 = require("../authentication/mariadb-cred
|
|
|
114
116
|
const index_js_1 = require("../session/index.js");
|
|
115
117
|
const persistence_module_js_1 = require("./persistence.module.js");
|
|
116
118
|
const auth_controller_js_1 = require("./auth.controller.js");
|
|
119
|
+
const account_controller_js_1 = require("./account.controller.js");
|
|
117
120
|
const pending_mfa_store_js_1 = require("./pending-mfa-store.js");
|
|
118
121
|
const tokens_js_1 = require("./tokens.js");
|
|
122
|
+
const attempt_limiter_js_1 = require("./rate-limit/attempt-limiter.js");
|
|
123
|
+
const tokens_js_2 = require("../request-auth/tokens.js");
|
|
124
|
+
const session_token_binder_js_1 = require("../request-auth/session-token-binder.js");
|
|
119
125
|
/**
|
|
120
126
|
* The injected HMAC-SHA1 primitive the core TOTP verifier needs (Core is zero-dep
|
|
121
127
|
* + node-type-free, so it cannot import `node:crypto`). Guard owns node:crypto and
|
|
@@ -124,6 +130,34 @@ const tokens_js_1 = require("./tokens.js");
|
|
|
124
130
|
*/
|
|
125
131
|
const hmacSha1 = (key, message) => new Uint8Array((0, node_crypto_1.createHmac)('sha1', Buffer.from(key)).update(Buffer.from(message)).digest());
|
|
126
132
|
exports.hmacSha1 = hmacSha1;
|
|
133
|
+
/**
|
|
134
|
+
* Resolve the login `totp` {@link FactorVerifier}: the consumer's `override` when
|
|
135
|
+
* supplied, else the framework's `defaultVerifier`. A pure, infra-free `??` — the
|
|
136
|
+
* ONE decision point of the Phase-2 verifier seam (override wins; absent → the
|
|
137
|
+
* default RFC-6238 single-secret verify, behavior unchanged).
|
|
138
|
+
*
|
|
139
|
+
* The default is passed IN (rather than built here) because the core factory
|
|
140
|
+
* `createTotpFactorVerifier` is loaded LAZILY via {@link loadCore} (the CJS↔ESM
|
|
141
|
+
* invariant, see the file header) — only reachable inside an async provider factory.
|
|
142
|
+
* So the authenticator's async `useFactory` awaits {@link loadCore}, builds the
|
|
143
|
+
* default, and hands it here alongside the injected override.
|
|
144
|
+
*/
|
|
145
|
+
function resolveTotpVerifier(override, defaultVerifier) {
|
|
146
|
+
return override ?? defaultVerifier;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Build the framework's DEFAULT single-secret TOTP {@link FactorVerifier}
|
|
150
|
+
* (`createTotpFactorVerifier({ hmacSha1 })`), loading the core factory lazily (the
|
|
151
|
+
* CJS↔ESM invariant, see the file header). Exported so a CONSUMER can obtain the
|
|
152
|
+
* framework's base per-secret verifier to WRAP (e.g. an OR-matching multi-device
|
|
153
|
+
* verifier delegates to it per device secret) WITHOUT importing `@astram/core`
|
|
154
|
+
* directly — which would become a runtime `require` on the ESM-only core in the
|
|
155
|
+
* consumer's built CJS dist. Async because of {@link loadCore}.
|
|
156
|
+
*/
|
|
157
|
+
async function makeDefaultTotpVerifier() {
|
|
158
|
+
const { createTotpFactorVerifier } = await loadCore();
|
|
159
|
+
return createTotpFactorVerifier({ hmacSha1: exports.hmacSha1 });
|
|
160
|
+
}
|
|
127
161
|
/**
|
|
128
162
|
* A `Function`-wrapped dynamic `import()` that survives the CommonJS **build**.
|
|
129
163
|
* Under `module: commonjs`, `tsc` rewrites a literal `import()` into
|
|
@@ -207,6 +241,88 @@ const hotSessionStoreProvider = {
|
|
|
207
241
|
useFactory: (redis) => new index_js_1.RedisHotSessionStore(redis),
|
|
208
242
|
inject: [tokens_js_1.SHIELD_REDIS],
|
|
209
243
|
};
|
|
244
|
+
/**
|
|
245
|
+
* The request-auth token binder (the flagged-gap fill): binds an opaque access token
|
|
246
|
+
* to its principal claims in Redis, so the enforcement guard can turn a later
|
|
247
|
+
* request's bearer token back into a `Principal`. Provided only under `auth: true`
|
|
248
|
+
* (it needs the live Redis client); exported so the enforcement guard injects it.
|
|
249
|
+
*/
|
|
250
|
+
const sessionTokenBinderProvider = {
|
|
251
|
+
provide: tokens_js_2.SHIELD_SESSION_TOKEN_BINDER,
|
|
252
|
+
useFactory: (redis) => new session_token_binder_js_1.SessionTokenBinder(redis),
|
|
253
|
+
inject: [tokens_js_1.SHIELD_REDIS],
|
|
254
|
+
};
|
|
255
|
+
/**
|
|
256
|
+
* The password-login rate limiter (spec §5 / H1) — a Redis-injected
|
|
257
|
+
* {@link AttemptLimiter}, 10 failures / 15-min window, keyed `login:<hint>`. Same
|
|
258
|
+
* live-Redis provider pattern as {@link sessionTokenBinderProvider} (a `useFactory`
|
|
259
|
+
* injecting {@link SHIELD_REDIS}); provided only under `auth: true`. Fail-open on a
|
|
260
|
+
* Redis outage is the limiter's own behavior (never locks out on a blip).
|
|
261
|
+
*/
|
|
262
|
+
const loginLimiterProvider = {
|
|
263
|
+
provide: tokens_js_1.SHIELD_LOGIN_LIMITER,
|
|
264
|
+
useFactory: (redis) => new attempt_limiter_js_1.AttemptLimiter(redis, { maxAttempts: 10, windowMs: 15 * 60 * 1000 }),
|
|
265
|
+
inject: [tokens_js_1.SHIELD_REDIS],
|
|
266
|
+
};
|
|
267
|
+
/**
|
|
268
|
+
* The recovery-code rate limiter (spec §5) — a Redis-injected {@link AttemptLimiter},
|
|
269
|
+
* 5 failures / 15-min window, keyed `recovery:<principalId>` (a lower ceiling than
|
|
270
|
+
* login: recovery codes don't rotate, so they're higher-value). Same live-Redis
|
|
271
|
+
* provider pattern as {@link sessionTokenBinderProvider}; provided only under
|
|
272
|
+
* `auth: true`.
|
|
273
|
+
*/
|
|
274
|
+
const recoveryLimiterProvider = {
|
|
275
|
+
provide: tokens_js_1.SHIELD_RECOVERY_LIMITER,
|
|
276
|
+
useFactory: (redis) => new attempt_limiter_js_1.AttemptLimiter(redis, { maxAttempts: 5, windowMs: 15 * 60 * 1000 }),
|
|
277
|
+
inject: [tokens_js_1.SHIELD_REDIS],
|
|
278
|
+
};
|
|
279
|
+
/**
|
|
280
|
+
* The forgot-password rate limiter (feature #141) — a Redis-injected
|
|
281
|
+
* {@link AttemptLimiter}, keyed BY CLIENT IP (`forgot:ip:<ip>`, 5 per 15-min window)
|
|
282
|
+
* AND, separately, per-hint (`forgot:<hint>`, provided as {@link forgotHintLimiterProvider}).
|
|
283
|
+
* NOTE the "attempt" semantics here differ from login: forgot-password is ALWAYS 204
|
|
284
|
+
* (returning 429 would itself be an enumeration signal), so this limiter counts every
|
|
285
|
+
* forgot REQUEST (a request-rate cap), and once the budget is exceeded the handler
|
|
286
|
+
* SILENTLY skips the email-send work while still returning 204. Same live-Redis
|
|
287
|
+
* provider pattern as {@link loginLimiterProvider}; provided only under `auth: true`.
|
|
288
|
+
* Fail-open on a Redis outage is the limiter's own behavior (never silently drops a
|
|
289
|
+
* legitimate reset email on a blip). Threshold chosen to stop IP-spray without locking
|
|
290
|
+
* out a genuine user who requests a couple of resets.
|
|
291
|
+
*/
|
|
292
|
+
const forgotLimiterProvider = {
|
|
293
|
+
provide: tokens_js_1.SHIELD_FORGOT_LIMITER,
|
|
294
|
+
useFactory: (redis) => new attempt_limiter_js_1.AttemptLimiter(redis, { maxAttempts: 5, windowMs: 15 * 60 * 1000 }),
|
|
295
|
+
inject: [tokens_js_1.SHIELD_REDIS],
|
|
296
|
+
};
|
|
297
|
+
/**
|
|
298
|
+
* The forgot-password PER-HINT rate limiter (feature #141) — a Redis-injected
|
|
299
|
+
* {@link AttemptLimiter}, keyed by the presented hint (`forgot:<hint>`, 3 per 60-min
|
|
300
|
+
* window). This caps how often ANY caller (across IPs) can trigger a reset email for
|
|
301
|
+
* ONE account, so a single victim's inbox can't be email-bombed even from a rotating
|
|
302
|
+
* IP pool. A GENEROUS per-hint ceiling (a real user rarely needs >3 reset emails/hour)
|
|
303
|
+
* with a longer window than the IP cap. Same request-rate-cap semantics as
|
|
304
|
+
* {@link forgotLimiterProvider}: on exceed, SILENTLY skip the send, still 204.
|
|
305
|
+
*/
|
|
306
|
+
const forgotHintLimiterProvider = {
|
|
307
|
+
provide: tokens_js_1.SHIELD_FORGOT_HINT_LIMITER,
|
|
308
|
+
useFactory: (redis) => new attempt_limiter_js_1.AttemptLimiter(redis, { maxAttempts: 3, windowMs: 60 * 60 * 1000 }),
|
|
309
|
+
inject: [tokens_js_1.SHIELD_REDIS],
|
|
310
|
+
};
|
|
311
|
+
/**
|
|
312
|
+
* The reset-password rate limiter (feature #141) — a Redis-injected
|
|
313
|
+
* {@link AttemptLimiter}, keyed by CLIENT IP (`reset:ip:<ip>`, 10 per 15-min window).
|
|
314
|
+
* Reset is token-gated (a 256-bit single-use token), so this is DoS/abuse throttling,
|
|
315
|
+
* NOT anti-brute-force — and unlike forgot it MAY return a generic 429 on lockout (a
|
|
316
|
+
* token-gated endpoint is not enumeration-sensitive). `recordFailure` on a bad/invalid
|
|
317
|
+
* token (the abuse signal), `reset` on a successful reset. Same live-Redis provider
|
|
318
|
+
* pattern as {@link loginLimiterProvider}; provided only under `auth: true`. Fail-open
|
|
319
|
+
* on a Redis outage (a legitimate reset is never blocked by a blip).
|
|
320
|
+
*/
|
|
321
|
+
const resetLimiterProvider = {
|
|
322
|
+
provide: tokens_js_1.SHIELD_RESET_LIMITER,
|
|
323
|
+
useFactory: (redis) => new attempt_limiter_js_1.AttemptLimiter(redis, { maxAttempts: 10, windowMs: 15 * 60 * 1000 }),
|
|
324
|
+
inject: [tokens_js_1.SHIELD_REDIS],
|
|
325
|
+
};
|
|
210
326
|
/** Opaque token generator (node:crypto adapter). */
|
|
211
327
|
const tokenGeneratorProvider = {
|
|
212
328
|
provide: index_js_1.SHIELD_TOKEN_GENERATOR,
|
|
@@ -252,20 +368,133 @@ const sessionAuthorityProvider = {
|
|
|
252
368
|
config_module_js_1.APP_CONFIG,
|
|
253
369
|
],
|
|
254
370
|
};
|
|
255
|
-
/**
|
|
371
|
+
/**
|
|
372
|
+
* The composed Authenticator (core) — password + TOTP factor, anti-enumeration.
|
|
373
|
+
*
|
|
374
|
+
* The `totp` verifier is the Phase-2 SEAM: {@link resolveTotpVerifier} picks the
|
|
375
|
+
* consumer's `totpOverride` (bound under {@link SHIELD_TOTP_VERIFIER_OVERRIDE}) when
|
|
376
|
+
* supplied, else the framework's default `createTotpFactorVerifier({ hmacSha1 })`.
|
|
377
|
+
* The override reaches this async factory via `inject` (not a captured closure) so
|
|
378
|
+
* the default binding stays exactly as before when no override is plugged.
|
|
379
|
+
*/
|
|
256
380
|
const authenticatorProvider = {
|
|
257
381
|
provide: tokens_js_1.SHIELD_AUTHENTICATOR,
|
|
258
382
|
// async: loads the core factories lazily (CJS→ESM, see loadCore()); Nest awaits it.
|
|
259
|
-
useFactory: async (hasher, resolver) => {
|
|
383
|
+
useFactory: async (hasher, resolver, totpOverride, rootIds) => {
|
|
260
384
|
const { createAuthenticator, createIdentityBroker, createTotpFactorVerifier } = await loadCore();
|
|
385
|
+
const totp = resolveTotpVerifier(totpOverride, createTotpFactorVerifier({ hmacSha1: exports.hmacSha1 }));
|
|
261
386
|
return createAuthenticator({
|
|
262
387
|
hasher,
|
|
263
388
|
resolver,
|
|
264
|
-
|
|
265
|
-
|
|
389
|
+
// ROOT SAFETY: the ONLY way a login mints a root is the
|
|
390
|
+
// server-side allowlist wired here (the Internal adapter keys the trusted
|
|
391
|
+
// signal off it, never off a claim). Absent → `[]` → default-deny (the
|
|
392
|
+
// historical `createIdentityBroker()` behavior, exactly unchanged).
|
|
393
|
+
identityBroker: createIdentityBroker({ internalRootIds: rootIds ?? [] }),
|
|
394
|
+
factorVerifiers: [totp],
|
|
266
395
|
});
|
|
267
396
|
},
|
|
268
|
-
inject: [
|
|
397
|
+
inject: [
|
|
398
|
+
argon2id_password_hasher_js_1.SHIELD_PASSWORD_HASHER,
|
|
399
|
+
tokens_js_1.SHIELD_CREDENTIAL_RESOLVER,
|
|
400
|
+
tokens_js_1.SHIELD_TOTP_VERIFIER_OVERRIDE,
|
|
401
|
+
tokens_js_1.SHIELD_ROOT_IDS,
|
|
402
|
+
],
|
|
403
|
+
};
|
|
404
|
+
/**
|
|
405
|
+
* Bind the OPTIONAL login `totp` verifier override (the Phase-2 multi-device seam).
|
|
406
|
+
* Same value-provider pattern as the other seams: the supplied verifier when present,
|
|
407
|
+
* else `null` so the authenticator factory's inject resolves to "no override" → the
|
|
408
|
+
* default RFC-6238 verifier. Always provided so the DI token exists.
|
|
409
|
+
*/
|
|
410
|
+
function makeTotpVerifierOverrideProvider(override) {
|
|
411
|
+
return { provide: tokens_js_1.SHIELD_TOTP_VERIFIER_OVERRIDE, useValue: override ?? null };
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Bind the OPTIONAL root allowlist (the {@link SHIELD_ROOT_IDS}
|
|
415
|
+
* seam). Same value-provider pattern as the other seams: the supplied ids when present,
|
|
416
|
+
* else `null` so the authenticator factory's inject resolves to "no allowlist" → the
|
|
417
|
+
* broker's default-deny empty set (nobody is a root — historical behavior). Always
|
|
418
|
+
* provided so the DI token exists. See {@link SHIELD_ROOT_IDS} for the security
|
|
419
|
+
* rationale (the flag is set SOLELY from this trusted list, never from a claim).
|
|
420
|
+
*/
|
|
421
|
+
function makeRootIdsProvider(rootIds) {
|
|
422
|
+
return { provide: tokens_js_1.SHIELD_ROOT_IDS, useValue: rootIds ?? null };
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* The core TOTP {@link FactorVerifier} — the SAME verifier the login MFA branch uses
|
|
426
|
+
* (`createTotpFactorVerifier({ hmacSha1 })`), provided app-wide so
|
|
427
|
+
* `POST /account/mfa/{enroll/confirm,disable}` can verify a presented code against the
|
|
428
|
+
* pending/stored secret. Built via {@link loadCore} (the CJS→ESM lazy load, see the
|
|
429
|
+
* file header) so guard's built CJS dist never eagerly `require`s the ESM-only core.
|
|
430
|
+
* Injected pre-built into {@link AccountController}, which cannot import the core
|
|
431
|
+
* factory at its own top level.
|
|
432
|
+
*/
|
|
433
|
+
const totpVerifierProvider = {
|
|
434
|
+
provide: tokens_js_1.SHIELD_TOTP_VERIFIER,
|
|
435
|
+
useFactory: async () => {
|
|
436
|
+
const { createTotpFactorVerifier } = await loadCore();
|
|
437
|
+
return createTotpFactorVerifier({ hmacSha1: exports.hmacSha1 });
|
|
438
|
+
},
|
|
439
|
+
};
|
|
440
|
+
/**
|
|
441
|
+
* Bind the OPTIONAL current-user profile resolver. When the consumer supplies one it
|
|
442
|
+
* is a value provider under {@link SHIELD_PROFILE_RESOLVER}; when absent we bind
|
|
443
|
+
* `null` so the `@Optional()` injection in the controller resolves cleanly to
|
|
444
|
+
* "no seam" (→ `/auth/me` returns 501). Always provided so the DI token exists.
|
|
445
|
+
*/
|
|
446
|
+
function makeProfileResolverProvider(override) {
|
|
447
|
+
return { provide: tokens_js_1.SHIELD_PROFILE_RESOLVER, useValue: override ?? null };
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Bind each OPTIONAL account/credential-surface seam. Same pattern as
|
|
451
|
+
* {@link makeProfileResolverProvider}: a value provider under the seam's token —
|
|
452
|
+
* the supplied impl when present, else `null` so the route's `@Optional()` injection
|
|
453
|
+
* resolves cleanly to "no seam" (→ 501). Always provided so the DI token exists.
|
|
454
|
+
*/
|
|
455
|
+
function makeCredentialWriterProvider(override) {
|
|
456
|
+
return { provide: tokens_js_1.SHIELD_CREDENTIAL_WRITER, useValue: override ?? null };
|
|
457
|
+
}
|
|
458
|
+
function makeResetTokenStoreProvider(override) {
|
|
459
|
+
return { provide: tokens_js_1.SHIELD_RESET_TOKEN_STORE, useValue: override ?? null };
|
|
460
|
+
}
|
|
461
|
+
function makeEmailSenderProvider(override) {
|
|
462
|
+
return { provide: tokens_js_1.SHIELD_EMAIL_SENDER, useValue: override ?? null };
|
|
463
|
+
}
|
|
464
|
+
function makeRecoveryCodeStoreProvider(override) {
|
|
465
|
+
return { provide: tokens_js_1.SHIELD_RECOVERY_CODE_STORE, useValue: override ?? null };
|
|
466
|
+
}
|
|
467
|
+
function makeProfileWriterProvider(override) {
|
|
468
|
+
return { provide: tokens_js_1.SHIELD_PROFILE_WRITER, useValue: override ?? null };
|
|
469
|
+
}
|
|
470
|
+
function makeSessionSweeperProvider(override) {
|
|
471
|
+
return { provide: tokens_js_1.SHIELD_SESSION_SWEEPER, useValue: override ?? null };
|
|
472
|
+
}
|
|
473
|
+
function makeMfaDeviceStoreProvider(override) {
|
|
474
|
+
return { provide: tokens_js_1.SHIELD_MFA_DEVICE_STORE, useValue: override ?? null };
|
|
475
|
+
}
|
|
476
|
+
function makeAuditSinkProvider(override) {
|
|
477
|
+
return { provide: tokens_js_1.SHIELD_AUDIT_SINK, useValue: override ?? null };
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Bind the OPTIONAL eager session-facts projector (ADR-0011). Same value-provider
|
|
481
|
+
* pattern as the other seams: the supplied impl when present, else `null` so the auth
|
|
482
|
+
* controller's `@Optional()` injection resolves cleanly to "no projector" (→ it omits
|
|
483
|
+
* `cachedFacts` → gate 5 DB-reloads). Always provided so the DI token exists.
|
|
484
|
+
*/
|
|
485
|
+
function makeSessionFactsProjectorProvider(override) {
|
|
486
|
+
return { provide: tokens_js_1.SHIELD_SESSION_FACTS_PROJECTOR, useValue: override ?? null };
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* The reset-link base URL the forgot-password handler builds the emailed link from
|
|
490
|
+
* (`${base}?token=<rawToken>`). A plain string VALUE (not a seam) resolved from the
|
|
491
|
+
* environment (`RESET_LINK_BASE_URL`) with a dev default, so the framework owns the
|
|
492
|
+
* URL composition and {@link SHIELD_EMAIL_SENDER} stays URL-agnostic. Always
|
|
493
|
+
* provided so the `@Optional()` injection resolves.
|
|
494
|
+
*/
|
|
495
|
+
const resetLinkBaseUrlProvider = {
|
|
496
|
+
provide: tokens_js_1.SHIELD_RESET_LINK_BASE_URL,
|
|
497
|
+
useValue: process.env.RESET_LINK_BASE_URL ?? 'http://localhost:5173/reset-password',
|
|
269
498
|
};
|
|
270
499
|
/**
|
|
271
500
|
* The auth composition module. Imports {@link PersistenceModule} (the live
|
|
@@ -285,13 +514,40 @@ let AuthModule = AuthModule_1 = class AuthModule {
|
|
|
285
514
|
return {
|
|
286
515
|
module: AuthModule_1,
|
|
287
516
|
imports: [persistence_module_js_1.PersistenceModule.forRoot()],
|
|
517
|
+
// (providers list below adds the profile-resolver seam)
|
|
288
518
|
// Team C's four `@Public()` /auth/* endpoints. Registered here (integration
|
|
289
519
|
// wiring) so the routes exist the moment auth is opted in — the controller
|
|
290
520
|
// injects the two composed engines (below) + PendingMfaStore.
|
|
291
|
-
controllers: [auth_controller_js_1.AuthController],
|
|
521
|
+
controllers: [auth_controller_js_1.AuthController, account_controller_js_1.AccountController],
|
|
292
522
|
providers: [
|
|
293
523
|
passwordHasherProvider,
|
|
294
524
|
makeCredentialResolverProvider(options.credentialResolver),
|
|
525
|
+
// The current-user profile seam backing `GET /auth/me` (null when absent).
|
|
526
|
+
makeProfileResolverProvider(options.profileResolver),
|
|
527
|
+
// The account/credential-surface seams (null when absent → route 501).
|
|
528
|
+
makeCredentialWriterProvider(options.credentialWriter),
|
|
529
|
+
makeResetTokenStoreProvider(options.resetTokenStore),
|
|
530
|
+
makeEmailSenderProvider(options.emailSender),
|
|
531
|
+
makeRecoveryCodeStoreProvider(options.recoveryCodeStore),
|
|
532
|
+
makeProfileWriterProvider(options.profileWriter),
|
|
533
|
+
makeSessionSweeperProvider(options.sessionSweeper),
|
|
534
|
+
// The auth-surface audit sink (null when absent → recovery-login audit no-ops).
|
|
535
|
+
makeAuditSinkProvider(options.auditSink),
|
|
536
|
+
// The multi-device MFA store (null when absent → device routes 501).
|
|
537
|
+
makeMfaDeviceStoreProvider(options.mfaDeviceStore),
|
|
538
|
+
// The login totp-verifier override (null when absent → default RFC-6238
|
|
539
|
+
// single-secret verify). Consumed by the authenticator factory.
|
|
540
|
+
makeTotpVerifierOverrideProvider(options.totpVerifier),
|
|
541
|
+
// The root allowlist (null when absent → broker default-deny).
|
|
542
|
+
// Consumed by the authenticator factory (createIdentityBroker). ROOT
|
|
543
|
+
// SAFETY: the ONLY source of the isRoot flag.
|
|
544
|
+
makeRootIdsProvider(options.rootIds),
|
|
545
|
+
// The eager session-facts projector (ADR-0011; null when absent → the auth
|
|
546
|
+
// controller omits cachedFacts → gate 5 DB-reloads). Consumed by AuthController.
|
|
547
|
+
makeSessionFactsProjectorProvider(options.sessionFactsProjector),
|
|
548
|
+
// The reset-link base URL value the forgot-password handler composes into the
|
|
549
|
+
// emailed link (framework-owned URL composition; EmailSender stays dumb).
|
|
550
|
+
resetLinkBaseUrlProvider,
|
|
295
551
|
sessionRecordStoreProvider,
|
|
296
552
|
hotSessionStoreProvider,
|
|
297
553
|
tokenGeneratorProvider,
|
|
@@ -299,15 +555,51 @@ let AuthModule = AuthModule_1 = class AuthModule {
|
|
|
299
555
|
clockProvider,
|
|
300
556
|
sessionAuthorityProvider,
|
|
301
557
|
authenticatorProvider,
|
|
558
|
+
// The core TOTP verifier (login MFA + account MFA enroll/disable share it).
|
|
559
|
+
totpVerifierProvider,
|
|
302
560
|
// Team C's server-side, single-use, TTL'd challenge store backing the
|
|
303
561
|
// login→mfa handoff (a plain class provider the controller injects).
|
|
304
562
|
pending_mfa_store_js_1.PendingMfaStore,
|
|
563
|
+
// The request-auth token binder (access-token → principal claims).
|
|
564
|
+
sessionTokenBinderProvider,
|
|
565
|
+
// The anti-brute-force rate limiters (spec §5): login (H1) + recovery, each
|
|
566
|
+
// a Redis-injected AttemptLimiter with its own threshold. Injected into
|
|
567
|
+
// AuthController (@Optional()).
|
|
568
|
+
loginLimiterProvider,
|
|
569
|
+
recoveryLimiterProvider,
|
|
570
|
+
// The forgot/reset-password limiters (feature #141): forgot keyed by client IP
|
|
571
|
+
// + per-hint (request-rate caps → silently skip the send when exceeded, stays
|
|
572
|
+
// 204); reset keyed by client IP (may 429 on lockout — token-gated, not enum-
|
|
573
|
+
// sensitive). Injected into AuthController (@Optional()).
|
|
574
|
+
forgotLimiterProvider,
|
|
575
|
+
forgotHintLimiterProvider,
|
|
576
|
+
resetLimiterProvider,
|
|
305
577
|
],
|
|
306
578
|
exports: [
|
|
307
579
|
tokens_js_1.SHIELD_AUTHENTICATOR,
|
|
308
580
|
tokens_js_1.SHIELD_SESSION_AUTHORITY,
|
|
309
581
|
tokens_js_1.SHIELD_CREDENTIAL_RESOLVER,
|
|
310
582
|
argon2id_password_hasher_js_1.SHIELD_PASSWORD_HASHER,
|
|
583
|
+
// Exported so the (app-wide) enforcement guard can inject it.
|
|
584
|
+
tokens_js_2.SHIELD_SESSION_TOKEN_BINDER,
|
|
585
|
+
// The account/credential-surface seams — exported so the consumer's impls
|
|
586
|
+
// and the account/auth controllers see them app-wide through @Global Shield.
|
|
587
|
+
tokens_js_1.SHIELD_CREDENTIAL_WRITER,
|
|
588
|
+
tokens_js_1.SHIELD_RESET_TOKEN_STORE,
|
|
589
|
+
tokens_js_1.SHIELD_EMAIL_SENDER,
|
|
590
|
+
tokens_js_1.SHIELD_RECOVERY_CODE_STORE,
|
|
591
|
+
tokens_js_1.SHIELD_PROFILE_WRITER,
|
|
592
|
+
tokens_js_1.SHIELD_SESSION_SWEEPER,
|
|
593
|
+
// The auth-surface audit sink (consumed by the /auth/mfa recovery branch).
|
|
594
|
+
tokens_js_1.SHIELD_AUDIT_SINK,
|
|
595
|
+
// The multi-device MFA store (consumed by /account/mfa/devices/*).
|
|
596
|
+
tokens_js_1.SHIELD_MFA_DEVICE_STORE,
|
|
597
|
+
// The reset-link base URL value (consumed by /auth/forgot-password).
|
|
598
|
+
tokens_js_1.SHIELD_RESET_LINK_BASE_URL,
|
|
599
|
+
// The core TOTP verifier (consumed by /account/mfa/{enroll/confirm,disable}).
|
|
600
|
+
tokens_js_1.SHIELD_TOTP_VERIFIER,
|
|
601
|
+
// The eager session-facts projector (consumed by AuthController).
|
|
602
|
+
tokens_js_1.SHIELD_SESSION_FACTS_PROJECTOR,
|
|
311
603
|
],
|
|
312
604
|
};
|
|
313
605
|
}
|
package/dist/auth/dto.d.ts
CHANGED
|
@@ -56,6 +56,14 @@ export interface SessionTokensView {
|
|
|
56
56
|
export interface SessionSuccess {
|
|
57
57
|
readonly session: SessionView;
|
|
58
58
|
readonly tokens: SessionTokensView;
|
|
59
|
+
/**
|
|
60
|
+
* Set to `true` ONLY when the session was completed via a RECOVERY CODE at the
|
|
61
|
+
* `/auth/mfa` step (the lost-device escape hatch). Absent on every normal
|
|
62
|
+
* password / TOTP / refresh success, so those paths are byte-for-byte
|
|
63
|
+
* unaffected. The frontend keys off this to route the recovered session into
|
|
64
|
+
* the enforced re-enroll flow. Carries NO secret — a pure signal flag.
|
|
65
|
+
*/
|
|
66
|
+
readonly mfaRecoveryUsed?: boolean;
|
|
59
67
|
}
|
|
60
68
|
/** `POST /auth/login` request body. */
|
|
61
69
|
export interface LoginRequest {
|
|
@@ -85,16 +93,18 @@ export interface MfaChallengeResponse {
|
|
|
85
93
|
export type LoginResponse = SessionSuccess | MfaChallengeResponse;
|
|
86
94
|
/**
|
|
87
95
|
* `POST /auth/mfa` request body — completes a pending MFA challenge. `factor` is
|
|
88
|
-
* `'totp'`
|
|
89
|
-
*
|
|
90
|
-
*
|
|
96
|
+
* `'totp'` (an authenticator code) or `'recovery_code'` (the lost-device escape
|
|
97
|
+
* hatch — a pre-issued single-use recovery code, verified in a separate branch,
|
|
98
|
+
* see `auth.controller.ts`). WebAuthn remains a contract slot only. `code` is the
|
|
99
|
+
* presented one-time code / recovery code. ⛔ `code` is single-use material —
|
|
100
|
+
* never logged.
|
|
91
101
|
*/
|
|
92
102
|
export interface MfaRequest {
|
|
93
103
|
/** The opaque handle from {@link MfaChallengeResponse}. Short-TTL, single-use. */
|
|
94
104
|
readonly challengeToken: string;
|
|
95
|
-
/** The second-factor class being satisfied (
|
|
96
|
-
readonly factor: 'totp';
|
|
97
|
-
/** The presented one-time code. NEVER logged. */
|
|
105
|
+
/** The second-factor class being satisfied (`'totp'` or `'recovery_code'`). */
|
|
106
|
+
readonly factor: 'totp' | 'recovery_code';
|
|
107
|
+
/** The presented one-time code / recovery code. NEVER logged. */
|
|
98
108
|
readonly code: string;
|
|
99
109
|
}
|
|
100
110
|
/**
|
package/dist/auth/dto.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dto.d.ts","sourceRoot":"","sources":["../../src/auth/dto.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AASH;;;;;;GAMG;AACH,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,QAAQ,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,CAAC;IAC9D,oBAAoB;IACpB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,oBAAoB;IACpB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;GAKG;AACH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B;AAED,kFAAkF;AAClF,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAC;CACpC;AAMD,uCAAuC;AACvC,MAAM,WAAW,YAAY;IAC3B,8EAA8E;IAC9E,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,+EAA+E;IAC/E,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;GAMG;AACH,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,WAAW,EAAE,IAAI,CAAC;IAC3B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;CACjC;AAED;;;;;;GAMG;AACH,MAAM,MAAM,aAAa,GAAG,cAAc,GAAG,oBAAoB,CAAC;AAMlE
|
|
1
|
+
{"version":3,"file":"dto.d.ts","sourceRoot":"","sources":["../../src/auth/dto.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AASH;;;;;;GAMG;AACH,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,QAAQ,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,CAAC;IAC9D,oBAAoB;IACpB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,oBAAoB;IACpB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;GAKG;AACH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B;AAED,kFAAkF;AAClF,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAC;IACnC;;;;;;OAMG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,OAAO,CAAC;CACpC;AAMD,uCAAuC;AACvC,MAAM,WAAW,YAAY;IAC3B,8EAA8E;IAC9E,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,+EAA+E;IAC/E,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;GAMG;AACH,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,WAAW,EAAE,IAAI,CAAC;IAC3B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;CACjC;AAED;;;;;;GAMG;AACH,MAAM,MAAM,aAAa,GAAG,cAAc,GAAG,oBAAoB,CAAC;AAMlE;;;;;;;GAOG;AACH,MAAM,WAAW,UAAU;IACzB,kFAAkF;IAClF,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,+EAA+E;IAC/E,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,eAAe,CAAC;IAC1C,iEAAiE;IACjE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED;;;;GAIG;AACH,MAAM,MAAM,WAAW,GAAG,cAAc,CAAC;AAMzC;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,eAAe,GAAG,cAAc,CAAC;AAM7C;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED;;;;GAIG;AACH,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAC;CACxB"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AuditSink — the auth-surface **audit seam** (spec §6.2). The auth surface is
|
|
3
|
+
* otherwise un-audited (only the @astram/engine pipe audits record CRUD); this port
|
|
4
|
+
* introduces the FIRST auth audit event. The framework builds the event; the consumer
|
|
5
|
+
* persists it (astram3 writes `shield_audit.audit_log`).
|
|
6
|
+
*
|
|
7
|
+
* OPTIONAL, consumer-plugged through `ShieldModule.forRoot({ auditSink })` and threaded
|
|
8
|
+
* to `AuthModule.forRoot` exactly like the other account/credential seams. Absent
|
|
9
|
+
* (`null`) → the route still works, just un-audited (the `@Optional()` injection in
|
|
10
|
+
* `AuthController` resolves to no seam → the best-effort call is a no-op).
|
|
11
|
+
*
|
|
12
|
+
* BEST-EFFORT by contract: `record` is invoked fire-and-forget on the recovery-login
|
|
13
|
+
* success path — a throwing or slow sink NEVER fails the (legitimate) login. The
|
|
14
|
+
* consumer's impl should not surface errors into the response path.
|
|
15
|
+
*
|
|
16
|
+
* NO SECRET LEAK: the event carries NO password, recovery code, TOTP secret, or token —
|
|
17
|
+
* only the actor id, tenant, action key, and non-secret attributes.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* A framework-built auth audit event. `type` is the audit category (`'authentication'`
|
|
21
|
+
* for the auth surface); `action` is the specific event key (e.g. `'mfa.recovery_used'`).
|
|
22
|
+
* `attributes` carries non-secret structured detail (never a code/token/secret).
|
|
23
|
+
*/
|
|
24
|
+
export interface AuthAuditEvent {
|
|
25
|
+
/** The audit category. Always `'authentication'` for auth-surface events. */
|
|
26
|
+
readonly type: 'authentication';
|
|
27
|
+
/** When the event occurred (epoch ms). */
|
|
28
|
+
readonly occurredAtMs: number;
|
|
29
|
+
/** The acting principal's id (the account the event is about). */
|
|
30
|
+
readonly actorId: string;
|
|
31
|
+
/** The active tenant/company the event is scoped to, if known. */
|
|
32
|
+
readonly tenantId?: string;
|
|
33
|
+
/** The specific event key, e.g. `'mfa.recovery_used'`. */
|
|
34
|
+
readonly action: string;
|
|
35
|
+
/** Non-secret structured detail. NEVER a code / token / secret. */
|
|
36
|
+
readonly attributes?: Record<string, unknown>;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The port a consumer implements to persist auth audit events. Supplied to the
|
|
40
|
+
* framework via `ShieldModule.forRoot({ auditSink })`. Best-effort: `record` is called
|
|
41
|
+
* fire-and-forget, so an impl error must not propagate into the login response.
|
|
42
|
+
*/
|
|
43
|
+
export interface AuditSink {
|
|
44
|
+
/** Persist one auth audit event. Best-effort — never fails the caller's flow. */
|
|
45
|
+
record(event: AuthAuditEvent): Promise<void>;
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=audit-sink.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit-sink.d.ts","sourceRoot":"","sources":["../../../src/auth/ports/audit-sink.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC7B,6EAA6E;IAC7E,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAC;IAChC,0CAA0C;IAC1C,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,kEAAkE;IAClE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,kEAAkE;IAClE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,0DAA0D;IAC1D,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,mEAAmE;IACnE,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/C;AAED;;;;GAIG;AACH,MAAM,WAAW,SAAS;IACxB,iFAAiF;IACjF,MAAM,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9C"}
|