@astram/guard 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth/account.controller.d.ts +245 -0
- package/dist/auth/account.controller.d.ts.map +1 -0
- package/dist/auth/account.controller.js +673 -0
- package/dist/auth/account.controller.js.map +1 -0
- package/dist/auth/auth.controller.d.ts +316 -0
- package/dist/auth/auth.controller.d.ts.map +1 -0
- package/dist/auth/auth.controller.js +1091 -0
- package/dist/auth/auth.controller.js.map +1 -0
- package/dist/auth/auth.module.d.ts +202 -0
- package/dist/auth/auth.module.d.ts.map +1 -0
- package/dist/auth/auth.module.js +610 -0
- package/dist/auth/auth.module.js.map +1 -0
- package/dist/auth/connections.d.ts +61 -0
- package/dist/auth/connections.d.ts.map +1 -0
- package/dist/auth/connections.js +116 -0
- package/dist/auth/connections.js.map +1 -0
- package/dist/auth/dto.d.ts +154 -0
- package/dist/auth/dto.d.ts.map +1 -0
- package/dist/auth/dto.js +29 -0
- package/dist/auth/dto.js.map +1 -0
- package/dist/auth/pending-mfa-store.d.ts +72 -0
- package/dist/auth/pending-mfa-store.d.ts.map +1 -0
- package/dist/auth/pending-mfa-store.js +106 -0
- package/dist/auth/pending-mfa-store.js.map +1 -0
- package/dist/auth/persistence.module.d.ts +42 -0
- package/dist/auth/persistence.module.d.ts.map +1 -0
- package/dist/auth/persistence.module.js +94 -0
- package/dist/auth/persistence.module.js.map +1 -0
- package/dist/auth/ports/audit-sink.d.ts +47 -0
- package/dist/auth/ports/audit-sink.d.ts.map +1 -0
- package/dist/auth/ports/audit-sink.js +20 -0
- package/dist/auth/ports/audit-sink.js.map +1 -0
- package/dist/auth/ports/credential-writer.d.ts +23 -0
- package/dist/auth/ports/credential-writer.d.ts.map +1 -0
- package/dist/auth/ports/credential-writer.js +2 -0
- package/dist/auth/ports/credential-writer.js.map +1 -0
- package/dist/auth/ports/email-sender.d.ts +14 -0
- package/dist/auth/ports/email-sender.d.ts.map +1 -0
- package/dist/auth/ports/email-sender.js +2 -0
- package/dist/auth/ports/email-sender.js.map +1 -0
- package/dist/auth/ports/mfa-device-store.d.ts +45 -0
- package/dist/auth/ports/mfa-device-store.d.ts.map +1 -0
- package/dist/auth/ports/mfa-device-store.js +13 -0
- package/dist/auth/ports/mfa-device-store.js.map +1 -0
- package/dist/auth/ports/profile-writer.d.ts +12 -0
- package/dist/auth/ports/profile-writer.d.ts.map +1 -0
- package/dist/auth/ports/profile-writer.js +2 -0
- package/dist/auth/ports/profile-writer.js.map +1 -0
- package/dist/auth/ports/recovery-code-store.d.ts +10 -0
- package/dist/auth/ports/recovery-code-store.d.ts.map +1 -0
- package/dist/auth/ports/recovery-code-store.js +2 -0
- package/dist/auth/ports/recovery-code-store.js.map +1 -0
- package/dist/auth/ports/reset-token-store.d.ts +12 -0
- package/dist/auth/ports/reset-token-store.d.ts.map +1 -0
- package/dist/auth/ports/reset-token-store.js +2 -0
- package/dist/auth/ports/reset-token-store.js.map +1 -0
- package/dist/auth/ports/session-facts-projector.d.ts +44 -0
- package/dist/auth/ports/session-facts-projector.d.ts.map +1 -0
- package/dist/auth/ports/session-facts-projector.js +30 -0
- package/dist/auth/ports/session-facts-projector.js.map +1 -0
- package/dist/auth/ports/session-sweeper.d.ts +12 -0
- package/dist/auth/ports/session-sweeper.d.ts.map +1 -0
- package/dist/auth/ports/session-sweeper.js +2 -0
- package/dist/auth/ports/session-sweeper.js.map +1 -0
- package/dist/auth/profile-resolver.d.ts +73 -0
- package/dist/auth/profile-resolver.d.ts.map +1 -0
- package/dist/auth/profile-resolver.js +28 -0
- package/dist/auth/profile-resolver.js.map +1 -0
- package/dist/auth/rate-limit/attempt-limiter.d.ts +91 -0
- package/dist/auth/rate-limit/attempt-limiter.d.ts.map +1 -0
- package/dist/auth/rate-limit/attempt-limiter.js +133 -0
- package/dist/auth/rate-limit/attempt-limiter.js.map +1 -0
- package/dist/auth/tokens.d.ts +236 -0
- package/dist/auth/tokens.d.ts.map +1 -0
- package/dist/auth/tokens.js +254 -0
- package/dist/auth/tokens.js.map +1 -0
- package/dist/auth/totp-enrollment.d.ts +42 -0
- package/dist/auth/totp-enrollment.d.ts.map +1 -0
- package/dist/auth/totp-enrollment.js +96 -0
- package/dist/auth/totp-enrollment.js.map +1 -0
- package/dist/authentication/argon2id-password-hasher.js.map +1 -0
- package/dist/authentication/mariadb-credential-resolver.d.ts +110 -0
- package/dist/authentication/mariadb-credential-resolver.d.ts.map +1 -0
- package/dist/authentication/mariadb-credential-resolver.js +170 -0
- package/dist/authentication/mariadb-credential-resolver.js.map +1 -0
- package/dist/authz/cross-tenant-grant-writer.d.ts +259 -0
- package/dist/authz/cross-tenant-grant-writer.d.ts.map +1 -0
- package/dist/authz/cross-tenant-grant-writer.js +397 -0
- package/dist/authz/cross-tenant-grant-writer.js.map +1 -0
- package/dist/authz/manifest-operation-authority-adapter.d.ts +65 -0
- package/dist/authz/manifest-operation-authority-adapter.d.ts.map +1 -0
- package/dist/authz/manifest-operation-authority-adapter.js +108 -0
- package/dist/authz/manifest-operation-authority-adapter.js.map +1 -0
- package/dist/authz/mariadb-cross-tenant-grant-adapter.d.ts +90 -0
- package/dist/authz/mariadb-cross-tenant-grant-adapter.d.ts.map +1 -0
- package/dist/authz/mariadb-cross-tenant-grant-adapter.js +211 -0
- package/dist/authz/mariadb-cross-tenant-grant-adapter.js.map +1 -0
- package/dist/authz/mariadb-record-rule-adapter.d.ts +60 -0
- package/dist/authz/mariadb-record-rule-adapter.d.ts.map +1 -0
- package/dist/authz/mariadb-record-rule-adapter.js +154 -0
- package/dist/authz/mariadb-record-rule-adapter.js.map +1 -0
- package/dist/authz/mariadb-record-share-adapter.d.ts +75 -0
- package/dist/authz/mariadb-record-share-adapter.d.ts.map +1 -0
- package/dist/authz/mariadb-record-share-adapter.js +106 -0
- package/dist/authz/mariadb-record-share-adapter.js.map +1 -0
- package/dist/authz/record-share-cascade.d.ts +52 -0
- package/dist/authz/record-share-cascade.d.ts.map +1 -0
- package/dist/authz/record-share-cascade.js +49 -0
- package/dist/authz/record-share-cascade.js.map +1 -0
- package/dist/authz/record-share-writer.d.ts +175 -0
- package/dist/authz/record-share-writer.d.ts.map +1 -0
- package/dist/authz/record-share-writer.js +225 -0
- package/dist/authz/record-share-writer.js.map +1 -0
- package/dist/authz/wire-record-authorization.d.ts +66 -0
- package/dist/authz/wire-record-authorization.d.ts.map +1 -0
- package/dist/authz/wire-record-authorization.js +34 -0
- package/dist/authz/wire-record-authorization.js.map +1 -0
- package/dist/boot/boot-sequence.js.map +1 -0
- package/dist/boot/boot.module.js.map +1 -0
- package/dist/completeness/catalog-reconciler.d.ts +21 -0
- package/dist/completeness/catalog-reconciler.d.ts.map +1 -0
- package/dist/completeness/catalog-reconciler.js +74 -0
- package/dist/completeness/catalog-reconciler.js.map +1 -0
- package/dist/completeness/operation-inventory-scanner.d.ts +17 -0
- package/dist/completeness/operation-inventory-scanner.d.ts.map +1 -0
- package/dist/completeness/operation-inventory-scanner.js +79 -0
- package/dist/completeness/operation-inventory-scanner.js.map +1 -0
- package/dist/completeness/ports/catalog-store.d.ts +33 -0
- package/dist/completeness/ports/catalog-store.d.ts.map +1 -0
- package/dist/completeness/ports/catalog-store.js +2 -0
- package/dist/completeness/ports/catalog-store.js.map +1 -0
- package/dist/config/app-config.d.ts +134 -1
- package/dist/config/app-config.d.ts.map +1 -1
- package/dist/config/app-config.js +144 -2
- package/dist/config/app-config.js.map +1 -0
- package/dist/config/config.module.js.map +1 -0
- package/dist/enforcement/classification.decorator.d.ts +33 -14
- package/dist/enforcement/classification.decorator.d.ts.map +1 -1
- package/dist/enforcement/classification.decorator.js +34 -14
- package/dist/enforcement/classification.decorator.js.map +1 -0
- package/dist/enforcement/core-seam.js.map +1 -0
- package/dist/enforcement/enforcement.module.js.map +1 -0
- package/dist/enforcement/global-enforcement.guard.d.ts +29 -19
- package/dist/enforcement/global-enforcement.guard.d.ts.map +1 -1
- package/dist/enforcement/global-enforcement.guard.js +108 -25
- package/dist/enforcement/global-enforcement.guard.js.map +1 -0
- package/dist/enforcement/reenroll-allowlist.d.ts +49 -0
- package/dist/enforcement/reenroll-allowlist.d.ts.map +1 -0
- package/dist/enforcement/reenroll-allowlist.js +77 -0
- package/dist/enforcement/reenroll-allowlist.js.map +1 -0
- package/dist/index.d.ts +43 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +198 -1
- package/dist/index.js.map +1 -0
- package/dist/module/shield-tokens.d.ts +15 -0
- package/dist/module/shield-tokens.d.ts.map +1 -0
- package/dist/module/shield-tokens.js +17 -0
- package/dist/module/shield-tokens.js.map +1 -0
- package/dist/module/shield.module.d.ts +203 -8
- package/dist/module/shield.module.d.ts.map +1 -1
- package/dist/module/shield.module.js +232 -14
- package/dist/module/shield.module.js.map +1 -0
- package/dist/request-auth/request-principal.d.ts +42 -0
- package/dist/request-auth/request-principal.d.ts.map +1 -0
- package/dist/request-auth/request-principal.js +87 -0
- package/dist/request-auth/request-principal.js.map +1 -0
- package/dist/request-auth/session-facts-als.d.ts +65 -0
- package/dist/request-auth/session-facts-als.d.ts.map +1 -0
- package/dist/request-auth/session-facts-als.js +57 -0
- package/dist/request-auth/session-facts-als.js.map +1 -0
- package/dist/request-auth/session-token-binder.d.ts +130 -0
- package/dist/request-auth/session-token-binder.d.ts.map +1 -0
- package/dist/request-auth/session-token-binder.js +277 -0
- package/dist/request-auth/session-token-binder.js.map +1 -0
- package/dist/request-auth/tokens.d.ts +14 -0
- package/dist/request-auth/tokens.d.ts.map +1 -0
- package/dist/request-auth/tokens.js +16 -0
- package/dist/request-auth/tokens.js.map +1 -0
- package/dist/session/crypto-adapters.js.map +1 -0
- package/dist/session/index.js.map +1 -0
- package/dist/session/mariadb-session-record-store.js.map +1 -0
- package/dist/session/redis-hot-session-store.js.map +1 -0
- package/package.json +2 -2
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live-connection factories + a shutdown registrar for the Phase 0b auth surface
|
|
3
|
+
* (Team B). This is the ONE module in the framework that opens a real MariaDB pool
|
|
4
|
+
* and a real Redis client (spec §4); everything auth persists (sessions,
|
|
5
|
+
* credentials, hot state) flows through the two connections created here.
|
|
6
|
+
*
|
|
7
|
+
* WHY a separate helper (not inline in the provider factory): a Nest `useFactory`
|
|
8
|
+
* value provider is NOT itself lifecycle-aware — `onModuleDestroy` only fires on
|
|
9
|
+
* PROVIDER CLASSES with that method. So the pool/redis are created by the pure
|
|
10
|
+
* factories below, and a tiny {@link ConnectionLifecycle} provider (a class Nest
|
|
11
|
+
* WILL call `onModuleDestroy` on) holds their handles and closes them on shutdown.
|
|
12
|
+
* This keeps `SHIELD_DB_POOL` / `SHIELD_REDIS` as the raw driver objects the
|
|
13
|
+
* adapters expect (`new MariaDbSessionRecordStore(pool)` etc.) while still getting
|
|
14
|
+
* graceful teardown.
|
|
15
|
+
*
|
|
16
|
+
* ⛔ SECURITY — never logs a credential. The DB password + Redis url (which may
|
|
17
|
+
* carry a password) are handed straight to the drivers; nothing here stringifies
|
|
18
|
+
* or logs the config.
|
|
19
|
+
*
|
|
20
|
+
* ADR-0003: the real `mariadb` / `ioredis` live in guard (Core stays zero-dep).
|
|
21
|
+
* ADR-0018: the pool cap is taken from the connection-pool budget
|
|
22
|
+
* (`config.pool.perAccountMax.shield_core`), NOT a duplicate knob.
|
|
23
|
+
*/
|
|
24
|
+
import { type OnModuleDestroy } from '@nestjs/common';
|
|
25
|
+
import { type Pool } from 'mariadb';
|
|
26
|
+
import Redis from 'ioredis';
|
|
27
|
+
import type { AppConfig } from '../config/app-config.js';
|
|
28
|
+
/**
|
|
29
|
+
* Create the ONE live `mariadb` {@link Pool} for the `shield_core` account from
|
|
30
|
+
* {@link AppConfig}. The `connectionLimit` comes from the ADR-0018 budget
|
|
31
|
+
* (`config.pool.perAccountMax.shield_core`) so per-worker pools stay within the DB
|
|
32
|
+
* `max_connections` ceiling — it is NOT a separate configurable.
|
|
33
|
+
*
|
|
34
|
+
* A blank `db.password` is ALLOWED here (it is not hard-required at config load);
|
|
35
|
+
* a wrong/blank password fails LOUD at connect time, which is the correct place
|
|
36
|
+
* for a runtime credential error. The host/port/user/database always have
|
|
37
|
+
* documented defaults, so the descriptor is always well-formed.
|
|
38
|
+
*/
|
|
39
|
+
export declare function createDbPool(config: AppConfig): Pool;
|
|
40
|
+
/**
|
|
41
|
+
* Create the ONE live `ioredis` client (the hot session tier, ADR-0013) from
|
|
42
|
+
* {@link AppConfig}. `config.redis.url` is the canonical `ioredis`-native form
|
|
43
|
+
* (`redis://[:pw@]host:port[/db]`) with a documented default. Lazy-connect is left
|
|
44
|
+
* at ioredis's default (connects on first command); a dead Redis surfaces as the
|
|
45
|
+
* `unavailable` hot-store outcome (fail-closed), never a silent `absent`.
|
|
46
|
+
*/
|
|
47
|
+
export declare function createRedisClient(config: AppConfig): Redis;
|
|
48
|
+
/**
|
|
49
|
+
* Lifecycle-aware holder that owns the pool + redis handles and closes them on
|
|
50
|
+
* app shutdown. Provided ALONGSIDE the two raw-connection providers so Nest calls
|
|
51
|
+
* `onModuleDestroy` here (value providers get no lifecycle callback). The teardown
|
|
52
|
+
* is best-effort + idempotent: a close error is swallowed so shutdown never hangs
|
|
53
|
+
* or throws, and both connections are always attempted.
|
|
54
|
+
*/
|
|
55
|
+
export declare class ConnectionLifecycle implements OnModuleDestroy {
|
|
56
|
+
private readonly pool;
|
|
57
|
+
private readonly redis;
|
|
58
|
+
constructor(pool: Pool, redis: Redis);
|
|
59
|
+
onModuleDestroy(): Promise<void>;
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=connections.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connections.d.ts","sourceRoot":"","sources":["../../src/auth/connections.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAc,KAAK,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAClE,OAAgB,EAAE,KAAK,IAAI,EAAE,MAAM,SAAS,CAAC;AAC7C,OAAO,KAAK,MAAM,SAAS,CAAC;AAC5B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAEzD;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CAcpD;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,SAAS,GAAG,KAAK,CAI1D;AAED;;;;;;GAMG;AACH,qBACa,mBAAoB,YAAW,eAAe;IAEvD,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,KAAK;gBADL,IAAI,EAAE,IAAI,EACV,KAAK,EAAE,KAAK;IAGzB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC;CAavC"}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Live-connection factories + a shutdown registrar for the Phase 0b auth surface
|
|
4
|
+
* (Team B). This is the ONE module in the framework that opens a real MariaDB pool
|
|
5
|
+
* and a real Redis client (spec §4); everything auth persists (sessions,
|
|
6
|
+
* credentials, hot state) flows through the two connections created here.
|
|
7
|
+
*
|
|
8
|
+
* WHY a separate helper (not inline in the provider factory): a Nest `useFactory`
|
|
9
|
+
* value provider is NOT itself lifecycle-aware — `onModuleDestroy` only fires on
|
|
10
|
+
* PROVIDER CLASSES with that method. So the pool/redis are created by the pure
|
|
11
|
+
* factories below, and a tiny {@link ConnectionLifecycle} provider (a class Nest
|
|
12
|
+
* WILL call `onModuleDestroy` on) holds their handles and closes them on shutdown.
|
|
13
|
+
* This keeps `SHIELD_DB_POOL` / `SHIELD_REDIS` as the raw driver objects the
|
|
14
|
+
* adapters expect (`new MariaDbSessionRecordStore(pool)` etc.) while still getting
|
|
15
|
+
* graceful teardown.
|
|
16
|
+
*
|
|
17
|
+
* ⛔ SECURITY — never logs a credential. The DB password + Redis url (which may
|
|
18
|
+
* carry a password) are handed straight to the drivers; nothing here stringifies
|
|
19
|
+
* or logs the config.
|
|
20
|
+
*
|
|
21
|
+
* ADR-0003: the real `mariadb` / `ioredis` live in guard (Core stays zero-dep).
|
|
22
|
+
* ADR-0018: the pool cap is taken from the connection-pool budget
|
|
23
|
+
* (`config.pool.perAccountMax.shield_core`), NOT a duplicate knob.
|
|
24
|
+
*/
|
|
25
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
26
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
27
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
28
|
+
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;
|
|
29
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
30
|
+
};
|
|
31
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
32
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
33
|
+
};
|
|
34
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
35
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
36
|
+
};
|
|
37
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
38
|
+
exports.ConnectionLifecycle = void 0;
|
|
39
|
+
exports.createDbPool = createDbPool;
|
|
40
|
+
exports.createRedisClient = createRedisClient;
|
|
41
|
+
const common_1 = require("@nestjs/common");
|
|
42
|
+
const mariadb_1 = __importDefault(require("mariadb"));
|
|
43
|
+
const ioredis_1 = __importDefault(require("ioredis"));
|
|
44
|
+
/**
|
|
45
|
+
* Create the ONE live `mariadb` {@link Pool} for the `shield_core` account from
|
|
46
|
+
* {@link AppConfig}. The `connectionLimit` comes from the ADR-0018 budget
|
|
47
|
+
* (`config.pool.perAccountMax.shield_core`) so per-worker pools stay within the DB
|
|
48
|
+
* `max_connections` ceiling — it is NOT a separate configurable.
|
|
49
|
+
*
|
|
50
|
+
* A blank `db.password` is ALLOWED here (it is not hard-required at config load);
|
|
51
|
+
* a wrong/blank password fails LOUD at connect time, which is the correct place
|
|
52
|
+
* for a runtime credential error. The host/port/user/database always have
|
|
53
|
+
* documented defaults, so the descriptor is always well-formed.
|
|
54
|
+
*/
|
|
55
|
+
function createDbPool(config) {
|
|
56
|
+
const { db, pool } = config;
|
|
57
|
+
return mariadb_1.default.createPool({
|
|
58
|
+
host: db.host,
|
|
59
|
+
port: db.port,
|
|
60
|
+
user: db.user,
|
|
61
|
+
password: db.password,
|
|
62
|
+
database: db.database,
|
|
63
|
+
connectionLimit: pool.perAccountMax.shield_core,
|
|
64
|
+
// Fail fast on an unreachable DB rather than hanging the boot indefinitely.
|
|
65
|
+
connectTimeout: 10_000,
|
|
66
|
+
// All platform timestamps are UTC (parity with the session adapters).
|
|
67
|
+
timezone: 'Z',
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Create the ONE live `ioredis` client (the hot session tier, ADR-0013) from
|
|
72
|
+
* {@link AppConfig}. `config.redis.url` is the canonical `ioredis`-native form
|
|
73
|
+
* (`redis://[:pw@]host:port[/db]`) with a documented default. Lazy-connect is left
|
|
74
|
+
* at ioredis's default (connects on first command); a dead Redis surfaces as the
|
|
75
|
+
* `unavailable` hot-store outcome (fail-closed), never a silent `absent`.
|
|
76
|
+
*/
|
|
77
|
+
function createRedisClient(config) {
|
|
78
|
+
// `maxRetriesPerRequest: null` keeps a transient blip from turning into a thrown
|
|
79
|
+
// command (the hot-store adapter classifies unreachable as `unavailable`).
|
|
80
|
+
return new ioredis_1.default(config.redis.url, { maxRetriesPerRequest: null, lazyConnect: false });
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Lifecycle-aware holder that owns the pool + redis handles and closes them on
|
|
84
|
+
* app shutdown. Provided ALONGSIDE the two raw-connection providers so Nest calls
|
|
85
|
+
* `onModuleDestroy` here (value providers get no lifecycle callback). The teardown
|
|
86
|
+
* is best-effort + idempotent: a close error is swallowed so shutdown never hangs
|
|
87
|
+
* or throws, and both connections are always attempted.
|
|
88
|
+
*/
|
|
89
|
+
let ConnectionLifecycle = class ConnectionLifecycle {
|
|
90
|
+
pool;
|
|
91
|
+
redis;
|
|
92
|
+
constructor(pool, redis) {
|
|
93
|
+
this.pool = pool;
|
|
94
|
+
this.redis = redis;
|
|
95
|
+
}
|
|
96
|
+
async onModuleDestroy() {
|
|
97
|
+
// Close both; never let one failure prevent the other, never throw.
|
|
98
|
+
try {
|
|
99
|
+
await this.pool.end();
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
// best-effort — a pool already ending / a mid-query close is non-fatal here.
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
await this.redis.quit();
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// best-effort — quit on an already-closed client is non-fatal.
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
exports.ConnectionLifecycle = ConnectionLifecycle;
|
|
113
|
+
exports.ConnectionLifecycle = ConnectionLifecycle = __decorate([
|
|
114
|
+
(0, common_1.Injectable)(),
|
|
115
|
+
__metadata("design:paramtypes", [Object, ioredis_1.default])
|
|
116
|
+
], ConnectionLifecycle);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connections.js","sourceRoot":"","sources":["../../src/auth/connections.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;;;;;;;;;;;;;;;AAkBH,oCAcC;AASD,8CAIC;AA3CD,2CAAkE;AAClE,sDAA6C;AAC7C,sDAA4B;AAG5B;;;;;;;;;;GAUG;AACH,SAAgB,YAAY,CAAC,MAAiB;IAC5C,MAAM,EAAE,EAAE,EAAE,IAAI,EAAE,GAAG,MAAM,CAAC;IAC5B,OAAO,iBAAO,CAAC,UAAU,CAAC;QACxB,IAAI,EAAE,EAAE,CAAC,IAAI;QACb,IAAI,EAAE,EAAE,CAAC,IAAI;QACb,IAAI,EAAE,EAAE,CAAC,IAAI;QACb,QAAQ,EAAE,EAAE,CAAC,QAAQ;QACrB,QAAQ,EAAE,EAAE,CAAC,QAAQ;QACrB,eAAe,EAAE,IAAI,CAAC,aAAa,CAAC,WAAW;QAC/C,4EAA4E;QAC5E,cAAc,EAAE,MAAM;QACtB,sEAAsE;QACtE,QAAQ,EAAE,GAAG;KACd,CAAC,CAAC;AACL,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,iBAAiB,CAAC,MAAiB;IACjD,iFAAiF;IACjF,2EAA2E;IAC3E,OAAO,IAAI,iBAAK,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,EAAE,oBAAoB,EAAE,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,CAAC,CAAC;AACzF,CAAC;AAED;;;;;;GAMG;AAEI,IAAM,mBAAmB,GAAzB,MAAM,mBAAmB;IAEX;IACA;IAFnB,YACmB,IAAU,EACV,KAAY;QADZ,SAAI,GAAJ,IAAI,CAAM;QACV,UAAK,GAAL,KAAK,CAAO;IAC5B,CAAC;IAEJ,KAAK,CAAC,eAAe;QACnB,oEAAoE;QACpE,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC;QACxB,CAAC;QAAC,MAAM,CAAC;YACP,6EAA6E;QAC/E,CAAC;QACD,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;QAC1B,CAAC;QAAC,MAAM,CAAC;YACP,+DAA+D;QACjE,CAAC;IACH,CAAC;CACF,CAAA;AAnBY,kDAAmB;8BAAnB,mBAAmB;IAD/B,IAAA,mBAAU,GAAE;6CAIe,iBAAK;GAHpB,mBAAmB,CAmB/B"}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 0b Auth-surface **endpoint DTOs / view types** — the request/response
|
|
3
|
+
* SHAPES the four `@Public()` `/auth/*` endpoints speak (spec §2). ⛔ FROZEN by
|
|
4
|
+
* this prelude; **Team C builds controllers to these**. Types only — no NestJS /
|
|
5
|
+
* class-validator decorators here (the prelude froze the wire shape; Team C adds
|
|
6
|
+
* the validation decorators on its own DTO classes if it wants runtime
|
|
7
|
+
* validation, keeping this file framework-agnostic and importable anywhere).
|
|
8
|
+
*
|
|
9
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
10
|
+
* SECURITY RULES these shapes ENCODE (spec §2, ACs 2/3/6; enforce in controllers):
|
|
11
|
+
*
|
|
12
|
+
* • ANTI-ENUMERATION — `/auth/login` and `/auth/mfa` failure responses are a
|
|
13
|
+
* SINGLE generic 401 for {unknown user, wrong password, wrong/expired factor}.
|
|
14
|
+
* The response body MUST NOT distinguish which failed (the engine's decoy-verify
|
|
15
|
+
* already equalizes timing). NEVER add a `reason`/`field`/`code` that leaks
|
|
16
|
+
* "no such user" vs "bad password".
|
|
17
|
+
* • NO SECRET LEAK — NO response/log ever carries a password, a password hash, a
|
|
18
|
+
* TOTP secret, or a RAW refresh/access token OTHER than the two token fields a
|
|
19
|
+
* successful lifecycle event legitimately returns ({@link SessionTokensView}).
|
|
20
|
+
* The {@link SessionView} below deliberately OMITS every secret — it is the
|
|
21
|
+
* ONLY session shape a response body may embed.
|
|
22
|
+
* • FAIL-CLOSED — a store/transient failure on refresh is the ADR-0016 retryable
|
|
23
|
+
* signal (503-style), NOT a terminal deny/logout (see {@link RefreshResponse}).
|
|
24
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
25
|
+
*
|
|
26
|
+
* ADR-0003: framework-agnostic — no framework / HTTP / DB imports.
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* The client-safe view of an established session. Carries ONLY non-secret
|
|
30
|
+
* lifecycle facts a caller may see. ⛔ It MUST NOT contain any token, hash, or
|
|
31
|
+
* secret — tokens travel ONLY in {@link SessionTokensView}. `principalId` /
|
|
32
|
+
* `tenantId` are opaque internal ids (not rendered to end users as UUIDs — the UI
|
|
33
|
+
* resolves display labels; these are for the client's own session bookkeeping).
|
|
34
|
+
*/
|
|
35
|
+
export interface SessionView {
|
|
36
|
+
readonly sessionId: string;
|
|
37
|
+
readonly principalId: string;
|
|
38
|
+
readonly tenantId: string;
|
|
39
|
+
readonly status: 'active' | 'rotated' | 'revoked' | 'expired';
|
|
40
|
+
/** UTC epoch ms. */
|
|
41
|
+
readonly issuedAtMs: number;
|
|
42
|
+
/** UTC epoch ms. */
|
|
43
|
+
readonly expiresAtMs: number;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The token pair minted at a session lifecycle event. ⛔ These are SECRETS in
|
|
47
|
+
* transit — returned to the caller EXACTLY at login / mfa-success / refresh, never
|
|
48
|
+
* persisted raw, never logged. The refresh token is stored server-side only as
|
|
49
|
+
* its keyed hash.
|
|
50
|
+
*/
|
|
51
|
+
export interface SessionTokensView {
|
|
52
|
+
readonly accessToken: string;
|
|
53
|
+
readonly refreshToken: string;
|
|
54
|
+
}
|
|
55
|
+
/** A successful session establishment payload (login / mfa / refresh success). */
|
|
56
|
+
export interface SessionSuccess {
|
|
57
|
+
readonly session: SessionView;
|
|
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;
|
|
67
|
+
}
|
|
68
|
+
/** `POST /auth/login` request body. */
|
|
69
|
+
export interface LoginRequest {
|
|
70
|
+
/** Username or email — resolved to a principal by the credential resolver. */
|
|
71
|
+
readonly identityHint: string;
|
|
72
|
+
/** ⛔ The plaintext password — verified then discarded; NEVER logged/echoed. */
|
|
73
|
+
readonly password: string;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The MFA-challenge branch of a login/step-up response: the password was correct
|
|
77
|
+
* but a second factor is required, so NO session is established yet. The caller
|
|
78
|
+
* re-submits to `/auth/mfa` with the `challengeToken`. ⛔ `challengeToken` is a
|
|
79
|
+
* short-TTL opaque handle to the pending authentication — it is NOT a session
|
|
80
|
+
* token and grants no access on its own; it carries no secret about the user.
|
|
81
|
+
*/
|
|
82
|
+
export interface MfaChallengeResponse {
|
|
83
|
+
readonly mfaRequired: true;
|
|
84
|
+
readonly challengeToken: string;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* `POST /auth/login` response — a discriminated union:
|
|
88
|
+
* • {@link SessionSuccess} — password-only user, authenticated + session;
|
|
89
|
+
* • {@link MfaChallengeResponse} — MFA-enabled user, challenge issued (no session).
|
|
90
|
+
* A FAILURE (unknown user / wrong password) is NOT modeled here — it is a generic
|
|
91
|
+
* HTTP 401 with a non-distinguishing body (anti-enumeration; see file header).
|
|
92
|
+
*/
|
|
93
|
+
export type LoginResponse = SessionSuccess | MfaChallengeResponse;
|
|
94
|
+
/**
|
|
95
|
+
* `POST /auth/mfa` request body — completes a pending MFA challenge. `factor` is
|
|
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.
|
|
101
|
+
*/
|
|
102
|
+
export interface MfaRequest {
|
|
103
|
+
/** The opaque handle from {@link MfaChallengeResponse}. Short-TTL, single-use. */
|
|
104
|
+
readonly challengeToken: string;
|
|
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. */
|
|
108
|
+
readonly code: string;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* `POST /auth/mfa` response — on success, the session + tokens
|
|
112
|
+
* ({@link SessionSuccess}). A wrong/expired code is a generic HTTP 401 (no session,
|
|
113
|
+
* no leak of why), NOT modeled as a body variant.
|
|
114
|
+
*/
|
|
115
|
+
export type MfaResponse = SessionSuccess;
|
|
116
|
+
/**
|
|
117
|
+
* `POST /auth/refresh` request body. The refresh token may alternatively arrive
|
|
118
|
+
* via an HttpOnly cookie (Team C's transport choice) — the shape is the same.
|
|
119
|
+
* ⛔ Secret — never logged; server matches it by keyed hash only.
|
|
120
|
+
*/
|
|
121
|
+
export interface RefreshRequest {
|
|
122
|
+
readonly refreshToken: string;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* `POST /auth/refresh` response — new tokens + the (rotated) session on success
|
|
126
|
+
* ({@link SessionSuccess}).
|
|
127
|
+
*
|
|
128
|
+
* ⛔ TWO distinct failure modes the controller MUST keep separate (spec §2, AC 7):
|
|
129
|
+
* • reuse/invalid/expired token → generic `session_rejected` (HTTP 401), lineage
|
|
130
|
+
* killed (ADR-0015) — a TERMINAL deny.
|
|
131
|
+
* • transient store outage (hot store unavailable) → the ADR-0016 RETRYABLE
|
|
132
|
+
* signal (HTTP 503-style), NOT a deny/logout. Conflating the two either logs
|
|
133
|
+
* everyone out on a blip or fails open — both are bugs.
|
|
134
|
+
* Neither failure is a body variant here; both are HTTP-status outcomes with
|
|
135
|
+
* non-secret bodies.
|
|
136
|
+
*/
|
|
137
|
+
export type RefreshResponse = SessionSuccess;
|
|
138
|
+
/**
|
|
139
|
+
* `POST /auth/logout` request body. `sessionId` may alternatively be derived from
|
|
140
|
+
* the access token (Team C's choice). Idempotent — revoking an already-revoked /
|
|
141
|
+
* unknown session still succeeds (no existence leak).
|
|
142
|
+
*/
|
|
143
|
+
export interface LogoutRequest {
|
|
144
|
+
readonly sessionId: string;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* `POST /auth/logout` acknowledgement. The endpoint returns HTTP 204 (no body) on
|
|
148
|
+
* success; this type documents an OPTIONAL ack body if Team C prefers 200 — it
|
|
149
|
+
* carries NO secret and NO session detail (just a flag). Prefer 204 per spec §2.
|
|
150
|
+
*/
|
|
151
|
+
export interface LogoutAck {
|
|
152
|
+
readonly revoked: true;
|
|
153
|
+
}
|
|
154
|
+
//# sourceMappingURL=dto.d.ts.map
|
|
@@ -0,0 +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;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"}
|
package/dist/auth/dto.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Phase 0b Auth-surface **endpoint DTOs / view types** — the request/response
|
|
4
|
+
* SHAPES the four `@Public()` `/auth/*` endpoints speak (spec §2). ⛔ FROZEN by
|
|
5
|
+
* this prelude; **Team C builds controllers to these**. Types only — no NestJS /
|
|
6
|
+
* class-validator decorators here (the prelude froze the wire shape; Team C adds
|
|
7
|
+
* the validation decorators on its own DTO classes if it wants runtime
|
|
8
|
+
* validation, keeping this file framework-agnostic and importable anywhere).
|
|
9
|
+
*
|
|
10
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
11
|
+
* SECURITY RULES these shapes ENCODE (spec §2, ACs 2/3/6; enforce in controllers):
|
|
12
|
+
*
|
|
13
|
+
* • ANTI-ENUMERATION — `/auth/login` and `/auth/mfa` failure responses are a
|
|
14
|
+
* SINGLE generic 401 for {unknown user, wrong password, wrong/expired factor}.
|
|
15
|
+
* The response body MUST NOT distinguish which failed (the engine's decoy-verify
|
|
16
|
+
* already equalizes timing). NEVER add a `reason`/`field`/`code` that leaks
|
|
17
|
+
* "no such user" vs "bad password".
|
|
18
|
+
* • NO SECRET LEAK — NO response/log ever carries a password, a password hash, a
|
|
19
|
+
* TOTP secret, or a RAW refresh/access token OTHER than the two token fields a
|
|
20
|
+
* successful lifecycle event legitimately returns ({@link SessionTokensView}).
|
|
21
|
+
* The {@link SessionView} below deliberately OMITS every secret — it is the
|
|
22
|
+
* ONLY session shape a response body may embed.
|
|
23
|
+
* • FAIL-CLOSED — a store/transient failure on refresh is the ADR-0016 retryable
|
|
24
|
+
* signal (503-style), NOT a terminal deny/logout (see {@link RefreshResponse}).
|
|
25
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
26
|
+
*
|
|
27
|
+
* ADR-0003: framework-agnostic — no framework / HTTP / DB imports.
|
|
28
|
+
*/
|
|
29
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dto.js","sourceRoot":"","sources":["../../src/auth/dto.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 0b Auth-surface — the **pending-MFA challenge store** backing
|
|
3
|
+
* `/auth/login` (mfa branch) → `/auth/mfa`.
|
|
4
|
+
*
|
|
5
|
+
* ── WHY THIS EXISTS ──────────────────────────────────────────────────────────
|
|
6
|
+
* The core `Authenticator` has NO separate "verify one more factor" entry point:
|
|
7
|
+
* `authenticate(hint, credentials[])` is the ONLY seam (see
|
|
8
|
+
* `../../core/authentication/authenticator.ts`). To COMPLETE an MFA login the
|
|
9
|
+
* controller must RE-CALL `authenticate(hint, [password, totp])` with BOTH the
|
|
10
|
+
* original password AND the freshly-presented TOTP code. So between the login that
|
|
11
|
+
* returned `mfa_required` and the `/auth/mfa` that finishes it, the server must
|
|
12
|
+
* hold the ORIGINAL `identityHint` + `password` (the "pending authentication").
|
|
13
|
+
*
|
|
14
|
+
* ── THE NON-FORGEABILITY DESIGN (the security decision) ──────────────────────
|
|
15
|
+
* The `challengeToken` handed to the client is a **cryptographically-random opaque
|
|
16
|
+
* handle** (`node:crypto.randomBytes`), NOT a signed/encrypted blob carrying the
|
|
17
|
+
* credentials. The pending `{ identityHint, password }` state lives ONLY
|
|
18
|
+
* server-side, keyed by that random token. Consequences:
|
|
19
|
+
* • FORGERY-PROOF BY CONSTRUCTION — a hand-crafted token maps to no entry, so it
|
|
20
|
+
* is rejected. There is no signature to strip, no payload to tamper with, and
|
|
21
|
+
* the client is never given the credentials (even encrypted) to hold.
|
|
22
|
+
* • SHORT-LIVED — each entry has an expiry (default 5 min). An expired handle is
|
|
23
|
+
* treated as unknown (same generic 401 as a forged one — anti-enumeration).
|
|
24
|
+
* • SINGLE-USE — `consume()` DELETES the entry as it reads it, so a
|
|
25
|
+
* challengeToken cannot be replayed to brute-force TOTP codes indefinitely on
|
|
26
|
+
* one handle. (A caller that fails still burns the handle → must re-login. This
|
|
27
|
+
* is the built-in anti-brute-force cooldown for THIS layer; the resolver/engine
|
|
28
|
+
* add no separate one.)
|
|
29
|
+
* • NEVER LOGGED — the stored password + the token are never logged.
|
|
30
|
+
*
|
|
31
|
+
* TRADE-OFF (documented): server-side state is process-local (an in-memory `Map`).
|
|
32
|
+
* For the single-instance framework scope of Phase 0b this is correct and the
|
|
33
|
+
* safest option (the password never reaches the client). A multi-instance
|
|
34
|
+
* deployment would swap this `Map` for a shared TTL store (Redis) behind the SAME
|
|
35
|
+
* interface — the controller depends only on `store/consume`, not on the backing.
|
|
36
|
+
*
|
|
37
|
+
* ADR-0003 note: this file DOES use `node:crypto` (guard owns node builtins; core
|
|
38
|
+
* does not). It holds NO framework/HTTP coupling — a plain injectable class.
|
|
39
|
+
*/
|
|
40
|
+
/**
|
|
41
|
+
* A minimal server-side, single-use, TTL'd store of pending MFA challenges.
|
|
42
|
+
* Injected into the auth controller. Deliberately depends on nothing framework-y.
|
|
43
|
+
*/
|
|
44
|
+
export declare class PendingMfaStore {
|
|
45
|
+
private readonly ttlMs;
|
|
46
|
+
/** Clock (UTC epoch ms) — injectable for deterministic tests. */
|
|
47
|
+
private readonly nowMs;
|
|
48
|
+
private readonly pending;
|
|
49
|
+
constructor(ttlMs?: number,
|
|
50
|
+
/** Clock (UTC epoch ms) — injectable for deterministic tests. */
|
|
51
|
+
nowMs?: () => number);
|
|
52
|
+
/**
|
|
53
|
+
* Store a pending authentication and return the opaque, unguessable
|
|
54
|
+
* `challengeToken` the client re-presents to `/auth/mfa`. The token carries NO
|
|
55
|
+
* information about the user — it is a random handle only.
|
|
56
|
+
*/
|
|
57
|
+
store(identityHint: string, password: string): string;
|
|
58
|
+
/**
|
|
59
|
+
* Look up + SINGLE-USE consume a pending challenge. Returns the held credentials
|
|
60
|
+
* for a live, known token; `null` for an unknown, forged, or expired token (the
|
|
61
|
+
* controller maps `null` to the SAME generic 401 as a wrong code — no leak of
|
|
62
|
+
* WHY it failed). The entry is DELETED whether or not it was live, so a handle
|
|
63
|
+
* is never replayable.
|
|
64
|
+
*/
|
|
65
|
+
consume(token: string): {
|
|
66
|
+
identityHint: string;
|
|
67
|
+
password: string;
|
|
68
|
+
} | null;
|
|
69
|
+
/** Drop expired entries so the map cannot grow unbounded from abandoned logins. */
|
|
70
|
+
private sweep;
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=pending-mfa-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pending-mfa-store.d.ts","sourceRoot":"","sources":["../../src/auth/pending-mfa-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAmBH;;;GAGG;AACH,qBAAa,eAAe;IAIxB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,iEAAiE;IACjE,OAAO,CAAC,QAAQ,CAAC,KAAK;IALxB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAmC;gBAGxC,KAAK,GAAE,MAAuB;IAC/C,iEAAiE;IAChD,KAAK,GAAE,MAAM,MAAiB;IAGjD;;;;OAIG;IACH,KAAK,CAAC,YAAY,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM;IAWrD;;;;;;OAMG;IACH,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG;QAAE,YAAY,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI;IAWzE,mFAAmF;IACnF,OAAO,CAAC,KAAK;CAMd"}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Phase 0b Auth-surface — the **pending-MFA challenge store** backing
|
|
4
|
+
* `/auth/login` (mfa branch) → `/auth/mfa`.
|
|
5
|
+
*
|
|
6
|
+
* ── WHY THIS EXISTS ──────────────────────────────────────────────────────────
|
|
7
|
+
* The core `Authenticator` has NO separate "verify one more factor" entry point:
|
|
8
|
+
* `authenticate(hint, credentials[])` is the ONLY seam (see
|
|
9
|
+
* `../../core/authentication/authenticator.ts`). To COMPLETE an MFA login the
|
|
10
|
+
* controller must RE-CALL `authenticate(hint, [password, totp])` with BOTH the
|
|
11
|
+
* original password AND the freshly-presented TOTP code. So between the login that
|
|
12
|
+
* returned `mfa_required` and the `/auth/mfa` that finishes it, the server must
|
|
13
|
+
* hold the ORIGINAL `identityHint` + `password` (the "pending authentication").
|
|
14
|
+
*
|
|
15
|
+
* ── THE NON-FORGEABILITY DESIGN (the security decision) ──────────────────────
|
|
16
|
+
* The `challengeToken` handed to the client is a **cryptographically-random opaque
|
|
17
|
+
* handle** (`node:crypto.randomBytes`), NOT a signed/encrypted blob carrying the
|
|
18
|
+
* credentials. The pending `{ identityHint, password }` state lives ONLY
|
|
19
|
+
* server-side, keyed by that random token. Consequences:
|
|
20
|
+
* • FORGERY-PROOF BY CONSTRUCTION — a hand-crafted token maps to no entry, so it
|
|
21
|
+
* is rejected. There is no signature to strip, no payload to tamper with, and
|
|
22
|
+
* the client is never given the credentials (even encrypted) to hold.
|
|
23
|
+
* • SHORT-LIVED — each entry has an expiry (default 5 min). An expired handle is
|
|
24
|
+
* treated as unknown (same generic 401 as a forged one — anti-enumeration).
|
|
25
|
+
* • SINGLE-USE — `consume()` DELETES the entry as it reads it, so a
|
|
26
|
+
* challengeToken cannot be replayed to brute-force TOTP codes indefinitely on
|
|
27
|
+
* one handle. (A caller that fails still burns the handle → must re-login. This
|
|
28
|
+
* is the built-in anti-brute-force cooldown for THIS layer; the resolver/engine
|
|
29
|
+
* add no separate one.)
|
|
30
|
+
* • NEVER LOGGED — the stored password + the token are never logged.
|
|
31
|
+
*
|
|
32
|
+
* TRADE-OFF (documented): server-side state is process-local (an in-memory `Map`).
|
|
33
|
+
* For the single-instance framework scope of Phase 0b this is correct and the
|
|
34
|
+
* safest option (the password never reaches the client). A multi-instance
|
|
35
|
+
* deployment would swap this `Map` for a shared TTL store (Redis) behind the SAME
|
|
36
|
+
* interface — the controller depends only on `store/consume`, not on the backing.
|
|
37
|
+
*
|
|
38
|
+
* ADR-0003 note: this file DOES use `node:crypto` (guard owns node builtins; core
|
|
39
|
+
* does not). It holds NO framework/HTTP coupling — a plain injectable class.
|
|
40
|
+
*/
|
|
41
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
|
+
exports.PendingMfaStore = void 0;
|
|
43
|
+
const node_crypto_1 = require("node:crypto");
|
|
44
|
+
/** Default challenge lifetime — short, per spec §2 ("a short TTL"). */
|
|
45
|
+
const DEFAULT_TTL_MS = 5 * 60 * 1000;
|
|
46
|
+
/** How many random bytes back a challenge handle (256-bit, url-safe hex → 64 chars). */
|
|
47
|
+
const TOKEN_BYTES = 32;
|
|
48
|
+
/**
|
|
49
|
+
* A minimal server-side, single-use, TTL'd store of pending MFA challenges.
|
|
50
|
+
* Injected into the auth controller. Deliberately depends on nothing framework-y.
|
|
51
|
+
*/
|
|
52
|
+
class PendingMfaStore {
|
|
53
|
+
ttlMs;
|
|
54
|
+
nowMs;
|
|
55
|
+
pending = new Map();
|
|
56
|
+
constructor(ttlMs = DEFAULT_TTL_MS,
|
|
57
|
+
/** Clock (UTC epoch ms) — injectable for deterministic tests. */
|
|
58
|
+
nowMs = Date.now) {
|
|
59
|
+
this.ttlMs = ttlMs;
|
|
60
|
+
this.nowMs = nowMs;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Store a pending authentication and return the opaque, unguessable
|
|
64
|
+
* `challengeToken` the client re-presents to `/auth/mfa`. The token carries NO
|
|
65
|
+
* information about the user — it is a random handle only.
|
|
66
|
+
*/
|
|
67
|
+
store(identityHint, password) {
|
|
68
|
+
this.sweep();
|
|
69
|
+
const token = (0, node_crypto_1.randomBytes)(TOKEN_BYTES).toString('hex');
|
|
70
|
+
this.pending.set(token, {
|
|
71
|
+
identityHint,
|
|
72
|
+
password,
|
|
73
|
+
expiresAtMs: this.nowMs() + this.ttlMs,
|
|
74
|
+
});
|
|
75
|
+
return token;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Look up + SINGLE-USE consume a pending challenge. Returns the held credentials
|
|
79
|
+
* for a live, known token; `null` for an unknown, forged, or expired token (the
|
|
80
|
+
* controller maps `null` to the SAME generic 401 as a wrong code — no leak of
|
|
81
|
+
* WHY it failed). The entry is DELETED whether or not it was live, so a handle
|
|
82
|
+
* is never replayable.
|
|
83
|
+
*/
|
|
84
|
+
consume(token) {
|
|
85
|
+
if (typeof token !== 'string' || token.length === 0)
|
|
86
|
+
return null;
|
|
87
|
+
const entry = this.pending.get(token);
|
|
88
|
+
// Single-use: remove on read regardless of validity (a spent/expired handle
|
|
89
|
+
// can never be retried → forces a fresh login, the anti-brute-force cooldown).
|
|
90
|
+
this.pending.delete(token);
|
|
91
|
+
if (entry === undefined)
|
|
92
|
+
return null;
|
|
93
|
+
if (this.nowMs() >= entry.expiresAtMs)
|
|
94
|
+
return null;
|
|
95
|
+
return { identityHint: entry.identityHint, password: entry.password };
|
|
96
|
+
}
|
|
97
|
+
/** Drop expired entries so the map cannot grow unbounded from abandoned logins. */
|
|
98
|
+
sweep() {
|
|
99
|
+
const now = this.nowMs();
|
|
100
|
+
for (const [token, entry] of this.pending) {
|
|
101
|
+
if (now >= entry.expiresAtMs)
|
|
102
|
+
this.pending.delete(token);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
exports.PendingMfaStore = PendingMfaStore;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pending-mfa-store.js","sourceRoot":"","sources":["../../src/auth/pending-mfa-store.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;;;AAEH,6CAA0C;AAW1C,uEAAuE;AACvE,MAAM,cAAc,GAAG,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC;AAErC,wFAAwF;AACxF,MAAM,WAAW,GAAG,EAAE,CAAC;AAEvB;;;GAGG;AACH,MAAa,eAAe;IAIP;IAEA;IALF,OAAO,GAAG,IAAI,GAAG,EAAwB,CAAC;IAE3D,YACmB,QAAgB,cAAc;IAC/C,iEAAiE;IAChD,QAAsB,IAAI,CAAC,GAAG;QAF9B,UAAK,GAAL,KAAK,CAAyB;QAE9B,UAAK,GAAL,KAAK,CAAyB;IAC9C,CAAC;IAEJ;;;;OAIG;IACH,KAAK,CAAC,YAAoB,EAAE,QAAgB;QAC1C,IAAI,CAAC,KAAK,EAAE,CAAC;QACb,MAAM,KAAK,GAAG,IAAA,yBAAW,EAAC,WAAW,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;QACvD,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE;YACtB,YAAY;YACZ,QAAQ;YACR,WAAW,EAAE,IAAI,CAAC,KAAK,EAAE,GAAG,IAAI,CAAC,KAAK;SACvC,CAAC,CAAC;QACH,OAAO,KAAK,CAAC;IACf,CAAC;IAED;;;;;;OAMG;IACH,OAAO,CAAC,KAAa;QACnB,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;QACjE,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QACtC,4EAA4E;QAC5E,+EAA+E;QAC/E,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAC3B,IAAI,KAAK,KAAK,SAAS;YAAE,OAAO,IAAI,CAAC;QACrC,IAAI,IAAI,CAAC,KAAK,EAAE,IAAI,KAAK,CAAC,WAAW;YAAE,OAAO,IAAI,CAAC;QACnD,OAAO,EAAE,YAAY,EAAE,KAAK,CAAC,YAAY,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE,CAAC;IACxE,CAAC;IAED,mFAAmF;IAC3E,KAAK;QACX,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC;QACzB,KAAK,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YAC1C,IAAI,GAAG,IAAI,KAAK,CAAC,WAAW;gBAAE,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAC3D,CAAC;IACH,CAAC;CACF;AAlDD,0CAkDC"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `PersistenceModule` — the ONE place live infra connections are created (spec
|
|
3
|
+
* §4). Provides the live `mariadb` {@link Pool} ({@link SHIELD_DB_POOL}) and the
|
|
4
|
+
* live `ioredis` client ({@link SHIELD_REDIS}) from {@link AppConfig}, with
|
|
5
|
+
* graceful shutdown. Everything auth persists (sessions, credentials, hot state)
|
|
6
|
+
* flows through these two connections.
|
|
7
|
+
*
|
|
8
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
9
|
+
* Team B (connection + composition) OWNS this file. The two factories create the
|
|
10
|
+
* real driver objects; the {@link ConnectionLifecycle} provider (a class Nest
|
|
11
|
+
* invokes `onModuleDestroy` on) closes them on shutdown — a value provider is not
|
|
12
|
+
* itself lifecycle-aware, so the teardown lives in that holder (see
|
|
13
|
+
* `connections.ts`). Both connections are exported so AuthModule can inject the
|
|
14
|
+
* pool (credential resolver + session record store) and the redis (hot store).
|
|
15
|
+
*
|
|
16
|
+
* SHIELD_DB_POOL — {@link createDbPool}: `mariadb.createPool` for the
|
|
17
|
+
* `shield_core` account from `config.db`, `connectionLimit` from the ADR-0018
|
|
18
|
+
* budget (`config.pool.perAccountMax.shield_core`). NOT a duplicate pool knob.
|
|
19
|
+
* SHIELD_REDIS — {@link createRedisClient}: `new Redis(config.redis.url)`
|
|
20
|
+
* (ioredis). The hot tier (ADR-0013).
|
|
21
|
+
*
|
|
22
|
+
* Neither factory reads the HMAC key (that is the session/hasher composition in
|
|
23
|
+
* AuthModule). A blank `db.password` is allowed — it fails LOUD at connect time,
|
|
24
|
+
* which is correct for a runtime credential. Nothing here logs a credential.
|
|
25
|
+
*
|
|
26
|
+
* ⛔ OPT-IN: like AuthModule, this module opens LIVE connections, so it must only
|
|
27
|
+
* be imported when a deployment actually wants auth (AuthModule imports it). An
|
|
28
|
+
* enforcement-only app that never imports AuthModule never opens a pool/redis and
|
|
29
|
+
* keeps the no-`.env` boot. See auth.module.ts for the opt-in mechanism.
|
|
30
|
+
*
|
|
31
|
+
* ADR-0003: the real `mariadb`/`ioredis` live in guard (Core stays zero-dep).
|
|
32
|
+
*/
|
|
33
|
+
import { type DynamicModule } from '@nestjs/common';
|
|
34
|
+
/**
|
|
35
|
+
* The persistence module. `forRoot()` returns the two connection providers (both
|
|
36
|
+
* exported so AuthModule can inject them) plus the shutdown holder. Opt-in: only
|
|
37
|
+
* imported when auth is wired.
|
|
38
|
+
*/
|
|
39
|
+
export declare class PersistenceModule {
|
|
40
|
+
static forRoot(): DynamicModule;
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=persistence.module.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"persistence.module.d.ts","sourceRoot":"","sources":["../../src/auth/persistence.module.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,OAAO,EAAU,KAAK,aAAa,EAAiB,MAAM,gBAAgB,CAAC;AA0C3E;;;;GAIG;AACH,qBACa,iBAAiB;IAC5B,MAAM,CAAC,OAAO,IAAI,aAAa;CAOhC"}
|