@astram/guard 0.1.0 → 0.2.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.
Files changed (34) hide show
  1. package/dist/auth/auth.controller.d.ts +102 -0
  2. package/dist/auth/auth.controller.d.ts.map +1 -0
  3. package/dist/auth/auth.controller.js +313 -0
  4. package/dist/auth/auth.module.d.ts +104 -0
  5. package/dist/auth/auth.module.d.ts.map +1 -0
  6. package/dist/auth/auth.module.js +318 -0
  7. package/dist/auth/connections.d.ts +61 -0
  8. package/dist/auth/connections.d.ts.map +1 -0
  9. package/dist/auth/connections.js +116 -0
  10. package/dist/auth/dto.d.ts +144 -0
  11. package/dist/auth/dto.d.ts.map +1 -0
  12. package/dist/auth/dto.js +29 -0
  13. package/dist/auth/pending-mfa-store.d.ts +72 -0
  14. package/dist/auth/pending-mfa-store.d.ts.map +1 -0
  15. package/dist/auth/pending-mfa-store.js +106 -0
  16. package/dist/auth/persistence.module.d.ts +42 -0
  17. package/dist/auth/persistence.module.d.ts.map +1 -0
  18. package/dist/auth/persistence.module.js +94 -0
  19. package/dist/auth/tokens.d.ts +62 -0
  20. package/dist/auth/tokens.d.ts.map +1 -0
  21. package/dist/auth/tokens.js +65 -0
  22. package/dist/authentication/mariadb-credential-resolver.d.ts +96 -0
  23. package/dist/authentication/mariadb-credential-resolver.d.ts.map +1 -0
  24. package/dist/authentication/mariadb-credential-resolver.js +156 -0
  25. package/dist/config/app-config.d.ts +134 -1
  26. package/dist/config/app-config.d.ts.map +1 -1
  27. package/dist/config/app-config.js +144 -2
  28. package/dist/index.d.ts +9 -1
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +46 -1
  31. package/dist/module/shield.module.d.ts +51 -0
  32. package/dist/module/shield.module.d.ts.map +1 -1
  33. package/dist/module/shield.module.js +65 -4
  34. package/package.json +9 -9
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Phase 0b Auth-surface — the framework-owned **`/auth/*` HTTP controllers**
3
+ * (spec §2). A consumer app gets login + MFA + refresh + logout FOR FREE by
4
+ * importing `ShieldModule.forRoot()` (which imports `AuthModule`); it writes no
5
+ * auth code. All four routes are `@Public()` (ADR-0002): they run BEFORE the
6
+ * caller has a session, so the global deny-by-default guard must NOT gate them.
7
+ *
8
+ * This controller is a THIN transport shim over the two composed core engines
9
+ * (`SHIELD_AUTHENTICATOR` / `SHIELD_SESSION_AUTHORITY`, from Team B). It holds NO
10
+ * business logic beyond mapping engine outcomes → HTTP status + the frozen DTO
11
+ * shapes (`./dto.ts`). Every security invariant is enforced here per the DTO
12
+ * header + the spec:
13
+ *
14
+ * ── OUTCOME → HTTP STATUS MAP (the security contract) ────────────────────────
15
+ * login authenticated → 200 SessionSuccess { session(no-secret), tokens }
16
+ * login mfa_required → 200 MfaChallengeResponse { mfaRequired, challengeToken }
17
+ * login invalid_credentials → 401 GENERIC ← unknown-user AND wrong-password IDENTICAL
18
+ * login auth_unavailable → 503 RETRYABLE ← transient blip is NOT a credential deny
19
+ * mfa success → 200 SessionSuccess
20
+ * mfa wrong/expired/forged → 401 GENERIC ← indistinguishable from a bad code
21
+ * mfa auth_unavailable → 503 RETRYABLE
22
+ * refresh ok → 200 SessionSuccess (new tokens, rotated session)
23
+ * refresh reuse/invalid → 401 GENERIC session_rejected (lineage-kill is the engine's)
24
+ * logout → 204 (idempotent; unknown/already-revoked → still 204)
25
+ *
26
+ * ── ANTI-ENUMERATION ── login + mfa failures are a SINGLE generic 401 with an
27
+ * identical body (`{ error: 'invalid_credentials' }`) whether the user is
28
+ * unknown, the password is wrong, or the factor is wrong/expired. The core
29
+ * authenticator's decoy-verify already equalizes timing; this layer never adds a
30
+ * distinguishing field/status.
31
+ *
32
+ * ── NO SECRET LEAK ── no password, hash, TOTP secret, raw token (beyond the two a
33
+ * success legitimately mints), or challenge internal ever appears in a body or a
34
+ * log. Request bodies are NEVER logged. The `SessionView` mapped from the core
35
+ * `Session` carries no secret by construction.
36
+ *
37
+ * ── TRANSIENT ≠ DENY (ADR-0016) ── a transient `authentication_unavailable` from
38
+ * the authenticator becomes a RETRYABLE 503, never a 401 — a dependency blip must
39
+ * not look like (or trigger the client to treat it as) a credential failure.
40
+ * ⚠ ENGINE-SEAM LIMIT on /auth/refresh: the core `SessionAuthority.rotate`
41
+ * collapses BOTH a terminal deny AND a transient store outage into one
42
+ * `SessionRejectedError` (its top-level fail-closed catch — see
43
+ * session-authority.ts). From THIS seam the two are indistinguishable, so refresh
44
+ * maps the generic reject to 401. Surfacing a retryable-503 on a refresh-time
45
+ * store outage would require the engine to expose a transient signal it does not
46
+ * today — flagged to the architect as a possible core-API gap (NOT worked around
47
+ * here; we do not invent a core API).
48
+ *
49
+ * ADR-0023: composition is the framework's job — this controller only CONSUMES the
50
+ * composed engines via DI tokens; it never news-up an engine or an adapter.
51
+ */
52
+ import type { Authenticator, SessionAuthority } from '@astram/core';
53
+ import { PendingMfaStore } from './pending-mfa-store.js';
54
+ import type { LoginRequest, LoginResponse, LogoutRequest, MfaRequest, RefreshRequest, SessionSuccess } from './dto.js';
55
+ /**
56
+ * The framework-owned auth endpoints. Registered into `AuthModule`'s `controllers`
57
+ * by the integration owner / Team B (this controller is EXPORTED for that purpose;
58
+ * it does NOT edit auth.module.ts itself). Depends on the two composed engines +
59
+ * the pending-MFA store (provided alongside it).
60
+ */
61
+ export declare class AuthController {
62
+ private readonly authenticator;
63
+ private readonly sessionAuthority;
64
+ private readonly pendingMfa;
65
+ constructor(authenticator: Authenticator, sessionAuthority: SessionAuthority, pendingMfa: PendingMfaStore);
66
+ /**
67
+ * `POST /auth/login` — verify a password credential and either establish a
68
+ * session, issue an MFA challenge, deny generically, or signal a transient
69
+ * outage. NEVER logs the request body.
70
+ */
71
+ login(body: LoginRequest): Promise<LoginResponse>;
72
+ /**
73
+ * `POST /auth/mfa` — complete a pending MFA login. Consumes the single-use
74
+ * challenge handle (unknown/expired/forged → same generic 401 as a bad code),
75
+ * then RE-RUNS `authenticate(hint, [password, totp])` with the presented code
76
+ * (the authenticator has no standalone factor-verify seam — the full attempt IS
77
+ * the verify). Success → establish + tokens.
78
+ */
79
+ mfa(body: MfaRequest): Promise<SessionSuccess>;
80
+ /**
81
+ * `POST /auth/refresh` — rotate the refresh token. Reuse/invalid/expired →
82
+ * generic 401 (`session_rejected`; the engine already killed the lineage,
83
+ * ADR-0015). ⚠ See the class header: the engine's rotate collapses a transient
84
+ * store outage into the SAME reject, so refresh cannot emit the ADR-0016 503 from
85
+ * this seam today (flagged core-API gap).
86
+ */
87
+ refresh(body: RefreshRequest): Promise<SessionSuccess>;
88
+ /**
89
+ * `POST /auth/logout` — revoke the session. Idempotent + leak-free: revoking an
90
+ * unknown or already-revoked session still returns 204 (the engine's `revoke`
91
+ * swallows and returns void either way), so existence is never revealed.
92
+ */
93
+ logout(body: LogoutRequest): Promise<void>;
94
+ /**
95
+ * Establish a session for an authenticated principal, mapping the engine's
96
+ * generic `SessionRejectedError` (a malformed principal or a store outage at
97
+ * establish time) to a generic 401. We never surface a session-establish failure
98
+ * as a distinct, enumerable outcome.
99
+ */
100
+ private establishOrReject;
101
+ }
102
+ //# sourceMappingURL=auth.controller.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.controller.d.ts","sourceRoot":"","sources":["../../src/auth/auth.controller.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkDG;AAWH,OAAO,KAAK,EACV,aAAa,EAKb,gBAAgB,EAEjB,MAAM,cAAc,CAAC;AAGtB,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,KAAK,EACV,YAAY,EACZ,aAAa,EACb,aAAa,EACb,UAAU,EACV,cAAc,EACd,cAAc,EAEf,MAAM,UAAU,CAAC;AAqElB;;;;;GAKG;AACH,qBACa,cAAc;IAGvB,OAAO,CAAC,QAAQ,CAAC,aAAa;IAE9B,OAAO,CAAC,QAAQ,CAAC,gBAAgB;IACjC,OAAO,CAAC,QAAQ,CAAC,UAAU;gBAHV,aAAa,EAAE,aAAa,EAE5B,gBAAgB,EAAE,gBAAgB,EAClC,UAAU,EAAE,eAAe;IAG9C;;;;OAIG;IAIG,KAAK,CAAS,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,aAAa,CAAC;IAgC/D;;;;;;OAMG;IAIG,GAAG,CAAS,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,cAAc,CAAC;IAkC5D;;;;;;OAMG;IAIG,OAAO,CAAS,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;IAapE;;;;OAIG;IAIG,MAAM,CAAS,IAAI,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAYxD;;;;;OAKG;YACW,iBAAiB;CAOhC"}
@@ -0,0 +1,313 @@
1
+ "use strict";
2
+ /**
3
+ * Phase 0b Auth-surface — the framework-owned **`/auth/*` HTTP controllers**
4
+ * (spec §2). A consumer app gets login + MFA + refresh + logout FOR FREE by
5
+ * importing `ShieldModule.forRoot()` (which imports `AuthModule`); it writes no
6
+ * auth code. All four routes are `@Public()` (ADR-0002): they run BEFORE the
7
+ * caller has a session, so the global deny-by-default guard must NOT gate them.
8
+ *
9
+ * This controller is a THIN transport shim over the two composed core engines
10
+ * (`SHIELD_AUTHENTICATOR` / `SHIELD_SESSION_AUTHORITY`, from Team B). It holds NO
11
+ * business logic beyond mapping engine outcomes → HTTP status + the frozen DTO
12
+ * shapes (`./dto.ts`). Every security invariant is enforced here per the DTO
13
+ * header + the spec:
14
+ *
15
+ * ── OUTCOME → HTTP STATUS MAP (the security contract) ────────────────────────
16
+ * login authenticated → 200 SessionSuccess { session(no-secret), tokens }
17
+ * login mfa_required → 200 MfaChallengeResponse { mfaRequired, challengeToken }
18
+ * login invalid_credentials → 401 GENERIC ← unknown-user AND wrong-password IDENTICAL
19
+ * login auth_unavailable → 503 RETRYABLE ← transient blip is NOT a credential deny
20
+ * mfa success → 200 SessionSuccess
21
+ * mfa wrong/expired/forged → 401 GENERIC ← indistinguishable from a bad code
22
+ * mfa auth_unavailable → 503 RETRYABLE
23
+ * refresh ok → 200 SessionSuccess (new tokens, rotated session)
24
+ * refresh reuse/invalid → 401 GENERIC session_rejected (lineage-kill is the engine's)
25
+ * logout → 204 (idempotent; unknown/already-revoked → still 204)
26
+ *
27
+ * ── ANTI-ENUMERATION ── login + mfa failures are a SINGLE generic 401 with an
28
+ * identical body (`{ error: 'invalid_credentials' }`) whether the user is
29
+ * unknown, the password is wrong, or the factor is wrong/expired. The core
30
+ * authenticator's decoy-verify already equalizes timing; this layer never adds a
31
+ * distinguishing field/status.
32
+ *
33
+ * ── NO SECRET LEAK ── no password, hash, TOTP secret, raw token (beyond the two a
34
+ * success legitimately mints), or challenge internal ever appears in a body or a
35
+ * log. Request bodies are NEVER logged. The `SessionView` mapped from the core
36
+ * `Session` carries no secret by construction.
37
+ *
38
+ * ── TRANSIENT ≠ DENY (ADR-0016) ── a transient `authentication_unavailable` from
39
+ * the authenticator becomes a RETRYABLE 503, never a 401 — a dependency blip must
40
+ * not look like (or trigger the client to treat it as) a credential failure.
41
+ * ⚠ ENGINE-SEAM LIMIT on /auth/refresh: the core `SessionAuthority.rotate`
42
+ * collapses BOTH a terminal deny AND a transient store outage into one
43
+ * `SessionRejectedError` (its top-level fail-closed catch — see
44
+ * session-authority.ts). From THIS seam the two are indistinguishable, so refresh
45
+ * maps the generic reject to 401. Surfacing a retryable-503 on a refresh-time
46
+ * store outage would require the engine to expose a transient signal it does not
47
+ * today — flagged to the architect as a possible core-API gap (NOT worked around
48
+ * here; we do not invent a core API).
49
+ *
50
+ * ADR-0023: composition is the framework's job — this controller only CONSUMES the
51
+ * composed engines via DI tokens; it never news-up an engine or an adapter.
52
+ */
53
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
54
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
55
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
56
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
57
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
58
+ };
59
+ var __metadata = (this && this.__metadata) || function (k, v) {
60
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
61
+ };
62
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
63
+ return function (target, key) { decorator(target, key, paramIndex); }
64
+ };
65
+ Object.defineProperty(exports, "__esModule", { value: true });
66
+ exports.AuthController = void 0;
67
+ const common_1 = require("@nestjs/common");
68
+ const classification_decorator_js_1 = require("../enforcement/classification.decorator.js");
69
+ const tokens_js_1 = require("./tokens.js");
70
+ const pending_mfa_store_js_1 = require("./pending-mfa-store.js");
71
+ /**
72
+ * The core `Authenticator`'s generic transient reason (see authenticator.ts —
73
+ * `REASON_AUTH_UNAVAILABLE`). A `rejected` result carrying THIS reason is the
74
+ * ADR-0016 transient signal → 503; any OTHER rejected reason is a credential deny
75
+ * → generic 401.
76
+ */
77
+ const REASON_AUTH_UNAVAILABLE = 'authentication_unavailable';
78
+ /**
79
+ * The SINGLE generic body for EVERY login/mfa/refresh failure. Deliberately
80
+ * non-distinguishing (anti-enumeration): unknown user, wrong password, wrong/
81
+ * expired/forged factor, and a rejected refresh ALL return this identical body.
82
+ * Carries NO reason detail that could leak which check failed.
83
+ */
84
+ const GENERIC_AUTH_FAILURE = Object.freeze({ error: 'invalid_credentials' });
85
+ /** The generic retryable body for a transient dependency outage (ADR-0016). */
86
+ const RETRYABLE_BODY = Object.freeze({ error: 'authentication_unavailable' });
87
+ /** Throw the generic 401 (indistinguishable across all credential-failure paths). */
88
+ function genericUnauthorized() {
89
+ throw new common_1.HttpException(GENERIC_AUTH_FAILURE, common_1.HttpStatus.UNAUTHORIZED);
90
+ }
91
+ /** Throw the retryable 503 (transient — NOT a deny; the client may retry). */
92
+ function retryableUnavailable() {
93
+ throw new common_1.HttpException(RETRYABLE_BODY, common_1.HttpStatus.SERVICE_UNAVAILABLE);
94
+ }
95
+ /**
96
+ * Map a core `Session` → the client-safe {@link SessionView}. Copies ONLY the
97
+ * non-secret lifecycle fields the DTO allows — there is no token/hash on `Session`
98
+ * to begin with, but this explicit projection guarantees no future `Session` field
99
+ * leaks into a response body by accident.
100
+ */
101
+ function toSessionView(session) {
102
+ return {
103
+ sessionId: session.sessionId,
104
+ principalId: session.principalId,
105
+ tenantId: session.tenantId,
106
+ status: session.status,
107
+ issuedAtMs: session.issuedAtMs,
108
+ expiresAtMs: session.expiresAtMs,
109
+ };
110
+ }
111
+ /** Map a core `SessionEstablishment` → the {@link SessionSuccess} success body. */
112
+ function toSessionSuccess(est) {
113
+ return {
114
+ session: toSessionView(est.session),
115
+ tokens: {
116
+ accessToken: est.tokens.accessToken,
117
+ refreshToken: est.tokens.refreshToken,
118
+ },
119
+ };
120
+ }
121
+ /** Build the password credential the authenticator expects. */
122
+ function passwordCredential(password) {
123
+ return { factor: 'password', payload: { password } };
124
+ }
125
+ /** Build the TOTP credential the authenticator's factor verifier expects (payload.code). */
126
+ function totpCredential(code) {
127
+ return { factor: 'totp', payload: { code } };
128
+ }
129
+ /**
130
+ * The framework-owned auth endpoints. Registered into `AuthModule`'s `controllers`
131
+ * by the integration owner / Team B (this controller is EXPORTED for that purpose;
132
+ * it does NOT edit auth.module.ts itself). Depends on the two composed engines +
133
+ * the pending-MFA store (provided alongside it).
134
+ */
135
+ let AuthController = class AuthController {
136
+ authenticator;
137
+ sessionAuthority;
138
+ pendingMfa;
139
+ constructor(authenticator, sessionAuthority, pendingMfa) {
140
+ this.authenticator = authenticator;
141
+ this.sessionAuthority = sessionAuthority;
142
+ this.pendingMfa = pendingMfa;
143
+ }
144
+ /**
145
+ * `POST /auth/login` — verify a password credential and either establish a
146
+ * session, issue an MFA challenge, deny generically, or signal a transient
147
+ * outage. NEVER logs the request body.
148
+ */
149
+ async login(body) {
150
+ const identityHint = typeof body?.identityHint === 'string' ? body.identityHint : '';
151
+ const password = typeof body?.password === 'string' ? body.password : '';
152
+ const result = await this.authenticator.authenticate(identityHint, [passwordCredential(password)]);
153
+ if (result.outcome === 'authenticated') {
154
+ const principal = requirePrincipal(result);
155
+ const est = await this.establishOrReject(principal);
156
+ return toSessionSuccess(est);
157
+ }
158
+ if (result.outcome === 'mfa_required') {
159
+ // Password verified, a second factor is outstanding. Hold the pending
160
+ // credentials server-side and hand back an OPAQUE, unforgeable challenge
161
+ // handle. NO session yet. The handle carries nothing about the user.
162
+ const challengeToken = this.pendingMfa.store(identityHint, password);
163
+ return { mfaRequired: true, challengeToken };
164
+ }
165
+ // outcome === 'rejected'. Distinguish ONLY transient-vs-deny (never which
166
+ // credential failed): a transient dependency blip is a retryable 503, anything
167
+ // else is the generic 401.
168
+ if (result.reason === REASON_AUTH_UNAVAILABLE) {
169
+ retryableUnavailable();
170
+ }
171
+ genericUnauthorized();
172
+ }
173
+ /**
174
+ * `POST /auth/mfa` — complete a pending MFA login. Consumes the single-use
175
+ * challenge handle (unknown/expired/forged → same generic 401 as a bad code),
176
+ * then RE-RUNS `authenticate(hint, [password, totp])` with the presented code
177
+ * (the authenticator has no standalone factor-verify seam — the full attempt IS
178
+ * the verify). Success → establish + tokens.
179
+ */
180
+ async mfa(body) {
181
+ const challengeToken = typeof body?.challengeToken === 'string' ? body.challengeToken : '';
182
+ const code = typeof body?.code === 'string' ? body.code : '';
183
+ // Phase 0b: only 'totp' is real. A body claiming another factor cannot be
184
+ // satisfied → treat as a generic failure (no leak of "unsupported factor").
185
+ const factor = body?.factor;
186
+ // Consume the pending challenge (single-use). A miss (unknown/forged/expired)
187
+ // is indistinguishable from a wrong code — generic 401, no leak.
188
+ const pending = this.pendingMfa.consume(challengeToken);
189
+ if (pending === null || factor !== 'totp') {
190
+ genericUnauthorized();
191
+ }
192
+ const result = await this.authenticator.authenticate(pending.identityHint, [passwordCredential(pending.password), totpCredential(code)]);
193
+ if (result.outcome === 'authenticated') {
194
+ const principal = requirePrincipal(result);
195
+ const est = await this.establishOrReject(principal);
196
+ return toSessionSuccess(est);
197
+ }
198
+ // Anything else (wrong code → rejected; still-outstanding → mfa_required) is a
199
+ // failure. A transient blip is retryable; everything else is a generic 401.
200
+ // (A repeat mfa_required here means the code did not satisfy the factor.)
201
+ if (result.reason === REASON_AUTH_UNAVAILABLE) {
202
+ retryableUnavailable();
203
+ }
204
+ genericUnauthorized();
205
+ }
206
+ /**
207
+ * `POST /auth/refresh` — rotate the refresh token. Reuse/invalid/expired →
208
+ * generic 401 (`session_rejected`; the engine already killed the lineage,
209
+ * ADR-0015). ⚠ See the class header: the engine's rotate collapses a transient
210
+ * store outage into the SAME reject, so refresh cannot emit the ADR-0016 503 from
211
+ * this seam today (flagged core-API gap).
212
+ */
213
+ async refresh(body) {
214
+ const refreshToken = typeof body?.refreshToken === 'string' ? body.refreshToken : '';
215
+ try {
216
+ const est = await this.sessionAuthority.rotate(refreshToken);
217
+ return toSessionSuccess(est);
218
+ }
219
+ catch {
220
+ // The engine throws only its generic SessionRejectedError (reuse, invalid,
221
+ // expiry, or a swallowed store outage — all indistinguishable here). Map to a
222
+ // generic 401; never leak which, never echo the token.
223
+ genericUnauthorized();
224
+ }
225
+ }
226
+ /**
227
+ * `POST /auth/logout` — revoke the session. Idempotent + leak-free: revoking an
228
+ * unknown or already-revoked session still returns 204 (the engine's `revoke`
229
+ * swallows and returns void either way), so existence is never revealed.
230
+ */
231
+ async logout(body) {
232
+ const sessionId = typeof body?.sessionId === 'string' ? body.sessionId : '';
233
+ // `revoke` is void + best-effort-idempotent by contract; it never throws a
234
+ // resource-existence signal. We still guard against an unexpected throw so a
235
+ // logout never 500s (which could itself be an existence oracle).
236
+ try {
237
+ await this.sessionAuthority.revoke(sessionId);
238
+ }
239
+ catch {
240
+ // Swallow — logout is idempotent + must not leak. 204 regardless.
241
+ }
242
+ }
243
+ /**
244
+ * Establish a session for an authenticated principal, mapping the engine's
245
+ * generic `SessionRejectedError` (a malformed principal or a store outage at
246
+ * establish time) to a generic 401. We never surface a session-establish failure
247
+ * as a distinct, enumerable outcome.
248
+ */
249
+ async establishOrReject(principal) {
250
+ try {
251
+ return await this.sessionAuthority.establish(principal);
252
+ }
253
+ catch {
254
+ genericUnauthorized();
255
+ }
256
+ }
257
+ };
258
+ exports.AuthController = AuthController;
259
+ __decorate([
260
+ (0, classification_decorator_js_1.Public)(),
261
+ (0, common_1.Post)('login'),
262
+ (0, common_1.HttpCode)(common_1.HttpStatus.OK),
263
+ __param(0, (0, common_1.Body)()),
264
+ __metadata("design:type", Function),
265
+ __metadata("design:paramtypes", [Object]),
266
+ __metadata("design:returntype", Promise)
267
+ ], AuthController.prototype, "login", null);
268
+ __decorate([
269
+ (0, classification_decorator_js_1.Public)(),
270
+ (0, common_1.Post)('mfa'),
271
+ (0, common_1.HttpCode)(common_1.HttpStatus.OK),
272
+ __param(0, (0, common_1.Body)()),
273
+ __metadata("design:type", Function),
274
+ __metadata("design:paramtypes", [Object]),
275
+ __metadata("design:returntype", Promise)
276
+ ], AuthController.prototype, "mfa", null);
277
+ __decorate([
278
+ (0, classification_decorator_js_1.Public)(),
279
+ (0, common_1.Post)('refresh'),
280
+ (0, common_1.HttpCode)(common_1.HttpStatus.OK),
281
+ __param(0, (0, common_1.Body)()),
282
+ __metadata("design:type", Function),
283
+ __metadata("design:paramtypes", [Object]),
284
+ __metadata("design:returntype", Promise)
285
+ ], AuthController.prototype, "refresh", null);
286
+ __decorate([
287
+ (0, classification_decorator_js_1.Public)(),
288
+ (0, common_1.Post)('logout'),
289
+ (0, common_1.HttpCode)(common_1.HttpStatus.NO_CONTENT),
290
+ __param(0, (0, common_1.Body)()),
291
+ __metadata("design:type", Function),
292
+ __metadata("design:paramtypes", [Object]),
293
+ __metadata("design:returntype", Promise)
294
+ ], AuthController.prototype, "logout", null);
295
+ exports.AuthController = AuthController = __decorate([
296
+ (0, common_1.Controller)('auth'),
297
+ __param(0, (0, common_1.Inject)(tokens_js_1.SHIELD_AUTHENTICATOR)),
298
+ __param(1, (0, common_1.Inject)(tokens_js_1.SHIELD_SESSION_AUTHORITY)),
299
+ __metadata("design:paramtypes", [Object, Object, pending_mfa_store_js_1.PendingMfaStore])
300
+ ], AuthController);
301
+ /**
302
+ * Narrow an `authenticated` result to its guaranteed principal. The core engine
303
+ * populates `principal` ONLY on `outcome === 'authenticated'`; this asserts that
304
+ * invariant so a missing principal fails closed (generic 401) rather than
305
+ * establishing a session for `undefined`.
306
+ */
307
+ function requirePrincipal(result) {
308
+ const principal = result.principal;
309
+ if (principal === undefined || principal === null) {
310
+ genericUnauthorized();
311
+ }
312
+ return principal;
313
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * `AuthModule` — the Phase 0b **composition seam** that assembles the core auth
3
+ * engines from guard's adapters + the live connections, and exposes them under
4
+ * {@link SHIELD_AUTHENTICATOR} / {@link SHIELD_SESSION_AUTHORITY} for Team C's
5
+ * `/auth/*` controllers (spec §4).
6
+ *
7
+ * ─────────────────────────────────────────────────────────────────────────────
8
+ * Team B (connection + composition) OWNS this file. It wires the frozen tokens to
9
+ * the real engines:
10
+ *
11
+ * SHIELD_PASSWORD_HASHER → new Argon2idPasswordHasher() (the flagged fix —
12
+ * the authenticator factory injects it, so THIS module must provide it).
13
+ * SHIELD_CREDENTIAL_RESOLVER → new MariaDbCredentialResolver(pool) (Team A's
14
+ * class; its `resolve()` body is Team A's — until then it throws not-implemented,
15
+ * which the engine treats as fail-closed `authentication_unavailable`).
16
+ * SHIELD_SESSION_RECORD_STORE → new MariaDbSessionRecordStore(pool).
17
+ * SHIELD_HOT_SESSION_STORE → new RedisHotSessionStore(redis).
18
+ * SHIELD_TOKEN_GENERATOR → new CryptoTokenGenerator().
19
+ * SHIELD_CLOCK → new SystemClock().
20
+ * SHIELD_REFRESH_HASHER → new Sha256RefreshTokenHasher(
21
+ * assertRefreshHmacKey(config.refreshHmacKey)) ⛔ FAIL-CLOSED gate:
22
+ * `assertRefreshHmacKey` THROWS on a blank key, so auth can NEVER compose with
23
+ * an unkeyed refresh-token hasher. This is the boot-time fail-closed point for
24
+ * the required secret (loadAppConfig deliberately does NOT throw, so a
25
+ * non-auth scaffold still boots with no `.env` — see app-config.ts).
26
+ *
27
+ * SHIELD_SESSION_AUTHORITY → createSessionAuthority({ recordStore, hotStore,
28
+ * tokenGenerator, refreshHasher, clock, lifetimes: config.lifetimes }).
29
+ * `createSessionAuthority` validates lifetime bounds eagerly (throws LOUD on a
30
+ * nonsensical config — construction-time, correct).
31
+ *
32
+ * SHIELD_AUTHENTICATOR → createAuthenticator({ hasher, resolver,
33
+ * identityBroker: createIdentityBroker(), // default Internal, empty
34
+ * factorVerifiers: [ createTotpFactorVerifier({ hmacSha1 }) ] }). The TOTP
35
+ * verifier is wired so MFA works; `hmacSha1` is the {@link hmacSha1} node:crypto
36
+ * one-liner (guard owns node:crypto, core does not). `createIdentityBroker()`
37
+ * with no args registers the default Internal adapter with an EMPTY superuser
38
+ * allowlist (default-deny).
39
+ * ─────────────────────────────────────────────────────────────────────────────
40
+ *
41
+ * ⛔ OPT-IN (how the integration owner gates this — for the no-`.env` boot):
42
+ * AuthModule opens live connections (via PersistenceModule) and fires the
43
+ * fail-closed `assertRefreshHmacKey` gate the moment it composes. So it MUST be
44
+ * imported ONLY when a deployment wants auth. `ShieldModule.forRoot(options)`
45
+ * should conditionally add `AuthModule.forRoot()` to its `imports` gated behind an
46
+ * option (e.g. `ShieldModule.forRoot({ auth: true })`) or an env flag:
47
+ *
48
+ * imports: [
49
+ * EnforcementModule, BootModule,
50
+ * ...(options?.auth ? [AuthModule.forRoot()] : []), // ← opt-in
51
+ * ],
52
+ * exports: [APP_CONFIG, SHIELD_DECISION_AUTHORITY,
53
+ * ...(options?.auth ? [SHIELD_AUTHENTICATOR, SHIELD_SESSION_AUTHORITY] : [])],
54
+ *
55
+ * An enforcement-only app that does NOT opt in never imports AuthModule → never
56
+ * opens a pool/redis → never calls `assertRefreshHmacKey` → keeps the no-`.env`
57
+ * boot. An auth-enabled app fails LOUD at boot if `SHIELD_REFRESH_HMAC_KEY` is
58
+ * blank — auth can never compose unkeyed. `APP_CONFIG` is already `@Global` from
59
+ * `ShieldModule`, so AuthModule's config-injecting factories resolve. See DONE.md.
60
+ *
61
+ * ADR-0003: the core factories (`createAuthenticator`/`createSessionAuthority`/
62
+ * `createIdentityBroker`/`createTotpFactorVerifier`) are pure; the adapters + the
63
+ * node:crypto HMAC live in guard. Core stays zero-dep + unchanged.
64
+ */
65
+ import { type DynamicModule } from '@nestjs/common';
66
+ import type { CredentialResolver, HmacSha1 } from '@astram/core';
67
+ /**
68
+ * The injected HMAC-SHA1 primitive the core TOTP verifier needs (Core is zero-dep
69
+ * + node-type-free, so it cannot import `node:crypto`). Guard owns node:crypto and
70
+ * supplies this one-liner: raw key bytes + message bytes → the 20-byte HMAC-SHA1
71
+ * digest. Never logs the key or message.
72
+ */
73
+ export declare const hmacSha1: HmacSha1;
74
+ /**
75
+ * Options for {@link AuthModule.forRoot} — registration-time collaborators the
76
+ * consumer plugs through {@link ShieldModule.forRoot}.
77
+ */
78
+ export interface AuthModuleOptions {
79
+ /**
80
+ * OPTIONAL custom credential resolver (the store login checks against). Omit →
81
+ * the framework's default {@link MariaDbCredentialResolver} (reads
82
+ * `shield_identity.identity_credential`). Supply a consumer implementation of the
83
+ * core `CredentialResolver` port to authenticate against your OWN user/identity
84
+ * schema. Only meaningful here (AuthModule is only imported under `auth: true`).
85
+ */
86
+ readonly credentialResolver?: CredentialResolver;
87
+ }
88
+ /**
89
+ * The auth composition module. Imports {@link PersistenceModule} (the live
90
+ * connections) and provides the adapters + composed engines. Exports the two
91
+ * engines (Team C consumes them) and the resolver token. Opt-in: imported ONLY by
92
+ * an auth-enabled `ShieldModule.forRoot({ auth: true })` — see the file header +
93
+ * DONE.md for the exact gating the integration owner applies.
94
+ */
95
+ export declare class AuthModule {
96
+ /**
97
+ * Compose the auth surface. `options.credentialResolver`, when supplied, REPLACES
98
+ * the default MariaDb store with the consumer's own (a value provider under
99
+ * {@link SHIELD_CREDENTIAL_RESOLVER}); omitted → the historical default binding,
100
+ * EXACTLY unchanged (zero behavior change for existing consumers).
101
+ */
102
+ static forRoot(options?: AuthModuleOptions): DynamicModule;
103
+ }
104
+ //# sourceMappingURL=auth.module.d.ts.map
@@ -0,0 +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,EACT,MAAM,cAAc,CAAC;AA+BtB;;;;;GAKG;AACH,eAAO,MAAM,QAAQ,EAAE,QACqE,CAAC;AAiL7F;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC;;;;;;OAMG;IACH,QAAQ,CAAC,kBAAkB,CAAC,EAAE,kBAAkB,CAAC;CAClD;AAED;;;;;;GAMG;AACH,qBACa,UAAU;IACrB;;;;;OAKG;IACH,MAAM,CAAC,OAAO,CAAC,OAAO,GAAE,iBAAsB,GAAG,aAAa;CA8B/D"}