@endora-commerce/mod-mfa 0.100.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 (126) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +58 -0
  3. package/dist/admin/index.d.ts +37 -0
  4. package/dist/admin/index.d.ts.map +1 -0
  5. package/dist/admin/index.js +12 -0
  6. package/dist/admin/index.js.map +1 -0
  7. package/dist/admin/pages/AdminSecuritySettings.d.ts +3 -0
  8. package/dist/admin/pages/AdminSecuritySettings.d.ts.map +1 -0
  9. package/dist/admin/pages/AdminSecuritySettings.js +71 -0
  10. package/dist/admin/pages/AdminSecuritySettings.js.map +1 -0
  11. package/dist/backend/entities/mfa-enrolment.entity.d.ts +33 -0
  12. package/dist/backend/entities/mfa-enrolment.entity.d.ts.map +1 -0
  13. package/dist/backend/entities/mfa-enrolment.entity.js +95 -0
  14. package/dist/backend/entities/mfa-enrolment.entity.js.map +1 -0
  15. package/dist/backend/entities/mfa-organization-policy.entity.d.ts +19 -0
  16. package/dist/backend/entities/mfa-organization-policy.entity.d.ts.map +1 -0
  17. package/dist/backend/entities/mfa-organization-policy.entity.js +60 -0
  18. package/dist/backend/entities/mfa-organization-policy.entity.js.map +1 -0
  19. package/dist/backend/entities/mfa-recovery-code.entity.d.ts +17 -0
  20. package/dist/backend/entities/mfa-recovery-code.entity.d.ts.map +1 -0
  21. package/dist/backend/entities/mfa-recovery-code.entity.js +54 -0
  22. package/dist/backend/entities/mfa-recovery-code.entity.js.map +1 -0
  23. package/dist/backend/entities/mfa-social-identity.entity.d.ts +24 -0
  24. package/dist/backend/entities/mfa-social-identity.entity.d.ts.map +1 -0
  25. package/dist/backend/entities/mfa-social-identity.entity.js +73 -0
  26. package/dist/backend/entities/mfa-social-identity.entity.js.map +1 -0
  27. package/dist/backend/index.d.ts +131 -0
  28. package/dist/backend/index.d.ts.map +1 -0
  29. package/dist/backend/index.js +173 -0
  30. package/dist/backend/index.js.map +1 -0
  31. package/dist/backend/plugin.d.ts +99 -0
  32. package/dist/backend/plugin.d.ts.map +1 -0
  33. package/dist/backend/plugin.js +111 -0
  34. package/dist/backend/plugin.js.map +1 -0
  35. package/dist/backend/routes.admin.d.ts +23 -0
  36. package/dist/backend/routes.admin.d.ts.map +1 -0
  37. package/dist/backend/routes.admin.js +93 -0
  38. package/dist/backend/routes.admin.js.map +1 -0
  39. package/dist/backend/routes.oauth.d.ts +26 -0
  40. package/dist/backend/routes.oauth.d.ts.map +1 -0
  41. package/dist/backend/routes.oauth.js +155 -0
  42. package/dist/backend/routes.oauth.js.map +1 -0
  43. package/dist/backend/routes.org.d.ts +21 -0
  44. package/dist/backend/routes.org.d.ts.map +1 -0
  45. package/dist/backend/routes.org.js +17 -0
  46. package/dist/backend/routes.org.js.map +1 -0
  47. package/dist/backend/routes.public.d.ts +21 -0
  48. package/dist/backend/routes.public.d.ts.map +1 -0
  49. package/dist/backend/routes.public.js +118 -0
  50. package/dist/backend/routes.public.js.map +1 -0
  51. package/dist/backend/routes.self-service.d.ts +34 -0
  52. package/dist/backend/routes.self-service.d.ts.map +1 -0
  53. package/dist/backend/routes.self-service.js +113 -0
  54. package/dist/backend/routes.self-service.js.map +1 -0
  55. package/dist/backend/services/account-identity.d.ts +71 -0
  56. package/dist/backend/services/account-identity.d.ts.map +1 -0
  57. package/dist/backend/services/account-identity.js +87 -0
  58. package/dist/backend/services/account-identity.js.map +1 -0
  59. package/dist/backend/services/challenge-store.d.ts +40 -0
  60. package/dist/backend/services/challenge-store.d.ts.map +1 -0
  61. package/dist/backend/services/challenge-store.js +90 -0
  62. package/dist/backend/services/challenge-store.js.map +1 -0
  63. package/dist/backend/services/mfa-enrolment-count.service.d.ts +26 -0
  64. package/dist/backend/services/mfa-enrolment-count.service.d.ts.map +1 -0
  65. package/dist/backend/services/mfa-enrolment-count.service.js +34 -0
  66. package/dist/backend/services/mfa-enrolment-count.service.js.map +1 -0
  67. package/dist/backend/services/mfa-enrolment-service.d.ts +54 -0
  68. package/dist/backend/services/mfa-enrolment-service.d.ts.map +1 -0
  69. package/dist/backend/services/mfa-enrolment-service.js +217 -0
  70. package/dist/backend/services/mfa-enrolment-service.js.map +1 -0
  71. package/dist/backend/services/mfa-enrolment-state.service.d.ts +27 -0
  72. package/dist/backend/services/mfa-enrolment-state.service.d.ts.map +1 -0
  73. package/dist/backend/services/mfa-enrolment-state.service.js +33 -0
  74. package/dist/backend/services/mfa-enrolment-state.service.js.map +1 -0
  75. package/dist/backend/services/mfa-login-service.d.ts +56 -0
  76. package/dist/backend/services/mfa-login-service.d.ts.map +1 -0
  77. package/dist/backend/services/mfa-login-service.js +89 -0
  78. package/dist/backend/services/mfa-login-service.js.map +1 -0
  79. package/dist/backend/services/mfa-org-policy-service.d.ts +16 -0
  80. package/dist/backend/services/mfa-org-policy-service.d.ts.map +1 -0
  81. package/dist/backend/services/mfa-org-policy-service.js +43 -0
  82. package/dist/backend/services/mfa-org-policy-service.js.map +1 -0
  83. package/dist/backend/services/mfa-policy-resolver.d.ts +47 -0
  84. package/dist/backend/services/mfa-policy-resolver.d.ts.map +1 -0
  85. package/dist/backend/services/mfa-policy-resolver.js +82 -0
  86. package/dist/backend/services/mfa-policy-resolver.js.map +1 -0
  87. package/dist/backend/services/oauth-provider-service.d.ts +65 -0
  88. package/dist/backend/services/oauth-provider-service.d.ts.map +1 -0
  89. package/dist/backend/services/oauth-provider-service.js +80 -0
  90. package/dist/backend/services/oauth-provider-service.js.map +1 -0
  91. package/dist/backend/services/recovery-codes.d.ts +10 -0
  92. package/dist/backend/services/recovery-codes.d.ts.map +1 -0
  93. package/dist/backend/services/recovery-codes.js +18 -0
  94. package/dist/backend/services/recovery-codes.js.map +1 -0
  95. package/dist/backend/services/secret-cipher.d.ts +16 -0
  96. package/dist/backend/services/secret-cipher.d.ts.map +1 -0
  97. package/dist/backend/services/secret-cipher.js +52 -0
  98. package/dist/backend/services/secret-cipher.js.map +1 -0
  99. package/dist/backend/services/social-identity-service.d.ts +40 -0
  100. package/dist/backend/services/social-identity-service.d.ts.map +1 -0
  101. package/dist/backend/services/social-identity-service.js +69 -0
  102. package/dist/backend/services/social-identity-service.js.map +1 -0
  103. package/dist/backend/services/social-link-service.d.ts +63 -0
  104. package/dist/backend/services/social-link-service.d.ts.map +1 -0
  105. package/dist/backend/services/social-link-service.js +121 -0
  106. package/dist/backend/services/social-link-service.js.map +1 -0
  107. package/dist/backend/services/totp.d.ts +11 -0
  108. package/dist/backend/services/totp.d.ts.map +1 -0
  109. package/dist/backend/services/totp.js +46 -0
  110. package/dist/backend/services/totp.js.map +1 -0
  111. package/dist/manifest.d.ts +184 -0
  112. package/dist/manifest.d.ts.map +1 -0
  113. package/dist/manifest.js +264 -0
  114. package/dist/manifest.js.map +1 -0
  115. package/dist/migrations/20260611T140409_mfa_init.d.ts +22 -0
  116. package/dist/migrations/20260611T140409_mfa_init.d.ts.map +1 -0
  117. package/dist/migrations/20260611T140409_mfa_init.js +94 -0
  118. package/dist/migrations/20260611T140409_mfa_init.js.map +1 -0
  119. package/dist/migrations/index.d.ts +27 -0
  120. package/dist/migrations/index.d.ts.map +1 -0
  121. package/dist/migrations/index.js +29 -0
  122. package/dist/migrations/index.js.map +1 -0
  123. package/i18n/en.json +14 -0
  124. package/i18n/pl.json +14 -0
  125. package/package.json +102 -0
  126. package/tailwind.css +14 -0
@@ -0,0 +1,217 @@
1
+ import { HttpError } from '@endora-commerce/platform/http';
2
+ import { MfaEnrolment } from '../entities/mfa-enrolment.entity.js';
3
+ import { MfaRecoveryCode } from '../entities/mfa-recovery-code.entity.js';
4
+ import { recordAuditFromContext } from '@endora-commerce/platform/commands';
5
+ import { generateRecoveryCodes, hashRecoveryCode, } from './recovery-codes.js';
6
+ import { generateSecret, verifyTotpStep } from './totp.js';
7
+ /**
8
+ * TOTP enrolment lifecycle (feature 042): setup → activate (issue recovery
9
+ * codes) → disable / regenerate, plus the second-factor verification used by
10
+ * the login flow. The TOTP secret is encrypted at rest via `SecretCipher`;
11
+ * recovery codes are stored only as SHA-256 hashes.
12
+ */
13
+ export class MfaEnrolmentService {
14
+ emFactory;
15
+ cipher;
16
+ auditLog;
17
+ constructor(emFactory, cipher, auditLog) {
18
+ this.emFactory = emFactory;
19
+ this.cipher = cipher;
20
+ this.auditLog = auditLog;
21
+ }
22
+ #audit(em, action, subject) {
23
+ if (this.auditLog) {
24
+ recordAuditFromContext(this.auditLog, em, {
25
+ action,
26
+ objectType: 'mfa_enrolment',
27
+ objectId: `${subject.subjectType}:${subject.subjectId}`,
28
+ stateBefore: null,
29
+ stateAfter: null,
30
+ });
31
+ }
32
+ }
33
+ /** Begin (or restart) enrolment — creates a `pending` enrolment. */
34
+ async setup(subject, accountLabel) {
35
+ const em = this.emFactory();
36
+ const active = await em.findOne(MfaEnrolment, {
37
+ subjectType: subject.subjectType,
38
+ subjectId: subject.subjectId,
39
+ status: 'active',
40
+ });
41
+ if (active) {
42
+ throw new HttpError(409, 'MFA_ALREADY_ENROLLED', '2FA is already enabled for this account.');
43
+ }
44
+ // Replace any half-finished pending enrolment.
45
+ const pendings = await em.find(MfaEnrolment, {
46
+ subjectType: subject.subjectType,
47
+ subjectId: subject.subjectId,
48
+ status: 'pending',
49
+ });
50
+ for (const p of pendings)
51
+ em.remove(p);
52
+ const { secret, otpauthUri } = generateSecret(accountLabel);
53
+ const enc = this.cipher.encrypt(secret);
54
+ const enrolment = em.create(MfaEnrolment, {
55
+ subjectType: subject.subjectType,
56
+ subjectId: subject.subjectId,
57
+ status: 'pending',
58
+ secretCiphertext: enc.ciphertext,
59
+ secretIv: enc.iv,
60
+ secretAuthTag: enc.authTag,
61
+ });
62
+ em.persist(enrolment);
63
+ this.#audit(em, 'mfa.setup', subject);
64
+ await em.flush();
65
+ return { secret, otpauthUri };
66
+ }
67
+ /** Confirm the pending enrolment with a live code; issue recovery codes. */
68
+ async activate(subject, code) {
69
+ const em = this.emFactory();
70
+ const pending = await em.findOne(MfaEnrolment, {
71
+ subjectType: subject.subjectType,
72
+ subjectId: subject.subjectId,
73
+ status: 'pending',
74
+ });
75
+ if (!pending) {
76
+ throw new HttpError(400, 'MFA_NO_PENDING_ENROLMENT', 'No pending 2FA setup to confirm.');
77
+ }
78
+ const secret = this.decryptSecret(pending);
79
+ const step = verifyTotpStep(secret, code);
80
+ if (step === null) {
81
+ throw new HttpError(401, 'MFA_INVALID_CODE', 'The code is invalid or expired.');
82
+ }
83
+ pending.status = 'active';
84
+ pending.confirmedAt = new Date();
85
+ pending.lastAcceptedStep = String(step);
86
+ const codes = generateRecoveryCodes();
87
+ for (const c of codes) {
88
+ em.persist(em.create(MfaRecoveryCode, {
89
+ enrolmentId: pending.id,
90
+ codeHash: hashRecoveryCode(c),
91
+ }));
92
+ }
93
+ this.#audit(em, 'mfa.activate', subject);
94
+ await em.flush();
95
+ return { recoveryCodes: codes };
96
+ }
97
+ /**
98
+ * Admin reset — clear ALL enrolments (active + pending) for a subject.
99
+ * Recovery codes cascade. Returns whether anything was removed (so a bulk
100
+ * caller can report affected vs skipped). FR-025.
101
+ */
102
+ async reset(subject) {
103
+ const em = this.emFactory();
104
+ const affected = await em.nativeDelete(MfaEnrolment, {
105
+ subjectType: subject.subjectType,
106
+ subjectId: subject.subjectId,
107
+ });
108
+ if (affected > 0) {
109
+ this.#audit(em, 'mfa.reset', subject);
110
+ await em.flush();
111
+ }
112
+ return affected > 0;
113
+ }
114
+ /** Disable 2FA for the subject (idempotent). Removes secret + recovery codes. */
115
+ async disable(subject) {
116
+ const em = this.emFactory();
117
+ const active = await em.findOne(MfaEnrolment, {
118
+ subjectType: subject.subjectType,
119
+ subjectId: subject.subjectId,
120
+ status: 'active',
121
+ });
122
+ if (!active)
123
+ return;
124
+ await em.nativeDelete(MfaRecoveryCode, { enrolmentId: active.id });
125
+ this.#audit(em, 'mfa.disable', subject);
126
+ await em.removeAndFlush(active);
127
+ }
128
+ /** Invalidate the old recovery-code set and issue a fresh one. */
129
+ async regenerateRecoveryCodes(subject) {
130
+ const em = this.emFactory();
131
+ const active = await em.findOne(MfaEnrolment, {
132
+ subjectType: subject.subjectType,
133
+ subjectId: subject.subjectId,
134
+ status: 'active',
135
+ });
136
+ if (!active) {
137
+ throw new HttpError(400, 'MFA_NO_ACTIVE_ENROLMENT', '2FA is not enabled for this account.');
138
+ }
139
+ await em.nativeDelete(MfaRecoveryCode, { enrolmentId: active.id });
140
+ const codes = generateRecoveryCodes();
141
+ for (const c of codes) {
142
+ em.persist(em.create(MfaRecoveryCode, {
143
+ enrolmentId: active.id,
144
+ codeHash: hashRecoveryCode(c),
145
+ }));
146
+ }
147
+ this.#audit(em, 'mfa.regenerate_recovery_codes', subject);
148
+ await em.flush();
149
+ return { recoveryCodes: codes };
150
+ }
151
+ async status(subject) {
152
+ const em = this.emFactory();
153
+ const active = await em.findOne(MfaEnrolment, {
154
+ subjectType: subject.subjectType,
155
+ subjectId: subject.subjectId,
156
+ status: 'active',
157
+ });
158
+ if (!active)
159
+ return { totpActive: false, recoveryCodesRemaining: 0 };
160
+ const recoveryCodesRemaining = await em.count(MfaRecoveryCode, {
161
+ enrolmentId: active.id,
162
+ usedAt: null,
163
+ });
164
+ return { totpActive: true, recoveryCodesRemaining };
165
+ }
166
+ /**
167
+ * Verify a second factor (TOTP or recovery code) for an active enrolment.
168
+ * A 6-digit input is treated as a TOTP code (with replay guard); anything
169
+ * else is matched against unused recovery codes (single-use). Reports which
170
+ * factor matched so callers can audit recovery-code use.
171
+ */
172
+ async verifySecondFactor(subject, code) {
173
+ // command-coverage-ignore: per-login 2FA verification — advances the TOTP
174
+ // replay-guard step and consumes a one-time recovery code; auth-flow
175
+ // bookkeeping (the enrolment lifecycle setup/activate/disable is audited).
176
+ const em = this.emFactory();
177
+ const active = await em.findOne(MfaEnrolment, {
178
+ subjectType: subject.subjectType,
179
+ subjectId: subject.subjectId,
180
+ status: 'active',
181
+ });
182
+ if (!active)
183
+ return { ok: false };
184
+ const normalised = code.replace(/\s+/g, '');
185
+ if (/^\d{6}$/.test(normalised)) {
186
+ const secret = this.decryptSecret(active);
187
+ const step = verifyTotpStep(secret, normalised);
188
+ if (step === null)
189
+ return { ok: false };
190
+ // Replay guard — a step at or before the last accepted one is rejected.
191
+ if (active.lastAcceptedStep != null && Number(active.lastAcceptedStep) >= step) {
192
+ return { ok: false };
193
+ }
194
+ active.lastAcceptedStep = String(step);
195
+ await em.flush();
196
+ return { ok: true, factor: 'totp' };
197
+ }
198
+ const rc = await em.findOne(MfaRecoveryCode, {
199
+ enrolmentId: active.id,
200
+ codeHash: hashRecoveryCode(normalised),
201
+ usedAt: null,
202
+ });
203
+ if (!rc)
204
+ return { ok: false };
205
+ rc.usedAt = new Date();
206
+ await em.flush();
207
+ return { ok: true, factor: 'recovery' };
208
+ }
209
+ decryptSecret(enrolment) {
210
+ return this.cipher.decrypt({
211
+ ciphertext: enrolment.secretCiphertext,
212
+ iv: enrolment.secretIv,
213
+ authTag: enrolment.secretAuthTag,
214
+ });
215
+ }
216
+ }
217
+ //# sourceMappingURL=mfa-enrolment-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mfa-enrolment-service.js","sourceRoot":"","sources":["../../../src/backend/services/mfa-enrolment-service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql';
2
+ import type { MfaEnrolmentStatePort, MfaSubjectType } from '@endora-commerce/contracts';
3
+ /**
4
+ * Who currently holds a second factor, for the surfaces that publish
5
+ * `twoFactorEnabled`.
6
+ *
7
+ * It lives here for the reason `MfaEnrolmentCountService` beside it does:
8
+ * `mfa_enrolments` is this module's table, and the two identity modules that
9
+ * publish the field own the *subject*, not its enrolment. Until this port
10
+ * existed each of them derived the field from a `two_factor_confirmed_at`
11
+ * column on its own table that no code has ever written — so the answer was a
12
+ * constant `false` on six responses, including the buyer's own account page.
13
+ *
14
+ * The read is a subset over the ids the caller already has, not a count and not
15
+ * a per-row lookup: every caller is a list surface, and one query per row
16
+ * behind `/admin-users` is how a correct field becomes a slow screen.
17
+ *
18
+ * `subjectId` is indexed and `status` is indexed; the pair is exactly the
19
+ * partial unique index the enrolment table was created with, so the subset is a
20
+ * single index scan.
21
+ */
22
+ export declare class MfaEnrolmentStateService implements MfaEnrolmentStatePort {
23
+ private readonly emFactory;
24
+ constructor(emFactory: () => EntityManager);
25
+ activeSubjectIds(subjectType: MfaSubjectType, subjectIds: readonly string[]): Promise<string[]>;
26
+ }
27
+ //# sourceMappingURL=mfa-enrolment-state.service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mfa-enrolment-state.service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/mfa-enrolment-state.service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EAAE,qBAAqB,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAGxF;;;;;;;;;;;;;;;;;;GAkBG;AACH,qBAAa,wBAAyB,YAAW,qBAAqB;IACxD,OAAO,CAAC,QAAQ,CAAC,SAAS;gBAAT,SAAS,EAAE,MAAM,aAAa;IAErD,gBAAgB,CACpB,WAAW,EAAE,cAAc,EAC3B,UAAU,EAAE,SAAS,MAAM,EAAE,GAC5B,OAAO,CAAC,MAAM,EAAE,CAAC;CASrB"}
@@ -0,0 +1,33 @@
1
+ import { MfaEnrolment } from '../entities/mfa-enrolment.entity.js';
2
+ /**
3
+ * Who currently holds a second factor, for the surfaces that publish
4
+ * `twoFactorEnabled`.
5
+ *
6
+ * It lives here for the reason `MfaEnrolmentCountService` beside it does:
7
+ * `mfa_enrolments` is this module's table, and the two identity modules that
8
+ * publish the field own the *subject*, not its enrolment. Until this port
9
+ * existed each of them derived the field from a `two_factor_confirmed_at`
10
+ * column on its own table that no code has ever written — so the answer was a
11
+ * constant `false` on six responses, including the buyer's own account page.
12
+ *
13
+ * The read is a subset over the ids the caller already has, not a count and not
14
+ * a per-row lookup: every caller is a list surface, and one query per row
15
+ * behind `/admin-users` is how a correct field becomes a slow screen.
16
+ *
17
+ * `subjectId` is indexed and `status` is indexed; the pair is exactly the
18
+ * partial unique index the enrolment table was created with, so the subset is a
19
+ * single index scan.
20
+ */
21
+ export class MfaEnrolmentStateService {
22
+ emFactory;
23
+ constructor(emFactory) {
24
+ this.emFactory = emFactory;
25
+ }
26
+ async activeSubjectIds(subjectType, subjectIds) {
27
+ if (subjectIds.length === 0)
28
+ return [];
29
+ const rows = await this.emFactory().find(MfaEnrolment, { status: 'active', subjectType, subjectId: { $in: [...new Set(subjectIds)] } }, { fields: ['subjectId'] });
30
+ return rows.map((row) => row.subjectId);
31
+ }
32
+ }
33
+ //# sourceMappingURL=mfa-enrolment-state.service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mfa-enrolment-state.service.js","sourceRoot":"","sources":["../../../src/backend/services/mfa-enrolment-state.service.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,MAAM,qCAAqC,CAAC;AAEnE;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,OAAO,wBAAwB;IACN;IAA7B,YAA6B,SAA8B;QAA9B,cAAS,GAAT,SAAS,CAAqB;IAAG,CAAC;IAE/D,KAAK,CAAC,gBAAgB,CACpB,WAA2B,EAC3B,UAA6B;QAE7B,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,EAAE,CAAC;QACvC,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,SAAS,EAAE,CAAC,IAAI,CACtC,YAAY,EACZ,EAAE,MAAM,EAAE,QAAQ,EAAE,WAAW,EAAE,SAAS,EAAE,EAAE,GAAG,EAAE,CAAC,GAAG,IAAI,GAAG,CAAC,UAAU,CAAC,CAAC,EAAE,EAAE,EAC/E,EAAE,MAAM,EAAE,CAAC,WAAW,CAAC,EAAE,CAC1B,CAAC;QACF,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IAC1C,CAAC;CACF"}
@@ -0,0 +1,56 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql';
2
+ import type { MfaLoginContext, MfaLoginDecision, MfaLoginPort, MfaSubjectRef } from '@endora-commerce/contracts';
3
+ import type { ChallengeStore } from './challenge-store.js';
4
+ import type { MfaPolicyResolver } from './mfa-policy-resolver.js';
5
+ import type { MfaEnrolmentService } from './mfa-enrolment-service.js';
6
+ /** Result of completing the second step. */
7
+ export type MfaVerifyResult = {
8
+ ok: true;
9
+ subject: MfaSubjectRef;
10
+ factor: 'totp' | 'recovery';
11
+ } | {
12
+ ok: false;
13
+ error: 'invalid_challenge';
14
+ } | {
15
+ ok: false;
16
+ error: 'invalid_code' | 'locked';
17
+ subject: MfaSubjectRef;
18
+ };
19
+ /**
20
+ * Orchestrates the post-first-factor MFA decision (feature 042, R4).
21
+ * Implements the `MfaLoginPort` consumed by the login services.
22
+ *
23
+ * The second-step verification (`verifySecondFactor`) and the enrolment
24
+ * endpoints arrive with User Story 1; this Phase-2 slice provides the
25
+ * `beginLogin` decision so login can already return the two-step union while
26
+ * behaving as password-only until a subject actually enrols or enforcement is
27
+ * switched on.
28
+ */
29
+ export declare class MfaLoginService implements MfaLoginPort {
30
+ private readonly emFactory;
31
+ private readonly challengeStore;
32
+ private readonly policyResolver;
33
+ /** Present once the cipher key is configured; required by `verifyChallenge`. */
34
+ private readonly enrolmentService?;
35
+ constructor(emFactory: () => EntityManager, challengeStore: ChallengeStore, policyResolver: MfaPolicyResolver,
36
+ /** Present once the cipher key is configured; required by `verifyChallenge`. */
37
+ enrolmentService?: MfaEnrolmentService | undefined);
38
+ beginLogin(subject: MfaSubjectRef, ctx: MfaLoginContext): Promise<MfaLoginDecision>;
39
+ /**
40
+ * Whether the subject currently has an active TOTP enrolment.
41
+ *
42
+ * D-96.4 — no longer part of `MfaLoginPort`: it had no consumer in `src/`,
43
+ * and the port answers exactly one question now. Kept on the service because
44
+ * it is a fact about this module's own table.
45
+ */
46
+ isTwoFactorActive(subject: MfaSubjectRef): Promise<boolean>;
47
+ /**
48
+ * Complete the second step: verify the code against the challenge's subject.
49
+ * On failure, the attempt budget is decremented and the challenge burned when
50
+ * exhausted. On success the challenge is consumed and the subject returned so
51
+ * the route can mint the session.
52
+ */
53
+ verifyChallenge(challengeId: string, code: string): Promise<MfaVerifyResult>;
54
+ private findActiveEnrolment;
55
+ }
56
+ //# sourceMappingURL=mfa-login-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mfa-login-service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/mfa-login-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EACV,eAAe,EACf,gBAAgB,EAChB,YAAY,EACZ,aAAa,EACd,MAAM,4BAA4B,CAAC;AAEpC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAClE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAEtE,4CAA4C;AAC5C,MAAM,MAAM,eAAe,GACvB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,aAAa,CAAC;IAAC,MAAM,EAAE,MAAM,GAAG,UAAU,CAAA;CAAE,GACjE;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,mBAAmB,CAAA;CAAE,GACzC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,cAAc,GAAG,QAAQ,CAAC;IAAC,OAAO,EAAE,aAAa,CAAA;CAAE,CAAC;AAE5E;;;;;;;;;GASG;AACH,qBAAa,eAAgB,YAAW,YAAY;IAEhD,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,cAAc;IAC/B,OAAO,CAAC,QAAQ,CAAC,cAAc;IAC/B,gFAAgF;IAChF,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAC;gBAJjB,SAAS,EAAE,MAAM,aAAa,EAC9B,cAAc,EAAE,cAAc,EAC9B,cAAc,EAAE,iBAAiB;IAClD,gFAAgF;IAC/D,gBAAgB,CAAC,EAAE,mBAAmB,YAAA;IAGnD,UAAU,CACd,OAAO,EAAE,aAAa,EACtB,GAAG,EAAE,eAAe,GACnB,OAAO,CAAC,gBAAgB,CAAC;IAwB5B;;;;;;OAMG;IACG,iBAAiB,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,OAAO,CAAC;IAIjE;;;;;OAKG;IACG,eAAe,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC;YAkBpE,mBAAmB;CAUlC"}
@@ -0,0 +1,89 @@
1
+ import { MfaEnrolment } from '../entities/mfa-enrolment.entity.js';
2
+ /**
3
+ * Orchestrates the post-first-factor MFA decision (feature 042, R4).
4
+ * Implements the `MfaLoginPort` consumed by the login services.
5
+ *
6
+ * The second-step verification (`verifySecondFactor`) and the enrolment
7
+ * endpoints arrive with User Story 1; this Phase-2 slice provides the
8
+ * `beginLogin` decision so login can already return the two-step union while
9
+ * behaving as password-only until a subject actually enrols or enforcement is
10
+ * switched on.
11
+ */
12
+ export class MfaLoginService {
13
+ emFactory;
14
+ challengeStore;
15
+ policyResolver;
16
+ enrolmentService;
17
+ constructor(emFactory, challengeStore, policyResolver,
18
+ /** Present once the cipher key is configured; required by `verifyChallenge`. */
19
+ enrolmentService) {
20
+ this.emFactory = emFactory;
21
+ this.challengeStore = challengeStore;
22
+ this.policyResolver = policyResolver;
23
+ this.enrolmentService = enrolmentService;
24
+ }
25
+ async beginLogin(subject, ctx) {
26
+ const active = await this.findActiveEnrolment(subject);
27
+ if (active) {
28
+ const challengeId = await this.challengeStore.issueChallenge({
29
+ subjectType: subject.subjectType,
30
+ subjectId: subject.subjectId,
31
+ salesChannelId: ctx.salesChannelId,
32
+ });
33
+ return { kind: 'challenge', challengeId };
34
+ }
35
+ const policy = await this.policyResolver.resolve(subject, ctx);
36
+ if (policy.totpEnforced) {
37
+ const setupTicket = await this.challengeStore.issueSetupTicket({
38
+ subjectType: subject.subjectType,
39
+ subjectId: subject.subjectId,
40
+ salesChannelId: ctx.salesChannelId,
41
+ });
42
+ return { kind: 'setup', setupTicket };
43
+ }
44
+ return { kind: 'proceed' };
45
+ }
46
+ /**
47
+ * Whether the subject currently has an active TOTP enrolment.
48
+ *
49
+ * D-96.4 — no longer part of `MfaLoginPort`: it had no consumer in `src/`,
50
+ * and the port answers exactly one question now. Kept on the service because
51
+ * it is a fact about this module's own table.
52
+ */
53
+ async isTwoFactorActive(subject) {
54
+ return (await this.findActiveEnrolment(subject)) !== null;
55
+ }
56
+ /**
57
+ * Complete the second step: verify the code against the challenge's subject.
58
+ * On failure, the attempt budget is decremented and the challenge burned when
59
+ * exhausted. On success the challenge is consumed and the subject returned so
60
+ * the route can mint the session.
61
+ */
62
+ async verifyChallenge(challengeId, code) {
63
+ if (!this.enrolmentService)
64
+ return { ok: false, error: 'invalid_challenge' };
65
+ const challenge = await this.challengeStore.getChallenge(challengeId);
66
+ if (!challenge)
67
+ return { ok: false, error: 'invalid_challenge' };
68
+ const subject = {
69
+ subjectType: challenge.subjectType,
70
+ subjectId: challenge.subjectId,
71
+ };
72
+ const verified = await this.enrolmentService.verifySecondFactor(subject, code);
73
+ if (!verified.ok) {
74
+ const remaining = await this.challengeStore.recordFailedAttempt(challengeId);
75
+ return { ok: false, error: remaining <= 0 ? 'locked' : 'invalid_code', subject };
76
+ }
77
+ await this.challengeStore.consumeChallenge(challengeId);
78
+ return { ok: true, subject, factor: verified.factor ?? 'totp' };
79
+ }
80
+ async findActiveEnrolment(subject) {
81
+ const em = this.emFactory();
82
+ return em.findOne(MfaEnrolment, {
83
+ subjectType: subject.subjectType,
84
+ subjectId: subject.subjectId,
85
+ status: 'active',
86
+ });
87
+ }
88
+ }
89
+ //# sourceMappingURL=mfa-login-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mfa-login-service.js","sourceRoot":"","sources":["../../../src/backend/services/mfa-login-service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql';
2
+ import type { AuditPort } from '@endora-commerce/platform/kernel';
3
+ /**
4
+ * Per-organization 2FA enforcement (feature 042, US3, FR-014). Owned by the
5
+ * `mfa` module; consulted additively by `MfaPolicyResolver`.
6
+ */
7
+ export declare class MfaOrgPolicyService {
8
+ private readonly emFactory;
9
+ private readonly auditLog?;
10
+ constructor(emFactory: () => EntityManager, auditLog?: AuditPort | undefined);
11
+ setEnforcement(organizationId: string, enforceTotp: boolean, actor: string): Promise<{
12
+ enforceTotp: boolean;
13
+ }>;
14
+ getEnforcement(organizationId: string): Promise<boolean>;
15
+ }
16
+ //# sourceMappingURL=mfa-org-policy-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mfa-org-policy-service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/mfa-org-policy-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAG3D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kCAAkC,CAAC;AAElE;;;GAGG;AACH,qBAAa,mBAAmB;IAE5B,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC;gBADT,SAAS,EAAE,MAAM,aAAa,EAC9B,QAAQ,CAAC,EAAE,SAAS,YAAA;IAGjC,cAAc,CAClB,cAAc,EAAE,MAAM,EACtB,WAAW,EAAE,OAAO,EACpB,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC;QAAE,WAAW,EAAE,OAAO,CAAA;KAAE,CAAC;IAuB9B,cAAc,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;CAK/D"}
@@ -0,0 +1,43 @@
1
+ import { MfaOrganizationPolicy } from '../entities/mfa-organization-policy.entity.js';
2
+ import { recordAuditFromContext } from '@endora-commerce/platform/commands';
3
+ /**
4
+ * Per-organization 2FA enforcement (feature 042, US3, FR-014). Owned by the
5
+ * `mfa` module; consulted additively by `MfaPolicyResolver`.
6
+ */
7
+ export class MfaOrgPolicyService {
8
+ emFactory;
9
+ auditLog;
10
+ constructor(emFactory, auditLog) {
11
+ this.emFactory = emFactory;
12
+ this.auditLog = auditLog;
13
+ }
14
+ async setEnforcement(organizationId, enforceTotp, actor) {
15
+ const em = this.emFactory();
16
+ let policy = await em.findOne(MfaOrganizationPolicy, { organizationId });
17
+ if (!policy) {
18
+ policy = em.create(MfaOrganizationPolicy, { organizationId, enforceTotp, updatedByActor: actor });
19
+ em.persist(policy);
20
+ }
21
+ else {
22
+ policy.enforceTotp = enforceTotp;
23
+ policy.updatedByActor = actor;
24
+ }
25
+ if (this.auditLog) {
26
+ recordAuditFromContext(this.auditLog, em, {
27
+ action: 'mfa.set_org_enforcement',
28
+ objectType: 'mfa_organization_policy',
29
+ objectId: organizationId,
30
+ stateBefore: null,
31
+ stateAfter: { enforceTotp },
32
+ });
33
+ }
34
+ await em.flush();
35
+ return { enforceTotp };
36
+ }
37
+ async getEnforcement(organizationId) {
38
+ const em = this.emFactory();
39
+ const policy = await em.findOne(MfaOrganizationPolicy, { organizationId });
40
+ return policy?.enforceTotp ?? false;
41
+ }
42
+ }
43
+ //# sourceMappingURL=mfa-org-policy-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mfa-org-policy-service.js","sourceRoot":"","sources":["../../../src/backend/services/mfa-org-policy-service.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,qBAAqB,EAAE,MAAM,+CAA+C,CAAC;AACtF,OAAO,EAAE,sBAAsB,EAAE,MAAM,oCAAoC,CAAC;AAG5E;;;GAGG;AACH,MAAM,OAAO,mBAAmB;IAEX;IACA;IAFnB,YACmB,SAA8B,EAC9B,QAAoB;QADpB,cAAS,GAAT,SAAS,CAAqB;QAC9B,aAAQ,GAAR,QAAQ,CAAY;IACpC,CAAC;IAEJ,KAAK,CAAC,cAAc,CAClB,cAAsB,EACtB,WAAoB,EACpB,KAAa;QAEb,MAAM,EAAE,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;QAC5B,IAAI,MAAM,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,qBAAqB,EAAE,EAAE,cAAc,EAAE,CAAC,CAAC;QACzE,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,MAAM,GAAG,EAAE,CAAC,MAAM,CAAC,qBAAqB,EAAE,EAAE,cAAc,EAAE,WAAW,EAAE,cAAc,EAAE,KAAK,EAAE,CAAC,CAAC;YAClG,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QACrB,CAAC;aAAM,CAAC;YACN,MAAM,CAAC,WAAW,GAAG,WAAW,CAAC;YACjC,MAAM,CAAC,cAAc,GAAG,KAAK,CAAC;QAChC,CAAC;QACD,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAClB,sBAAsB,CAAC,IAAI,CAAC,QAAQ,EAAE,EAAE,EAAE;gBACxC,MAAM,EAAE,yBAAyB;gBACjC,UAAU,EAAE,yBAAyB;gBACrC,QAAQ,EAAE,cAAc;gBACxB,WAAW,EAAE,IAAI;gBACjB,UAAU,EAAE,EAAE,WAAW,EAAE;aAC5B,CAAC,CAAC;QACL,CAAC;QACD,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC;QACjB,OAAO,EAAE,WAAW,EAAE,CAAC;IACzB,CAAC;IAED,KAAK,CAAC,cAAc,CAAC,cAAsB;QACzC,MAAM,EAAE,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;QAC5B,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,qBAAqB,EAAE,EAAE,cAAc,EAAE,CAAC,CAAC;QAC3E,OAAO,MAAM,EAAE,WAAW,IAAI,KAAK,CAAC;IACtC,CAAC;CACF"}
@@ -0,0 +1,47 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql';
2
+ import { z } from 'zod';
3
+ import type { MfaLoginContext, MfaSubjectRef } from '@endora-commerce/contracts';
4
+ /**
5
+ * Resolves the effective MFA policy for a subject (feature 042, R5).
6
+ *
7
+ * Reuses the Settings engine's most-specific-wins resolution: storefront
8
+ * settings read at the subject's sales channel (per-channel value → global →
9
+ * default); admin settings are read platform-wide, which since feature 072
10
+ * (D-41) has a spelling of its own — `null`. Organization enforcement is
11
+ * additive. The invariant `enforced ⇒ enabled` (FR-016) is applied here.
12
+ *
13
+ * Every settings read is defensive: if a setting is not yet registered (e.g. a
14
+ * narrow test that skips manifest seeding) the resolver treats it as `false`,
15
+ * so login degrades safely to password-only rather than erroring.
16
+ */
17
+ export interface MfaResolvedPolicy {
18
+ totpEnabled: boolean;
19
+ totpEnforced: boolean;
20
+ googleEnabled: boolean;
21
+ microsoftEnabled: boolean;
22
+ }
23
+ /** The slice of SettingsService the resolver needs (port — not the class). */
24
+ export interface SettingsReader {
25
+ get<T>(code: string, salesChannelId: string | null, schema: z.ZodType<T>): Promise<T>;
26
+ }
27
+ export declare class MfaPolicyResolver {
28
+ private readonly settings;
29
+ private readonly emFactory;
30
+ /**
31
+ * Resolves the system-default sales-channel id for a storefront request
32
+ * that arrived without one. `settingsService.get` requires a real channel
33
+ * uuid or `null`, so a literal sentinel cannot be used.
34
+ */
35
+ private readonly resolveDefaultChannelId;
36
+ constructor(settings: SettingsReader, emFactory: () => EntityManager,
37
+ /**
38
+ * Resolves the system-default sales-channel id for a storefront request
39
+ * that arrived without one. `settingsService.get` requires a real channel
40
+ * uuid or `null`, so a literal sentinel cannot be used.
41
+ */
42
+ resolveDefaultChannelId?: () => Promise<string | null>);
43
+ resolve(subject: MfaSubjectRef, ctx: MfaLoginContext): Promise<MfaResolvedPolicy>;
44
+ private readBool;
45
+ private readOrgEnforcement;
46
+ }
47
+ //# sourceMappingURL=mfa-policy-resolver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mfa-policy-resolver.d.ts","sourceRoot":"","sources":["../../../src/backend/services/mfa-policy-resolver.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAGjF;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,OAAO,CAAC;IACrB,YAAY,EAAE,OAAO,CAAC;IACtB,aAAa,EAAE,OAAO,CAAC;IACvB,gBAAgB,EAAE,OAAO,CAAC;CAC3B;AAED,8EAA8E;AAC9E,MAAM,WAAW,cAAc;IAC7B,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;CACvF;AAED,qBAAa,iBAAiB;IAE1B,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,uBAAuB;gBAPvB,QAAQ,EAAE,cAAc,EACxB,SAAS,EAAE,MAAM,aAAa;IAC/C;;;;OAIG;IACc,uBAAuB,GAAE,MAAM,OAAO,CAAC,MAAM,GAAG,IAAI,CAAoB;IAGrF,OAAO,CACX,OAAO,EAAE,aAAa,EACtB,GAAG,EAAE,eAAe,GACnB,OAAO,CAAC,iBAAiB,CAAC;YAmDf,QAAQ;YAQR,kBAAkB;CASjC"}