@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.
- package/LICENSE +21 -0
- package/README.md +58 -0
- package/dist/admin/index.d.ts +37 -0
- package/dist/admin/index.d.ts.map +1 -0
- package/dist/admin/index.js +12 -0
- package/dist/admin/index.js.map +1 -0
- package/dist/admin/pages/AdminSecuritySettings.d.ts +3 -0
- package/dist/admin/pages/AdminSecuritySettings.d.ts.map +1 -0
- package/dist/admin/pages/AdminSecuritySettings.js +71 -0
- package/dist/admin/pages/AdminSecuritySettings.js.map +1 -0
- package/dist/backend/entities/mfa-enrolment.entity.d.ts +33 -0
- package/dist/backend/entities/mfa-enrolment.entity.d.ts.map +1 -0
- package/dist/backend/entities/mfa-enrolment.entity.js +95 -0
- package/dist/backend/entities/mfa-enrolment.entity.js.map +1 -0
- package/dist/backend/entities/mfa-organization-policy.entity.d.ts +19 -0
- package/dist/backend/entities/mfa-organization-policy.entity.d.ts.map +1 -0
- package/dist/backend/entities/mfa-organization-policy.entity.js +60 -0
- package/dist/backend/entities/mfa-organization-policy.entity.js.map +1 -0
- package/dist/backend/entities/mfa-recovery-code.entity.d.ts +17 -0
- package/dist/backend/entities/mfa-recovery-code.entity.d.ts.map +1 -0
- package/dist/backend/entities/mfa-recovery-code.entity.js +54 -0
- package/dist/backend/entities/mfa-recovery-code.entity.js.map +1 -0
- package/dist/backend/entities/mfa-social-identity.entity.d.ts +24 -0
- package/dist/backend/entities/mfa-social-identity.entity.d.ts.map +1 -0
- package/dist/backend/entities/mfa-social-identity.entity.js +73 -0
- package/dist/backend/entities/mfa-social-identity.entity.js.map +1 -0
- package/dist/backend/index.d.ts +131 -0
- package/dist/backend/index.d.ts.map +1 -0
- package/dist/backend/index.js +173 -0
- package/dist/backend/index.js.map +1 -0
- package/dist/backend/plugin.d.ts +99 -0
- package/dist/backend/plugin.d.ts.map +1 -0
- package/dist/backend/plugin.js +111 -0
- package/dist/backend/plugin.js.map +1 -0
- package/dist/backend/routes.admin.d.ts +23 -0
- package/dist/backend/routes.admin.d.ts.map +1 -0
- package/dist/backend/routes.admin.js +93 -0
- package/dist/backend/routes.admin.js.map +1 -0
- package/dist/backend/routes.oauth.d.ts +26 -0
- package/dist/backend/routes.oauth.d.ts.map +1 -0
- package/dist/backend/routes.oauth.js +155 -0
- package/dist/backend/routes.oauth.js.map +1 -0
- package/dist/backend/routes.org.d.ts +21 -0
- package/dist/backend/routes.org.d.ts.map +1 -0
- package/dist/backend/routes.org.js +17 -0
- package/dist/backend/routes.org.js.map +1 -0
- package/dist/backend/routes.public.d.ts +21 -0
- package/dist/backend/routes.public.d.ts.map +1 -0
- package/dist/backend/routes.public.js +118 -0
- package/dist/backend/routes.public.js.map +1 -0
- package/dist/backend/routes.self-service.d.ts +34 -0
- package/dist/backend/routes.self-service.d.ts.map +1 -0
- package/dist/backend/routes.self-service.js +113 -0
- package/dist/backend/routes.self-service.js.map +1 -0
- package/dist/backend/services/account-identity.d.ts +71 -0
- package/dist/backend/services/account-identity.d.ts.map +1 -0
- package/dist/backend/services/account-identity.js +87 -0
- package/dist/backend/services/account-identity.js.map +1 -0
- package/dist/backend/services/challenge-store.d.ts +40 -0
- package/dist/backend/services/challenge-store.d.ts.map +1 -0
- package/dist/backend/services/challenge-store.js +90 -0
- package/dist/backend/services/challenge-store.js.map +1 -0
- package/dist/backend/services/mfa-enrolment-count.service.d.ts +26 -0
- package/dist/backend/services/mfa-enrolment-count.service.d.ts.map +1 -0
- package/dist/backend/services/mfa-enrolment-count.service.js +34 -0
- package/dist/backend/services/mfa-enrolment-count.service.js.map +1 -0
- package/dist/backend/services/mfa-enrolment-service.d.ts +54 -0
- package/dist/backend/services/mfa-enrolment-service.d.ts.map +1 -0
- package/dist/backend/services/mfa-enrolment-service.js +217 -0
- package/dist/backend/services/mfa-enrolment-service.js.map +1 -0
- package/dist/backend/services/mfa-enrolment-state.service.d.ts +27 -0
- package/dist/backend/services/mfa-enrolment-state.service.d.ts.map +1 -0
- package/dist/backend/services/mfa-enrolment-state.service.js +33 -0
- package/dist/backend/services/mfa-enrolment-state.service.js.map +1 -0
- package/dist/backend/services/mfa-login-service.d.ts +56 -0
- package/dist/backend/services/mfa-login-service.d.ts.map +1 -0
- package/dist/backend/services/mfa-login-service.js +89 -0
- package/dist/backend/services/mfa-login-service.js.map +1 -0
- package/dist/backend/services/mfa-org-policy-service.d.ts +16 -0
- package/dist/backend/services/mfa-org-policy-service.d.ts.map +1 -0
- package/dist/backend/services/mfa-org-policy-service.js +43 -0
- package/dist/backend/services/mfa-org-policy-service.js.map +1 -0
- package/dist/backend/services/mfa-policy-resolver.d.ts +47 -0
- package/dist/backend/services/mfa-policy-resolver.d.ts.map +1 -0
- package/dist/backend/services/mfa-policy-resolver.js +82 -0
- package/dist/backend/services/mfa-policy-resolver.js.map +1 -0
- package/dist/backend/services/oauth-provider-service.d.ts +65 -0
- package/dist/backend/services/oauth-provider-service.d.ts.map +1 -0
- package/dist/backend/services/oauth-provider-service.js +80 -0
- package/dist/backend/services/oauth-provider-service.js.map +1 -0
- package/dist/backend/services/recovery-codes.d.ts +10 -0
- package/dist/backend/services/recovery-codes.d.ts.map +1 -0
- package/dist/backend/services/recovery-codes.js +18 -0
- package/dist/backend/services/recovery-codes.js.map +1 -0
- package/dist/backend/services/secret-cipher.d.ts +16 -0
- package/dist/backend/services/secret-cipher.d.ts.map +1 -0
- package/dist/backend/services/secret-cipher.js +52 -0
- package/dist/backend/services/secret-cipher.js.map +1 -0
- package/dist/backend/services/social-identity-service.d.ts +40 -0
- package/dist/backend/services/social-identity-service.d.ts.map +1 -0
- package/dist/backend/services/social-identity-service.js +69 -0
- package/dist/backend/services/social-identity-service.js.map +1 -0
- package/dist/backend/services/social-link-service.d.ts +63 -0
- package/dist/backend/services/social-link-service.d.ts.map +1 -0
- package/dist/backend/services/social-link-service.js +121 -0
- package/dist/backend/services/social-link-service.js.map +1 -0
- package/dist/backend/services/totp.d.ts +11 -0
- package/dist/backend/services/totp.d.ts.map +1 -0
- package/dist/backend/services/totp.js +46 -0
- package/dist/backend/services/totp.js.map +1 -0
- package/dist/manifest.d.ts +184 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +264 -0
- package/dist/manifest.js.map +1 -0
- package/dist/migrations/20260611T140409_mfa_init.d.ts +22 -0
- package/dist/migrations/20260611T140409_mfa_init.d.ts.map +1 -0
- package/dist/migrations/20260611T140409_mfa_init.js +94 -0
- package/dist/migrations/20260611T140409_mfa_init.js.map +1 -0
- package/dist/migrations/index.d.ts +27 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/migrations/index.js +29 -0
- package/dist/migrations/index.js.map +1 -0
- package/i18n/en.json +14 -0
- package/i18n/pl.json +14 -0
- package/package.json +102 -0
- package/tailwind.css +14 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { mfaActivateRequestSchema, mfaDisableRequestSchema, mfaRegenerateRequestSchema, mfaSocialUnlinkParamsSchema, } from '@endora-commerce/contracts';
|
|
2
|
+
import { HttpError } from '@endora-commerce/platform/http';
|
|
3
|
+
import { currentSalesChannel } from '@endora-commerce/platform/kernel';
|
|
4
|
+
export async function registerMfaSelfServiceRoutes(app, opts) {
|
|
5
|
+
const { pathPrefix, subjectType, auditObjectType, requireGuard, resolveSubjectId, enrolmentService, policyResolver, socialLinkService, auditLogService, } = opts;
|
|
6
|
+
const subjectOf = (req) => ({
|
|
7
|
+
subjectType,
|
|
8
|
+
subjectId: resolveSubjectId(req),
|
|
9
|
+
});
|
|
10
|
+
app.post(`${pathPrefix}/setup`, { preHandler: requireGuard }, async (request) => {
|
|
11
|
+
const subject = subjectOf(request);
|
|
12
|
+
// FR-001 — enrolment is only permitted when 2FA is enabled for the scope.
|
|
13
|
+
const policy = await policyResolver.resolve(subject, {
|
|
14
|
+
salesChannelId: currentSalesChannel()?.id ?? null,
|
|
15
|
+
organizationId: opts.resolveOrganizationId?.(request) ?? null,
|
|
16
|
+
});
|
|
17
|
+
if (!policy.totpEnabled) {
|
|
18
|
+
throw new HttpError(403, 'MFA_NOT_ENABLED', 'Two-factor authentication is not enabled for your account.');
|
|
19
|
+
}
|
|
20
|
+
const label = (await opts.resolveAccountEmail(subjectType, subject.subjectId)) ?? subject.subjectId;
|
|
21
|
+
return { data: await enrolmentService.setup(subject, label) };
|
|
22
|
+
});
|
|
23
|
+
app.post(`${pathPrefix}/activate`, { preHandler: requireGuard, schema: { body: mfaActivateRequestSchema } }, async (request) => {
|
|
24
|
+
const body = mfaActivateRequestSchema.parse(request.body);
|
|
25
|
+
const subject = subjectOf(request);
|
|
26
|
+
const res = await enrolmentService.activate(subject, body.code);
|
|
27
|
+
await auditLogService.record({
|
|
28
|
+
action: 'mfa.enabled',
|
|
29
|
+
objectType: auditObjectType,
|
|
30
|
+
objectId: subject.subjectId,
|
|
31
|
+
...(request.ip ? { ipAddress: request.ip } : {}),
|
|
32
|
+
});
|
|
33
|
+
return { data: res };
|
|
34
|
+
});
|
|
35
|
+
app.post(`${pathPrefix}/disable`, { preHandler: requireGuard, schema: { body: mfaDisableRequestSchema } }, async (request) => {
|
|
36
|
+
const body = mfaDisableRequestSchema.parse(request.body);
|
|
37
|
+
const subject = subjectOf(request);
|
|
38
|
+
await reauthenticate(opts, subject, body);
|
|
39
|
+
await enrolmentService.disable(subject);
|
|
40
|
+
await auditLogService.record({
|
|
41
|
+
action: 'mfa.disabled',
|
|
42
|
+
objectType: auditObjectType,
|
|
43
|
+
objectId: subject.subjectId,
|
|
44
|
+
...(request.ip ? { ipAddress: request.ip } : {}),
|
|
45
|
+
});
|
|
46
|
+
return { data: { status: 'disabled' } };
|
|
47
|
+
});
|
|
48
|
+
app.post(`${pathPrefix}/recovery-codes/regenerate`, { preHandler: requireGuard, schema: { body: mfaRegenerateRequestSchema } }, async (request) => {
|
|
49
|
+
const body = mfaRegenerateRequestSchema.parse(request.body);
|
|
50
|
+
const subject = subjectOf(request);
|
|
51
|
+
const verified = await enrolmentService.verifySecondFactor(subject, body.code);
|
|
52
|
+
if (!verified.ok)
|
|
53
|
+
throw new HttpError(401, 'MFA_INVALID_CODE', 'The code is invalid or expired.');
|
|
54
|
+
const res = await enrolmentService.regenerateRecoveryCodes(subject);
|
|
55
|
+
await auditLogService.record({
|
|
56
|
+
action: 'mfa.recovery_codes_regenerated',
|
|
57
|
+
objectType: auditObjectType,
|
|
58
|
+
objectId: subject.subjectId,
|
|
59
|
+
...(request.ip ? { ipAddress: request.ip } : {}),
|
|
60
|
+
});
|
|
61
|
+
return { data: res };
|
|
62
|
+
});
|
|
63
|
+
app.get(`${pathPrefix}/status`, { preHandler: requireGuard }, async (request) => {
|
|
64
|
+
const subject = subjectOf(request);
|
|
65
|
+
const status = await enrolmentService.status(subject);
|
|
66
|
+
const policy = await policyResolver.resolve(subject, {
|
|
67
|
+
salesChannelId: currentSalesChannel()?.id ?? null,
|
|
68
|
+
organizationId: opts.resolveOrganizationId?.(request) ?? null,
|
|
69
|
+
});
|
|
70
|
+
return {
|
|
71
|
+
data: {
|
|
72
|
+
totpActive: status.totpActive,
|
|
73
|
+
recoveryCodesRemaining: status.recoveryCodesRemaining,
|
|
74
|
+
totpEnabledForScope: policy.totpEnabled,
|
|
75
|
+
totpEnforcedForScope: policy.totpEnforced,
|
|
76
|
+
// Issue #194 — the contract has declared this field since feature 042
|
|
77
|
+
// and the handler answered `[]` unconditionally, so the account holder
|
|
78
|
+
// could neither see a linked identity nor reach the route that removes
|
|
79
|
+
// one. The verdict travels with each link, so the surface renders the
|
|
80
|
+
// refusal instead of discovering it on the click.
|
|
81
|
+
socialLinks: await socialLinkService.list(subject),
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
/**
|
|
86
|
+
* Sever a federated identity (issue #194). Refused when it is the account's
|
|
87
|
+
* only link — see {@link SocialLinkService} for why that is the rule and what
|
|
88
|
+
* would be needed to relax it.
|
|
89
|
+
*/
|
|
90
|
+
app.delete(`${pathPrefix}/social-links/:provider`, { preHandler: requireGuard, schema: { params: mfaSocialUnlinkParamsSchema } }, async (request) => {
|
|
91
|
+
const { provider } = mfaSocialUnlinkParamsSchema.parse(request.params);
|
|
92
|
+
const subject = subjectOf(request);
|
|
93
|
+
await socialLinkService.unlink(subject, provider);
|
|
94
|
+
return { data: { status: 'unlinked', provider } };
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
/** Re-auth on self-disable: a current TOTP/recovery code or the password. */
|
|
98
|
+
async function reauthenticate(opts, subject, body) {
|
|
99
|
+
if (body.code) {
|
|
100
|
+
const verified = await opts.enrolmentService.verifySecondFactor(subject, body.code);
|
|
101
|
+
if (verified.ok)
|
|
102
|
+
return;
|
|
103
|
+
throw new HttpError(401, 'MFA_INVALID_CODE', 'The code is invalid or expired.');
|
|
104
|
+
}
|
|
105
|
+
if (body.password) {
|
|
106
|
+
const ok = await opts.verifyAccountPassword(subject.subjectType, subject.subjectId, body.password);
|
|
107
|
+
if (ok)
|
|
108
|
+
return;
|
|
109
|
+
throw new HttpError(401, 'INVALID_CREDENTIALS', 'The password is incorrect.');
|
|
110
|
+
}
|
|
111
|
+
throw new HttpError(400, 'MFA_REAUTH_REQUIRED', 'A current code is required to disable 2FA.');
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=routes.self-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.self-service.js","sourceRoot":"","sources":["../../src/backend/routes.self-service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { AdminPasswordVerificationPort, AdminUserReadPort, CustomerAccountReadPort, CustomerPasswordVerificationPort } from '@endora-commerce/contracts';
|
|
2
|
+
/**
|
|
3
|
+
* The four questions MFA asks the two identity modules about an account
|
|
4
|
+
* (`specs/110-instance-repository/` T118c).
|
|
5
|
+
*
|
|
6
|
+
* All four were `MfaActorBridge` members — closures a composition root wrote,
|
|
7
|
+
* once in `backend/src/composition.ts` and once in
|
|
8
|
+
* `backend/test/helpers/test-server.ts`. None of them was a composition's answer
|
|
9
|
+
* to give: every one is `adminUserReadPort`, `customerAccountReadPort`,
|
|
10
|
+
* `adminPasswordVerificationPort` or `customerPasswordVerificationPort` and
|
|
11
|
+
* nothing else, and the two roots spelled the same body twice — where they
|
|
12
|
+
* spelled it at all. Two of the four the harness did not spell, which is what
|
|
13
|
+
* made them invisible; see the note on each.
|
|
14
|
+
*
|
|
15
|
+
* They are functions of the ports they read rather than of a `ModuleContext`, so
|
|
16
|
+
* `backend/index.ts` supplies the real inputs and this file is testable over a
|
|
17
|
+
* stub with nothing composed.
|
|
18
|
+
*
|
|
19
|
+
* **No `catch` anywhere below, deliberately.** Each call goes through a
|
|
20
|
+
* `lazyPort` proxy that throws `ModuleDisabledError` when its owner is absent,
|
|
21
|
+
* and reading that as "this account has no e-mail" or "the password does not
|
|
22
|
+
* match" is the fail-open the composition checklist's item 7 refuses — on an
|
|
23
|
+
* authentication path, where it is the difference between requiring a second
|
|
24
|
+
* factor and accepting a credential nobody verified. `admin_users` and
|
|
25
|
+
* `customer_accounts` both declare `nonDeactivatable`, so it cannot happen
|
|
26
|
+
* today; the absence of the `catch` is what keeps it fail-closed if that ever
|
|
27
|
+
* changes.
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
* The caller's organisation, and the assertion that they may set policy for it
|
|
31
|
+
* (US3, FR-014).
|
|
32
|
+
*
|
|
33
|
+
* Takes the account id rather than the request: who is asking is
|
|
34
|
+
* `customerActorResolver`'s answer, which the platform contributes and eight
|
|
35
|
+
* other modules already read, and asking it a second time here is the duplicate
|
|
36
|
+
* spelling this drain removes.
|
|
37
|
+
*/
|
|
38
|
+
export declare function createOrganizationAdminResolver(accounts: Pick<CustomerAccountReadPort, 'findById'>): (customerAccountId: string) => Promise<{
|
|
39
|
+
organizationId: string;
|
|
40
|
+
actor: string;
|
|
41
|
+
}>;
|
|
42
|
+
/** The accounts a bulk 2FA reset by organisation covers (US6, FR-026/028). */
|
|
43
|
+
export declare function createOrganizationCustomerIdsResolver(accounts: Pick<CustomerAccountReadPort, 'listByOrganization'>): (organizationId: string) => Promise<string[]>;
|
|
44
|
+
/**
|
|
45
|
+
* The address that labels an authenticator entry.
|
|
46
|
+
*
|
|
47
|
+
* **This is one of the two members no test in the tree could see.** Production
|
|
48
|
+
* contributed it and the harness did not, so under test the label fell back to
|
|
49
|
+
* the subject id and the `otpauth://` URI carried a UUID where a real enrolment
|
|
50
|
+
* carries an e-mail address — a difference every MFA test was blind to, because
|
|
51
|
+
* the one assertion over that URI is `toContain('otpauth://totp/')`.
|
|
52
|
+
*
|
|
53
|
+
* `null` for an account that does not resolve; the route falls back to the
|
|
54
|
+
* subject id, which is a worse label and nothing more.
|
|
55
|
+
*/
|
|
56
|
+
export declare function createAccountEmailResolver(customers: Pick<CustomerAccountReadPort, 'findById'>, admins: Pick<AdminUserReadPort, 'findById'>): (subjectType: 'customer' | 'admin', subjectId: string) => Promise<string | null>;
|
|
57
|
+
/**
|
|
58
|
+
* Does the account's stored credential match the password presented?
|
|
59
|
+
*
|
|
60
|
+
* **The other member no test could see**, and the one whose absence changed
|
|
61
|
+
* behaviour rather than a label: `reauthenticate` accepts either a current
|
|
62
|
+
* second factor or the account password before disabling 2FA, and with no
|
|
63
|
+
* verifier the password branch is unreachable, so the harness answered 400
|
|
64
|
+
* `MFA_REAUTH_REQUIRED` where production answers 401 or lets the disable
|
|
65
|
+
* through. Every `/disable` call in the tree passes a code, so nothing ever
|
|
66
|
+
* took the branch that differed.
|
|
67
|
+
*
|
|
68
|
+
* Only the boolean crosses: the hash stays behind each owner's port (T052).
|
|
69
|
+
*/
|
|
70
|
+
export declare function createAccountPasswordVerifier(customers: CustomerPasswordVerificationPort, admins: AdminPasswordVerificationPort): (subjectType: 'customer' | 'admin', subjectId: string, password: string) => Promise<boolean>;
|
|
71
|
+
//# sourceMappingURL=account-identity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"account-identity.d.ts","sourceRoot":"","sources":["../../../src/backend/services/account-identity.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,6BAA6B,EAC7B,iBAAiB,EACjB,uBAAuB,EACvB,gCAAgC,EACjC,MAAM,4BAA4B,CAAC;AAGpC;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH;;;;;;;;GAQG;AACH,wBAAgB,+BAA+B,CAC7C,QAAQ,EAAE,IAAI,CAAC,uBAAuB,EAAE,UAAU,CAAC,GAClD,CAAC,iBAAiB,EAAE,MAAM,KAAK,OAAO,CAAC;IAAE,cAAc,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAYnF;AAED,8EAA8E;AAC9E,wBAAgB,qCAAqC,CACnD,QAAQ,EAAE,IAAI,CAAC,uBAAuB,EAAE,oBAAoB,CAAC,GAC5D,CAAC,cAAc,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,CAG/C;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,0BAA0B,CACxC,SAAS,EAAE,IAAI,CAAC,uBAAuB,EAAE,UAAU,CAAC,EACpD,MAAM,EAAE,IAAI,CAAC,iBAAiB,EAAE,UAAU,CAAC,GAC1C,CAAC,WAAW,EAAE,UAAU,GAAG,OAAO,EAAE,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAKlF;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,6BAA6B,CAC3C,SAAS,EAAE,gCAAgC,EAC3C,MAAM,EAAE,6BAA6B,GACpC,CACD,WAAW,EAAE,UAAU,GAAG,OAAO,EACjC,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,KACb,OAAO,CAAC,OAAO,CAAC,CAKpB"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { ERROR_CODES } from '@endora-commerce/contracts';
|
|
2
|
+
import { HttpError } from '@endora-commerce/platform/http';
|
|
3
|
+
/**
|
|
4
|
+
* The four questions MFA asks the two identity modules about an account
|
|
5
|
+
* (`specs/110-instance-repository/` T118c).
|
|
6
|
+
*
|
|
7
|
+
* All four were `MfaActorBridge` members — closures a composition root wrote,
|
|
8
|
+
* once in `backend/src/composition.ts` and once in
|
|
9
|
+
* `backend/test/helpers/test-server.ts`. None of them was a composition's answer
|
|
10
|
+
* to give: every one is `adminUserReadPort`, `customerAccountReadPort`,
|
|
11
|
+
* `adminPasswordVerificationPort` or `customerPasswordVerificationPort` and
|
|
12
|
+
* nothing else, and the two roots spelled the same body twice — where they
|
|
13
|
+
* spelled it at all. Two of the four the harness did not spell, which is what
|
|
14
|
+
* made them invisible; see the note on each.
|
|
15
|
+
*
|
|
16
|
+
* They are functions of the ports they read rather than of a `ModuleContext`, so
|
|
17
|
+
* `backend/index.ts` supplies the real inputs and this file is testable over a
|
|
18
|
+
* stub with nothing composed.
|
|
19
|
+
*
|
|
20
|
+
* **No `catch` anywhere below, deliberately.** Each call goes through a
|
|
21
|
+
* `lazyPort` proxy that throws `ModuleDisabledError` when its owner is absent,
|
|
22
|
+
* and reading that as "this account has no e-mail" or "the password does not
|
|
23
|
+
* match" is the fail-open the composition checklist's item 7 refuses — on an
|
|
24
|
+
* authentication path, where it is the difference between requiring a second
|
|
25
|
+
* factor and accepting a credential nobody verified. `admin_users` and
|
|
26
|
+
* `customer_accounts` both declare `nonDeactivatable`, so it cannot happen
|
|
27
|
+
* today; the absence of the `catch` is what keeps it fail-closed if that ever
|
|
28
|
+
* changes.
|
|
29
|
+
*/
|
|
30
|
+
/**
|
|
31
|
+
* The caller's organisation, and the assertion that they may set policy for it
|
|
32
|
+
* (US3, FR-014).
|
|
33
|
+
*
|
|
34
|
+
* Takes the account id rather than the request: who is asking is
|
|
35
|
+
* `customerActorResolver`'s answer, which the platform contributes and eight
|
|
36
|
+
* other modules already read, and asking it a second time here is the duplicate
|
|
37
|
+
* spelling this drain removes.
|
|
38
|
+
*/
|
|
39
|
+
export function createOrganizationAdminResolver(accounts) {
|
|
40
|
+
return async (customerAccountId) => {
|
|
41
|
+
const account = await accounts.findById(customerAccountId);
|
|
42
|
+
if (!account || account.role !== 'organization_admin' || !account.organizationId) {
|
|
43
|
+
throw new HttpError(403, ERROR_CODES.FORBIDDEN, 'Organization administrator role required.');
|
|
44
|
+
}
|
|
45
|
+
return { organizationId: account.organizationId, actor: account.id };
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/** The accounts a bulk 2FA reset by organisation covers (US6, FR-026/028). */
|
|
49
|
+
export function createOrganizationCustomerIdsResolver(accounts) {
|
|
50
|
+
return async (organizationId) => (await accounts.listByOrganization(organizationId)).map((account) => account.id);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The address that labels an authenticator entry.
|
|
54
|
+
*
|
|
55
|
+
* **This is one of the two members no test in the tree could see.** Production
|
|
56
|
+
* contributed it and the harness did not, so under test the label fell back to
|
|
57
|
+
* the subject id and the `otpauth://` URI carried a UUID where a real enrolment
|
|
58
|
+
* carries an e-mail address — a difference every MFA test was blind to, because
|
|
59
|
+
* the one assertion over that URI is `toContain('otpauth://totp/')`.
|
|
60
|
+
*
|
|
61
|
+
* `null` for an account that does not resolve; the route falls back to the
|
|
62
|
+
* subject id, which is a worse label and nothing more.
|
|
63
|
+
*/
|
|
64
|
+
export function createAccountEmailResolver(customers, admins) {
|
|
65
|
+
return async (subjectType, subjectId) => subjectType === 'admin'
|
|
66
|
+
? ((await admins.findById(subjectId))?.email ?? null)
|
|
67
|
+
: ((await customers.findById(subjectId))?.email ?? null);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Does the account's stored credential match the password presented?
|
|
71
|
+
*
|
|
72
|
+
* **The other member no test could see**, and the one whose absence changed
|
|
73
|
+
* behaviour rather than a label: `reauthenticate` accepts either a current
|
|
74
|
+
* second factor or the account password before disabling 2FA, and with no
|
|
75
|
+
* verifier the password branch is unreachable, so the harness answered 400
|
|
76
|
+
* `MFA_REAUTH_REQUIRED` where production answers 401 or lets the disable
|
|
77
|
+
* through. Every `/disable` call in the tree passes a code, so nothing ever
|
|
78
|
+
* took the branch that differed.
|
|
79
|
+
*
|
|
80
|
+
* Only the boolean crosses: the hash stays behind each owner's port (T052).
|
|
81
|
+
*/
|
|
82
|
+
export function createAccountPasswordVerifier(customers, admins) {
|
|
83
|
+
return async (subjectType, subjectId, password) => subjectType === 'admin'
|
|
84
|
+
? admins.verifyPassword(subjectId, password)
|
|
85
|
+
: customers.verifyPassword(subjectId, password);
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=account-identity.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"account-identity.js","sourceRoot":"","sources":["../../../src/backend/services/account-identity.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AAOzD,OAAO,EAAE,SAAS,EAAE,MAAM,gCAAgC,CAAC;AAE3D;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH;;;;;;;;GAQG;AACH,MAAM,UAAU,+BAA+B,CAC7C,QAAmD;IAEnD,OAAO,KAAK,EAAE,iBAAiB,EAAE,EAAE;QACjC,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,QAAQ,CAAC,iBAAiB,CAAC,CAAC;QAC3D,IAAI,CAAC,OAAO,IAAI,OAAO,CAAC,IAAI,KAAK,oBAAoB,IAAI,CAAC,OAAO,CAAC,cAAc,EAAE,CAAC;YACjF,MAAM,IAAI,SAAS,CACjB,GAAG,EACH,WAAW,CAAC,SAAS,EACrB,2CAA2C,CAC5C,CAAC;QACJ,CAAC;QACD,OAAO,EAAE,cAAc,EAAE,OAAO,CAAC,cAAc,EAAE,KAAK,EAAE,OAAO,CAAC,EAAE,EAAE,CAAC;IACvE,CAAC,CAAC;AACJ,CAAC;AAED,8EAA8E;AAC9E,MAAM,UAAU,qCAAqC,CACnD,QAA6D;IAE7D,OAAO,KAAK,EAAE,cAAc,EAAE,EAAE,CAC9B,CAAC,MAAM,QAAQ,CAAC,kBAAkB,CAAC,cAAc,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;AACrF,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,0BAA0B,CACxC,SAAoD,EACpD,MAA2C;IAE3C,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,EAAE,CACtC,WAAW,KAAK,OAAO;QACrB,CAAC,CAAC,CAAC,CAAC,MAAM,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,EAAE,KAAK,IAAI,IAAI,CAAC;QACrD,CAAC,CAAC,CAAC,CAAC,MAAM,SAAS,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,EAAE,KAAK,IAAI,IAAI,CAAC,CAAC;AAC/D,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,6BAA6B,CAC3C,SAA2C,EAC3C,MAAqC;IAMrC,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,QAAQ,EAAE,EAAE,CAChD,WAAW,KAAK,OAAO;QACrB,CAAC,CAAC,MAAM,CAAC,cAAc,CAAC,SAAS,EAAE,QAAQ,CAAC;QAC5C,CAAC,CAAC,SAAS,CAAC,cAAc,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;AACtD,CAAC"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Redis } from 'ioredis';
|
|
2
|
+
export interface PendingChallenge {
|
|
3
|
+
subjectType: 'customer' | 'admin';
|
|
4
|
+
subjectId: string;
|
|
5
|
+
salesChannelId: string | null;
|
|
6
|
+
attemptsRemaining: number;
|
|
7
|
+
}
|
|
8
|
+
export interface SetupTicket {
|
|
9
|
+
subjectType: 'customer' | 'admin';
|
|
10
|
+
subjectId: string;
|
|
11
|
+
salesChannelId: string | null;
|
|
12
|
+
}
|
|
13
|
+
export interface OAuthTransaction {
|
|
14
|
+
surface: 'customer' | 'admin';
|
|
15
|
+
provider: 'google' | 'microsoft';
|
|
16
|
+
salesChannelId: string | null;
|
|
17
|
+
pkceVerifier: string;
|
|
18
|
+
nonce: string;
|
|
19
|
+
next: string;
|
|
20
|
+
}
|
|
21
|
+
export declare class ChallengeStore {
|
|
22
|
+
private readonly redis;
|
|
23
|
+
constructor(redis: Redis);
|
|
24
|
+
issueChallenge(input: Omit<PendingChallenge, 'attemptsRemaining'>, attemptBudget?: number): Promise<string>;
|
|
25
|
+
getChallenge(id: string): Promise<PendingChallenge | null>;
|
|
26
|
+
/**
|
|
27
|
+
* Record a failed attempt: decrement the budget and burn the challenge when
|
|
28
|
+
* it reaches zero. Returns the remaining budget (0 ⇒ burned).
|
|
29
|
+
*/
|
|
30
|
+
recordFailedAttempt(id: string): Promise<number>;
|
|
31
|
+
consumeChallenge(id: string): Promise<void>;
|
|
32
|
+
issueSetupTicket(input: SetupTicket): Promise<string>;
|
|
33
|
+
getSetupTicket(id: string): Promise<SetupTicket | null>;
|
|
34
|
+
consumeSetupTicket(id: string): Promise<void>;
|
|
35
|
+
/** The `state` value is returned to the provider and echoed on callback. */
|
|
36
|
+
issueOAuthTransaction(input: OAuthTransaction): Promise<string>;
|
|
37
|
+
getOAuthTransaction(state: string): Promise<OAuthTransaction | null>;
|
|
38
|
+
consumeOAuthTransaction(state: string): Promise<void>;
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=challenge-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"challenge-store.d.ts","sourceRoot":"","sources":["../../../src/backend/services/challenge-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAuBrC,MAAM,WAAW,gBAAgB;IAC/B,WAAW,EAAE,UAAU,GAAG,OAAO,CAAC;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,WAAW;IAC1B,WAAW,EAAE,UAAU,GAAG,OAAO,CAAC;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC;IAC9B,QAAQ,EAAE,QAAQ,GAAG,WAAW,CAAC;IACjC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;CACd;AAMD,qBAAa,cAAc;IACb,OAAO,CAAC,QAAQ,CAAC,KAAK;gBAAL,KAAK,EAAE,KAAK;IAInC,cAAc,CAClB,KAAK,EAAE,IAAI,CAAC,gBAAgB,EAAE,mBAAmB,CAAC,EAClD,aAAa,SAAyB,GACrC,OAAO,CAAC,MAAM,CAAC;IAeZ,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC;IAKhE;;;OAGG;IACG,mBAAmB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAkBhD,gBAAgB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAM3C,gBAAgB,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC;IAWrD,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC;IAKvD,kBAAkB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAMnD,4EAA4E;IACtE,qBAAqB,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,MAAM,CAAC;IAW/D,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC;IAKpE,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAG5D"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { randomBytes } from 'crypto';
|
|
2
|
+
/**
|
|
3
|
+
* Short-lived, server-held login state in Redis (feature 042, R4/R9).
|
|
4
|
+
*
|
|
5
|
+
* Three kinds of ephemeral token, all TTL-bounded so abandonment never
|
|
6
|
+
* materializes into a session:
|
|
7
|
+
* - pending-login challenge (`mfa:chal:<id>`) — after a correct password,
|
|
8
|
+
* before the second factor; carries an attempt budget;
|
|
9
|
+
* - setup ticket (`mfa:setup:<id>`) — enforced-but-unenrolled subject, may
|
|
10
|
+
* only call the enrolment endpoints until activation;
|
|
11
|
+
* - OAuth transaction (`mfa:oauth:<state>`) — PKCE verifier + nonce + next.
|
|
12
|
+
*/
|
|
13
|
+
const CHAL_PREFIX = 'mfa:chal:';
|
|
14
|
+
const SETUP_PREFIX = 'mfa:setup:';
|
|
15
|
+
const OAUTH_PREFIX = 'mfa:oauth:';
|
|
16
|
+
const CHALLENGE_TTL_SECONDS = 5 * 60;
|
|
17
|
+
const SETUP_TTL_SECONDS = 10 * 60;
|
|
18
|
+
const OAUTH_TTL_SECONDS = 10 * 60;
|
|
19
|
+
const DEFAULT_ATTEMPT_BUDGET = 5;
|
|
20
|
+
function newToken() {
|
|
21
|
+
return randomBytes(32).toString('base64url');
|
|
22
|
+
}
|
|
23
|
+
export class ChallengeStore {
|
|
24
|
+
redis;
|
|
25
|
+
constructor(redis) {
|
|
26
|
+
this.redis = redis;
|
|
27
|
+
}
|
|
28
|
+
// --- pending-login challenge ---------------------------------------------
|
|
29
|
+
async issueChallenge(input, attemptBudget = DEFAULT_ATTEMPT_BUDGET) {
|
|
30
|
+
const id = newToken();
|
|
31
|
+
const payload = {
|
|
32
|
+
...input,
|
|
33
|
+
attemptsRemaining: attemptBudget,
|
|
34
|
+
};
|
|
35
|
+
await this.redis.set(CHAL_PREFIX + id, JSON.stringify(payload), 'EX', CHALLENGE_TTL_SECONDS);
|
|
36
|
+
return id;
|
|
37
|
+
}
|
|
38
|
+
async getChallenge(id) {
|
|
39
|
+
const raw = await this.redis.get(CHAL_PREFIX + id);
|
|
40
|
+
return raw ? JSON.parse(raw) : null;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Record a failed attempt: decrement the budget and burn the challenge when
|
|
44
|
+
* it reaches zero. Returns the remaining budget (0 ⇒ burned).
|
|
45
|
+
*/
|
|
46
|
+
async recordFailedAttempt(id) {
|
|
47
|
+
const challenge = await this.getChallenge(id);
|
|
48
|
+
if (!challenge)
|
|
49
|
+
return 0;
|
|
50
|
+
const remaining = challenge.attemptsRemaining - 1;
|
|
51
|
+
if (remaining <= 0) {
|
|
52
|
+
await this.consumeChallenge(id);
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
const ttl = await this.redis.ttl(CHAL_PREFIX + id);
|
|
56
|
+
await this.redis.set(CHAL_PREFIX + id, JSON.stringify({ ...challenge, attemptsRemaining: remaining }), 'EX', ttl > 0 ? ttl : CHALLENGE_TTL_SECONDS);
|
|
57
|
+
return remaining;
|
|
58
|
+
}
|
|
59
|
+
async consumeChallenge(id) {
|
|
60
|
+
await this.redis.del(CHAL_PREFIX + id);
|
|
61
|
+
}
|
|
62
|
+
// --- setup ticket ---------------------------------------------------------
|
|
63
|
+
async issueSetupTicket(input) {
|
|
64
|
+
const id = newToken();
|
|
65
|
+
await this.redis.set(SETUP_PREFIX + id, JSON.stringify(input), 'EX', SETUP_TTL_SECONDS);
|
|
66
|
+
return id;
|
|
67
|
+
}
|
|
68
|
+
async getSetupTicket(id) {
|
|
69
|
+
const raw = await this.redis.get(SETUP_PREFIX + id);
|
|
70
|
+
return raw ? JSON.parse(raw) : null;
|
|
71
|
+
}
|
|
72
|
+
async consumeSetupTicket(id) {
|
|
73
|
+
await this.redis.del(SETUP_PREFIX + id);
|
|
74
|
+
}
|
|
75
|
+
// --- OAuth transaction ----------------------------------------------------
|
|
76
|
+
/** The `state` value is returned to the provider and echoed on callback. */
|
|
77
|
+
async issueOAuthTransaction(input) {
|
|
78
|
+
const state = newToken();
|
|
79
|
+
await this.redis.set(OAUTH_PREFIX + state, JSON.stringify(input), 'EX', OAUTH_TTL_SECONDS);
|
|
80
|
+
return state;
|
|
81
|
+
}
|
|
82
|
+
async getOAuthTransaction(state) {
|
|
83
|
+
const raw = await this.redis.get(OAUTH_PREFIX + state);
|
|
84
|
+
return raw ? JSON.parse(raw) : null;
|
|
85
|
+
}
|
|
86
|
+
async consumeOAuthTransaction(state) {
|
|
87
|
+
await this.redis.del(OAUTH_PREFIX + state);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=challenge-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"challenge-store.js","sourceRoot":"","sources":["../../../src/backend/services/challenge-store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql';
|
|
2
|
+
import type { MfaActiveEnrolmentCounts, MfaEnrolmentCountPort } from '@endora-commerce/contracts';
|
|
3
|
+
/**
|
|
4
|
+
* How many subjects hold an active second factor (owner ruling on D-96.5).
|
|
5
|
+
*
|
|
6
|
+
* The deactivation-confirmation dialog on `/platform/modules` renders one
|
|
7
|
+
* static sentence per dependent, from a manifest. This is the first **live**
|
|
8
|
+
* datum in it, and it is the most decision-relevant fact an operator can be
|
|
9
|
+
* shown before switching this module off: the `whenAbsent` sentence says what
|
|
10
|
+
* stops working, the count says to how many people.
|
|
11
|
+
*
|
|
12
|
+
* It lives here, behind a port, because `mfa_enrolments` is this module's
|
|
13
|
+
* table. The kernel does not name it and no other module queries it; the
|
|
14
|
+
* lifecycle surface asks a question and this module answers it.
|
|
15
|
+
*
|
|
16
|
+
* Nothing about the count is cached, denormalised or persisted outside this
|
|
17
|
+
* table, deliberately: a number that survived deactivation would be a second
|
|
18
|
+
* copy of the fact, and the only moment it is asked for is a moment at which
|
|
19
|
+
* the real one is readable.
|
|
20
|
+
*/
|
|
21
|
+
export declare class MfaEnrolmentCountService implements MfaEnrolmentCountPort {
|
|
22
|
+
private readonly emFactory;
|
|
23
|
+
constructor(emFactory: () => EntityManager);
|
|
24
|
+
countActiveEnrolments(): Promise<MfaActiveEnrolmentCounts>;
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=mfa-enrolment-count.service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mfa-enrolment-count.service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/mfa-enrolment-count.service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EAAE,wBAAwB,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AAGlG;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,wBAAyB,YAAW,qBAAqB;IACxD,OAAO,CAAC,QAAQ,CAAC,SAAS;gBAAT,SAAS,EAAE,MAAM,aAAa;IAErD,qBAAqB,IAAI,OAAO,CAAC,wBAAwB,CAAC;CAQjE"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { MfaEnrolment } from '../entities/mfa-enrolment.entity.js';
|
|
2
|
+
/**
|
|
3
|
+
* How many subjects hold an active second factor (owner ruling on D-96.5).
|
|
4
|
+
*
|
|
5
|
+
* The deactivation-confirmation dialog on `/platform/modules` renders one
|
|
6
|
+
* static sentence per dependent, from a manifest. This is the first **live**
|
|
7
|
+
* datum in it, and it is the most decision-relevant fact an operator can be
|
|
8
|
+
* shown before switching this module off: the `whenAbsent` sentence says what
|
|
9
|
+
* stops working, the count says to how many people.
|
|
10
|
+
*
|
|
11
|
+
* It lives here, behind a port, because `mfa_enrolments` is this module's
|
|
12
|
+
* table. The kernel does not name it and no other module queries it; the
|
|
13
|
+
* lifecycle surface asks a question and this module answers it.
|
|
14
|
+
*
|
|
15
|
+
* Nothing about the count is cached, denormalised or persisted outside this
|
|
16
|
+
* table, deliberately: a number that survived deactivation would be a second
|
|
17
|
+
* copy of the fact, and the only moment it is asked for is a moment at which
|
|
18
|
+
* the real one is readable.
|
|
19
|
+
*/
|
|
20
|
+
export class MfaEnrolmentCountService {
|
|
21
|
+
emFactory;
|
|
22
|
+
constructor(emFactory) {
|
|
23
|
+
this.emFactory = emFactory;
|
|
24
|
+
}
|
|
25
|
+
async countActiveEnrolments() {
|
|
26
|
+
const em = this.emFactory();
|
|
27
|
+
const [admins, customers] = await Promise.all([
|
|
28
|
+
em.count(MfaEnrolment, { status: 'active', subjectType: 'admin' }),
|
|
29
|
+
em.count(MfaEnrolment, { status: 'active', subjectType: 'customer' }),
|
|
30
|
+
]);
|
|
31
|
+
return { admins, customers };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=mfa-enrolment-count.service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mfa-enrolment-count.service.js","sourceRoot":"","sources":["../../../src/backend/services/mfa-enrolment-count.service.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,MAAM,qCAAqC,CAAC;AAEnE;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,OAAO,wBAAwB;IACN;IAA7B,YAA6B,SAA8B;QAA9B,cAAS,GAAT,SAAS,CAAqB;IAAG,CAAC;IAE/D,KAAK,CAAC,qBAAqB;QACzB,MAAM,EAAE,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;QAC5B,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;YAC5C,EAAE,CAAC,KAAK,CAAC,YAAY,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,WAAW,EAAE,OAAO,EAAE,CAAC;YAClE,EAAE,CAAC,KAAK,CAAC,YAAY,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,WAAW,EAAE,UAAU,EAAE,CAAC;SACtE,CAAC,CAAC;QACH,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;IAC/B,CAAC;CACF"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql';
|
|
2
|
+
import type { MfaSubjectRef } from '@endora-commerce/contracts';
|
|
3
|
+
import type { SecretCipher } from './secret-cipher.js';
|
|
4
|
+
import type { AuditPort } from '@endora-commerce/platform/kernel';
|
|
5
|
+
/**
|
|
6
|
+
* TOTP enrolment lifecycle (feature 042): setup → activate (issue recovery
|
|
7
|
+
* codes) → disable / regenerate, plus the second-factor verification used by
|
|
8
|
+
* the login flow. The TOTP secret is encrypted at rest via `SecretCipher`;
|
|
9
|
+
* recovery codes are stored only as SHA-256 hashes.
|
|
10
|
+
*/
|
|
11
|
+
export declare class MfaEnrolmentService {
|
|
12
|
+
#private;
|
|
13
|
+
private readonly emFactory;
|
|
14
|
+
private readonly cipher;
|
|
15
|
+
private readonly auditLog?;
|
|
16
|
+
constructor(emFactory: () => EntityManager, cipher: SecretCipher, auditLog?: AuditPort | undefined);
|
|
17
|
+
/** Begin (or restart) enrolment — creates a `pending` enrolment. */
|
|
18
|
+
setup(subject: MfaSubjectRef, accountLabel: string): Promise<{
|
|
19
|
+
secret: string;
|
|
20
|
+
otpauthUri: string;
|
|
21
|
+
}>;
|
|
22
|
+
/** Confirm the pending enrolment with a live code; issue recovery codes. */
|
|
23
|
+
activate(subject: MfaSubjectRef, code: string): Promise<{
|
|
24
|
+
recoveryCodes: string[];
|
|
25
|
+
}>;
|
|
26
|
+
/**
|
|
27
|
+
* Admin reset — clear ALL enrolments (active + pending) for a subject.
|
|
28
|
+
* Recovery codes cascade. Returns whether anything was removed (so a bulk
|
|
29
|
+
* caller can report affected vs skipped). FR-025.
|
|
30
|
+
*/
|
|
31
|
+
reset(subject: MfaSubjectRef): Promise<boolean>;
|
|
32
|
+
/** Disable 2FA for the subject (idempotent). Removes secret + recovery codes. */
|
|
33
|
+
disable(subject: MfaSubjectRef): Promise<void>;
|
|
34
|
+
/** Invalidate the old recovery-code set and issue a fresh one. */
|
|
35
|
+
regenerateRecoveryCodes(subject: MfaSubjectRef): Promise<{
|
|
36
|
+
recoveryCodes: string[];
|
|
37
|
+
}>;
|
|
38
|
+
status(subject: MfaSubjectRef): Promise<{
|
|
39
|
+
totpActive: boolean;
|
|
40
|
+
recoveryCodesRemaining: number;
|
|
41
|
+
}>;
|
|
42
|
+
/**
|
|
43
|
+
* Verify a second factor (TOTP or recovery code) for an active enrolment.
|
|
44
|
+
* A 6-digit input is treated as a TOTP code (with replay guard); anything
|
|
45
|
+
* else is matched against unused recovery codes (single-use). Reports which
|
|
46
|
+
* factor matched so callers can audit recovery-code use.
|
|
47
|
+
*/
|
|
48
|
+
verifySecondFactor(subject: MfaSubjectRef, code: string): Promise<{
|
|
49
|
+
ok: boolean;
|
|
50
|
+
factor?: 'totp' | 'recovery';
|
|
51
|
+
}>;
|
|
52
|
+
private decryptSecret;
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=mfa-enrolment-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mfa-enrolment-service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/mfa-enrolment-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAE3D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAGhE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kCAAkC,CAAC;AAOlE;;;;;GAKG;AACH,qBAAa,mBAAmB;;IAE5B,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC;gBAFT,SAAS,EAAE,MAAM,aAAa,EAC9B,MAAM,EAAE,YAAY,EACpB,QAAQ,CAAC,EAAE,SAAS,YAAA;IAevC,oEAAoE;IAC9D,KAAK,CACT,OAAO,EAAE,aAAa,EACtB,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC;IAkClD,4EAA4E;IACtE,QAAQ,CACZ,OAAO,EAAE,aAAa,EACtB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC;QAAE,aAAa,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IAiCvC;;;;OAIG;IACG,KAAK,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,OAAO,CAAC;IAarD,iFAAiF;IAC3E,OAAO,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAapD,kEAAkE;IAC5D,uBAAuB,CAC3B,OAAO,EAAE,aAAa,GACrB,OAAO,CAAC;QAAE,aAAa,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IAyBjC,MAAM,CACV,OAAO,EAAE,aAAa,GACrB,OAAO,CAAC;QAAE,UAAU,EAAE,OAAO,CAAC;QAAC,sBAAsB,EAAE,MAAM,CAAA;KAAE,CAAC;IAenE;;;;;OAKG;IACG,kBAAkB,CACtB,OAAO,EAAE,aAAa,EACtB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC;QAAE,EAAE,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,GAAG,UAAU,CAAA;KAAE,CAAC;IAqCzD,OAAO,CAAC,aAAa;CAOtB"}
|