@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.
Files changed (183) hide show
  1. package/dist/auth/account.controller.d.ts +245 -0
  2. package/dist/auth/account.controller.d.ts.map +1 -0
  3. package/dist/auth/account.controller.js +673 -0
  4. package/dist/auth/account.controller.js.map +1 -0
  5. package/dist/auth/auth.controller.d.ts +316 -0
  6. package/dist/auth/auth.controller.d.ts.map +1 -0
  7. package/dist/auth/auth.controller.js +1091 -0
  8. package/dist/auth/auth.controller.js.map +1 -0
  9. package/dist/auth/auth.module.d.ts +202 -0
  10. package/dist/auth/auth.module.d.ts.map +1 -0
  11. package/dist/auth/auth.module.js +610 -0
  12. package/dist/auth/auth.module.js.map +1 -0
  13. package/dist/auth/connections.d.ts +61 -0
  14. package/dist/auth/connections.d.ts.map +1 -0
  15. package/dist/auth/connections.js +116 -0
  16. package/dist/auth/connections.js.map +1 -0
  17. package/dist/auth/dto.d.ts +154 -0
  18. package/dist/auth/dto.d.ts.map +1 -0
  19. package/dist/auth/dto.js +29 -0
  20. package/dist/auth/dto.js.map +1 -0
  21. package/dist/auth/pending-mfa-store.d.ts +72 -0
  22. package/dist/auth/pending-mfa-store.d.ts.map +1 -0
  23. package/dist/auth/pending-mfa-store.js +106 -0
  24. package/dist/auth/pending-mfa-store.js.map +1 -0
  25. package/dist/auth/persistence.module.d.ts +42 -0
  26. package/dist/auth/persistence.module.d.ts.map +1 -0
  27. package/dist/auth/persistence.module.js +94 -0
  28. package/dist/auth/persistence.module.js.map +1 -0
  29. package/dist/auth/ports/audit-sink.d.ts +47 -0
  30. package/dist/auth/ports/audit-sink.d.ts.map +1 -0
  31. package/dist/auth/ports/audit-sink.js +20 -0
  32. package/dist/auth/ports/audit-sink.js.map +1 -0
  33. package/dist/auth/ports/credential-writer.d.ts +23 -0
  34. package/dist/auth/ports/credential-writer.d.ts.map +1 -0
  35. package/dist/auth/ports/credential-writer.js +2 -0
  36. package/dist/auth/ports/credential-writer.js.map +1 -0
  37. package/dist/auth/ports/email-sender.d.ts +14 -0
  38. package/dist/auth/ports/email-sender.d.ts.map +1 -0
  39. package/dist/auth/ports/email-sender.js +2 -0
  40. package/dist/auth/ports/email-sender.js.map +1 -0
  41. package/dist/auth/ports/mfa-device-store.d.ts +45 -0
  42. package/dist/auth/ports/mfa-device-store.d.ts.map +1 -0
  43. package/dist/auth/ports/mfa-device-store.js +13 -0
  44. package/dist/auth/ports/mfa-device-store.js.map +1 -0
  45. package/dist/auth/ports/profile-writer.d.ts +12 -0
  46. package/dist/auth/ports/profile-writer.d.ts.map +1 -0
  47. package/dist/auth/ports/profile-writer.js +2 -0
  48. package/dist/auth/ports/profile-writer.js.map +1 -0
  49. package/dist/auth/ports/recovery-code-store.d.ts +10 -0
  50. package/dist/auth/ports/recovery-code-store.d.ts.map +1 -0
  51. package/dist/auth/ports/recovery-code-store.js +2 -0
  52. package/dist/auth/ports/recovery-code-store.js.map +1 -0
  53. package/dist/auth/ports/reset-token-store.d.ts +12 -0
  54. package/dist/auth/ports/reset-token-store.d.ts.map +1 -0
  55. package/dist/auth/ports/reset-token-store.js +2 -0
  56. package/dist/auth/ports/reset-token-store.js.map +1 -0
  57. package/dist/auth/ports/session-facts-projector.d.ts +44 -0
  58. package/dist/auth/ports/session-facts-projector.d.ts.map +1 -0
  59. package/dist/auth/ports/session-facts-projector.js +30 -0
  60. package/dist/auth/ports/session-facts-projector.js.map +1 -0
  61. package/dist/auth/ports/session-sweeper.d.ts +12 -0
  62. package/dist/auth/ports/session-sweeper.d.ts.map +1 -0
  63. package/dist/auth/ports/session-sweeper.js +2 -0
  64. package/dist/auth/ports/session-sweeper.js.map +1 -0
  65. package/dist/auth/profile-resolver.d.ts +73 -0
  66. package/dist/auth/profile-resolver.d.ts.map +1 -0
  67. package/dist/auth/profile-resolver.js +28 -0
  68. package/dist/auth/profile-resolver.js.map +1 -0
  69. package/dist/auth/rate-limit/attempt-limiter.d.ts +91 -0
  70. package/dist/auth/rate-limit/attempt-limiter.d.ts.map +1 -0
  71. package/dist/auth/rate-limit/attempt-limiter.js +133 -0
  72. package/dist/auth/rate-limit/attempt-limiter.js.map +1 -0
  73. package/dist/auth/tokens.d.ts +236 -0
  74. package/dist/auth/tokens.d.ts.map +1 -0
  75. package/dist/auth/tokens.js +254 -0
  76. package/dist/auth/tokens.js.map +1 -0
  77. package/dist/auth/totp-enrollment.d.ts +42 -0
  78. package/dist/auth/totp-enrollment.d.ts.map +1 -0
  79. package/dist/auth/totp-enrollment.js +96 -0
  80. package/dist/auth/totp-enrollment.js.map +1 -0
  81. package/dist/authentication/argon2id-password-hasher.js.map +1 -0
  82. package/dist/authentication/mariadb-credential-resolver.d.ts +110 -0
  83. package/dist/authentication/mariadb-credential-resolver.d.ts.map +1 -0
  84. package/dist/authentication/mariadb-credential-resolver.js +170 -0
  85. package/dist/authentication/mariadb-credential-resolver.js.map +1 -0
  86. package/dist/authz/cross-tenant-grant-writer.d.ts +259 -0
  87. package/dist/authz/cross-tenant-grant-writer.d.ts.map +1 -0
  88. package/dist/authz/cross-tenant-grant-writer.js +397 -0
  89. package/dist/authz/cross-tenant-grant-writer.js.map +1 -0
  90. package/dist/authz/manifest-operation-authority-adapter.d.ts +65 -0
  91. package/dist/authz/manifest-operation-authority-adapter.d.ts.map +1 -0
  92. package/dist/authz/manifest-operation-authority-adapter.js +108 -0
  93. package/dist/authz/manifest-operation-authority-adapter.js.map +1 -0
  94. package/dist/authz/mariadb-cross-tenant-grant-adapter.d.ts +90 -0
  95. package/dist/authz/mariadb-cross-tenant-grant-adapter.d.ts.map +1 -0
  96. package/dist/authz/mariadb-cross-tenant-grant-adapter.js +211 -0
  97. package/dist/authz/mariadb-cross-tenant-grant-adapter.js.map +1 -0
  98. package/dist/authz/mariadb-record-rule-adapter.d.ts +60 -0
  99. package/dist/authz/mariadb-record-rule-adapter.d.ts.map +1 -0
  100. package/dist/authz/mariadb-record-rule-adapter.js +154 -0
  101. package/dist/authz/mariadb-record-rule-adapter.js.map +1 -0
  102. package/dist/authz/mariadb-record-share-adapter.d.ts +75 -0
  103. package/dist/authz/mariadb-record-share-adapter.d.ts.map +1 -0
  104. package/dist/authz/mariadb-record-share-adapter.js +106 -0
  105. package/dist/authz/mariadb-record-share-adapter.js.map +1 -0
  106. package/dist/authz/record-share-cascade.d.ts +52 -0
  107. package/dist/authz/record-share-cascade.d.ts.map +1 -0
  108. package/dist/authz/record-share-cascade.js +49 -0
  109. package/dist/authz/record-share-cascade.js.map +1 -0
  110. package/dist/authz/record-share-writer.d.ts +175 -0
  111. package/dist/authz/record-share-writer.d.ts.map +1 -0
  112. package/dist/authz/record-share-writer.js +225 -0
  113. package/dist/authz/record-share-writer.js.map +1 -0
  114. package/dist/authz/wire-record-authorization.d.ts +66 -0
  115. package/dist/authz/wire-record-authorization.d.ts.map +1 -0
  116. package/dist/authz/wire-record-authorization.js +34 -0
  117. package/dist/authz/wire-record-authorization.js.map +1 -0
  118. package/dist/boot/boot-sequence.js.map +1 -0
  119. package/dist/boot/boot.module.js.map +1 -0
  120. package/dist/completeness/catalog-reconciler.d.ts +21 -0
  121. package/dist/completeness/catalog-reconciler.d.ts.map +1 -0
  122. package/dist/completeness/catalog-reconciler.js +74 -0
  123. package/dist/completeness/catalog-reconciler.js.map +1 -0
  124. package/dist/completeness/operation-inventory-scanner.d.ts +17 -0
  125. package/dist/completeness/operation-inventory-scanner.d.ts.map +1 -0
  126. package/dist/completeness/operation-inventory-scanner.js +79 -0
  127. package/dist/completeness/operation-inventory-scanner.js.map +1 -0
  128. package/dist/completeness/ports/catalog-store.d.ts +33 -0
  129. package/dist/completeness/ports/catalog-store.d.ts.map +1 -0
  130. package/dist/completeness/ports/catalog-store.js +2 -0
  131. package/dist/completeness/ports/catalog-store.js.map +1 -0
  132. package/dist/config/app-config.d.ts +134 -1
  133. package/dist/config/app-config.d.ts.map +1 -1
  134. package/dist/config/app-config.js +144 -2
  135. package/dist/config/app-config.js.map +1 -0
  136. package/dist/config/config.module.js.map +1 -0
  137. package/dist/enforcement/classification.decorator.d.ts +33 -14
  138. package/dist/enforcement/classification.decorator.d.ts.map +1 -1
  139. package/dist/enforcement/classification.decorator.js +34 -14
  140. package/dist/enforcement/classification.decorator.js.map +1 -0
  141. package/dist/enforcement/core-seam.js.map +1 -0
  142. package/dist/enforcement/enforcement.module.js.map +1 -0
  143. package/dist/enforcement/global-enforcement.guard.d.ts +29 -19
  144. package/dist/enforcement/global-enforcement.guard.d.ts.map +1 -1
  145. package/dist/enforcement/global-enforcement.guard.js +108 -25
  146. package/dist/enforcement/global-enforcement.guard.js.map +1 -0
  147. package/dist/enforcement/reenroll-allowlist.d.ts +49 -0
  148. package/dist/enforcement/reenroll-allowlist.d.ts.map +1 -0
  149. package/dist/enforcement/reenroll-allowlist.js +77 -0
  150. package/dist/enforcement/reenroll-allowlist.js.map +1 -0
  151. package/dist/index.d.ts +43 -3
  152. package/dist/index.d.ts.map +1 -1
  153. package/dist/index.js +198 -1
  154. package/dist/index.js.map +1 -0
  155. package/dist/module/shield-tokens.d.ts +15 -0
  156. package/dist/module/shield-tokens.d.ts.map +1 -0
  157. package/dist/module/shield-tokens.js +17 -0
  158. package/dist/module/shield-tokens.js.map +1 -0
  159. package/dist/module/shield.module.d.ts +203 -8
  160. package/dist/module/shield.module.d.ts.map +1 -1
  161. package/dist/module/shield.module.js +232 -14
  162. package/dist/module/shield.module.js.map +1 -0
  163. package/dist/request-auth/request-principal.d.ts +42 -0
  164. package/dist/request-auth/request-principal.d.ts.map +1 -0
  165. package/dist/request-auth/request-principal.js +87 -0
  166. package/dist/request-auth/request-principal.js.map +1 -0
  167. package/dist/request-auth/session-facts-als.d.ts +65 -0
  168. package/dist/request-auth/session-facts-als.d.ts.map +1 -0
  169. package/dist/request-auth/session-facts-als.js +57 -0
  170. package/dist/request-auth/session-facts-als.js.map +1 -0
  171. package/dist/request-auth/session-token-binder.d.ts +130 -0
  172. package/dist/request-auth/session-token-binder.d.ts.map +1 -0
  173. package/dist/request-auth/session-token-binder.js +277 -0
  174. package/dist/request-auth/session-token-binder.js.map +1 -0
  175. package/dist/request-auth/tokens.d.ts +14 -0
  176. package/dist/request-auth/tokens.d.ts.map +1 -0
  177. package/dist/request-auth/tokens.js +16 -0
  178. package/dist/request-auth/tokens.js.map +1 -0
  179. package/dist/session/crypto-adapters.js.map +1 -0
  180. package/dist/session/index.js.map +1 -0
  181. package/dist/session/mariadb-session-record-store.js.map +1 -0
  182. package/dist/session/redis-hot-session-store.js.map +1 -0
  183. 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"}
@@ -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"}