@astram/guard 0.2.0 → 0.4.0

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