@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,121 @@
|
|
|
1
|
+
import { ERROR_CODES } from '@endora-commerce/contracts';
|
|
2
|
+
import { HttpError } from '@endora-commerce/platform/http';
|
|
3
|
+
import { MfaSocialIdentity } from '../entities/mfa-social-identity.entity.js';
|
|
4
|
+
/**
|
|
5
|
+
* The account-holder's view of their federated identities, and the one write
|
|
6
|
+
* that removes one (issue #194).
|
|
7
|
+
*
|
|
8
|
+
* Separate from {@link SocialIdentityService}, which resolves an incoming
|
|
9
|
+
* provider identity to an account at sign-in and therefore needs the account
|
|
10
|
+
* resolvers and a configured provider. This surface needs neither: the links a
|
|
11
|
+
* subject already holds stay visible and severable after an operator switches a
|
|
12
|
+
* provider off, which is exactly when somebody wants to look at them.
|
|
13
|
+
*
|
|
14
|
+
* ## Why an unlink can be refused
|
|
15
|
+
*
|
|
16
|
+
* A customer auto-created by a federated sign-in is given a random password at
|
|
17
|
+
* signup (`customer_accounts/backend.ts`) — one nobody was ever told. So
|
|
18
|
+
* `password_hash is not null` is true for such an account and answers a
|
|
19
|
+
* different question from the one that matters here, which is whether the
|
|
20
|
+
* holder has a credential they can actually use.
|
|
21
|
+
*
|
|
22
|
+
* Issue #194 could not ask that question and therefore refused the **last**
|
|
23
|
+
* link of every account, absolutely. Issue #222 added the datum —
|
|
24
|
+
* `customer_accounts.password_set_at`, read here over
|
|
25
|
+
* {@link CustomerPasswordStatePort} — so the rule is now the narrower and
|
|
26
|
+
* truthful one: refuse the last link **only** while the account has no password
|
|
27
|
+
* on record. Where another link remains, or a password has been set, the
|
|
28
|
+
* removal goes through. An active TOTP enrolment still does not unblock it: a
|
|
29
|
+
* second factor is not a first one, and nobody signs in with TOTP alone.
|
|
30
|
+
*
|
|
31
|
+
* That is also what makes the sentence at the seam an instruction again. It
|
|
32
|
+
* told the holder to set a password first, then said plainly that there was no
|
|
33
|
+
* way out (!723), because with the rule counting links the instruction named a
|
|
34
|
+
* step that lifted nothing. It lifts it now, so the wording went back — in the
|
|
35
|
+
* thrown message and in `errors.MFA_SOCIAL_LAST_CREDENTIAL`, both languages.
|
|
36
|
+
*
|
|
37
|
+
* **The admin arm asks nobody.** An `AdminUser` cannot exist without a password
|
|
38
|
+
* somebody supplied: `AdminUserService.create` takes a required one (as does
|
|
39
|
+
* `createAdminUserRequestSchema`), and `SocialIdentityService.signInAdmin`
|
|
40
|
+
* matches an existing admin and refuses when there is none — there is no
|
|
41
|
+
* auto-create on that side to mint a random hash. So the answer is structurally
|
|
42
|
+
* `true` and a second column would record a constant.
|
|
43
|
+
* `test/unit/mfa/admin-password-is-structural.test.ts` is what keeps that
|
|
44
|
+
* premise from rotting quietly.
|
|
45
|
+
*/
|
|
46
|
+
export class SocialLinkService {
|
|
47
|
+
emFactory;
|
|
48
|
+
commandBus;
|
|
49
|
+
passwordState;
|
|
50
|
+
constructor(emFactory, commandBus, passwordState) {
|
|
51
|
+
this.emFactory = emFactory;
|
|
52
|
+
this.commandBus = commandBus;
|
|
53
|
+
this.passwordState = passwordState;
|
|
54
|
+
}
|
|
55
|
+
/** The links this subject holds, in link order, each with its unlink verdict. */
|
|
56
|
+
async list(subject) {
|
|
57
|
+
const rows = await this.emFactory().find(MfaSocialIdentity, { subjectType: subject.subjectType, subjectId: subject.subjectId }, { orderBy: { linkedAt: 'asc' } });
|
|
58
|
+
// Only the single-link case can be blocked, so only it asks — a status call
|
|
59
|
+
// for an account with two links, or none, has nothing to look up.
|
|
60
|
+
const blocked = rows.length === 1 && !(await this.#hasPasswordOnRecord(subject));
|
|
61
|
+
return rows.map((row) => ({
|
|
62
|
+
provider: row.provider,
|
|
63
|
+
email: row.email,
|
|
64
|
+
linkedAt: row.linkedAt.toISOString(),
|
|
65
|
+
canUnlink: !blocked,
|
|
66
|
+
unlinkBlockedReason: blocked ? 'last_credential' : null,
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Remove one provider link from this subject.
|
|
71
|
+
*
|
|
72
|
+
* Runs through the Command Bus (Constitution XIII): a security-relevant
|
|
73
|
+
* account change is audited co-transactionally, so a rollback takes the audit
|
|
74
|
+
* row with it.
|
|
75
|
+
*/
|
|
76
|
+
async unlink(subject, provider) {
|
|
77
|
+
await this.commandBus.run(this.#unlinkCommand(subject, provider));
|
|
78
|
+
}
|
|
79
|
+
#unlinkCommand(subject, provider) {
|
|
80
|
+
return {
|
|
81
|
+
action: 'mfa.social_unlink',
|
|
82
|
+
objectType: subject.subjectType === 'admin' ? 'admin_user' : 'customer_account',
|
|
83
|
+
objectId: subject.subjectId,
|
|
84
|
+
run: async ({ em }) => {
|
|
85
|
+
const where = { subjectType: subject.subjectType, subjectId: subject.subjectId };
|
|
86
|
+
const rows = await em.find(MfaSocialIdentity, where);
|
|
87
|
+
const target = rows.find((row) => row.provider === provider);
|
|
88
|
+
if (!target) {
|
|
89
|
+
throw new HttpError(404, ERROR_CODES.NOT_FOUND, `No ${provider} identity is linked to this account.`);
|
|
90
|
+
}
|
|
91
|
+
if (rows.length <= 1 && !(await this.#hasPasswordOnRecord(subject))) {
|
|
92
|
+
throw new HttpError(409, ERROR_CODES.MFA_SOCIAL_LAST_CREDENTIAL, 'This is the last sign-in identity linked to the account and it has no password on record: set a password for the account first, then it can be removed.');
|
|
93
|
+
}
|
|
94
|
+
const before = {
|
|
95
|
+
provider: target.provider,
|
|
96
|
+
email: target.email,
|
|
97
|
+
linkedAt: target.linkedAt.toISOString(),
|
|
98
|
+
};
|
|
99
|
+
em.remove(target);
|
|
100
|
+
return { result: undefined, before, after: null };
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Whether the subject has a password its holder can use.
|
|
106
|
+
*
|
|
107
|
+
* Deliberately not wrapped in a `catch`. `customer_accounts` declares itself
|
|
108
|
+
* non-deactivatable, so the gate on its port cannot be reached today — but a
|
|
109
|
+
* `catch` here would be the fail-open shape whatever the manifest says, and
|
|
110
|
+
* the wrong answer to this question removes somebody's last way into their
|
|
111
|
+
* account. A `ModuleDisabledError` must surface as 503, not as "no password
|
|
112
|
+
* on record" and not as "has one".
|
|
113
|
+
*/
|
|
114
|
+
async #hasPasswordOnRecord(subject) {
|
|
115
|
+
// Structurally true — see the class doc block.
|
|
116
|
+
if (subject.subjectType === 'admin')
|
|
117
|
+
return true;
|
|
118
|
+
return (await this.passwordState.passwordSetAt(subject.subjectId)) !== null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=social-link-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"social-link-service.js","sourceRoot":"","sources":["../../../src/backend/services/social-link-service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare function generateSecret(accountLabel: string): {
|
|
2
|
+
secret: string;
|
|
3
|
+
otpauthUri: string;
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* Verify a 6-digit code against a base32 secret. Returns the absolute TOTP
|
|
7
|
+
* time-step that matched (for the replay guard), or null if invalid. Accepts
|
|
8
|
+
* the ±1 step drift window.
|
|
9
|
+
*/
|
|
10
|
+
export declare function verifyTotpStep(secretBase32: string, code: string): number | null;
|
|
11
|
+
//# sourceMappingURL=totp.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"totp.d.ts","sourceRoot":"","sources":["../../../src/backend/services/totp.ts"],"names":[],"mappings":"AAaA,wBAAgB,cAAc,CAAC,YAAY,EAAE,MAAM,GAAG;IACpD,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;CACpB,CAWA;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,YAAY,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAchF"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { TOTP, Secret } from 'otpauth';
|
|
2
|
+
/**
|
|
3
|
+
* TOTP helpers (feature 042) using the already-present `otpauth` dependency.
|
|
4
|
+
* Same parameters as the platform's existing TOTP primitive
|
|
5
|
+
* for authenticator-app compatibility (the `auth` re-export shim it used to
|
|
6
|
+
* name was deleted once it had no importers).
|
|
7
|
+
*/
|
|
8
|
+
const ISSUER = 'B2B Platform';
|
|
9
|
+
const PERIOD = 30;
|
|
10
|
+
const DIGITS = 6;
|
|
11
|
+
const ALGORITHM = 'SHA1';
|
|
12
|
+
export function generateSecret(accountLabel) {
|
|
13
|
+
const secret = new Secret({ size: 20 });
|
|
14
|
+
const totp = new TOTP({
|
|
15
|
+
issuer: ISSUER,
|
|
16
|
+
label: accountLabel,
|
|
17
|
+
algorithm: ALGORITHM,
|
|
18
|
+
digits: DIGITS,
|
|
19
|
+
period: PERIOD,
|
|
20
|
+
secret,
|
|
21
|
+
});
|
|
22
|
+
return { secret: secret.base32, otpauthUri: totp.toString() };
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Verify a 6-digit code against a base32 secret. Returns the absolute TOTP
|
|
26
|
+
* time-step that matched (for the replay guard), or null if invalid. Accepts
|
|
27
|
+
* the ±1 step drift window.
|
|
28
|
+
*/
|
|
29
|
+
export function verifyTotpStep(secretBase32, code) {
|
|
30
|
+
const normalised = code.replace(/\s+/g, '');
|
|
31
|
+
if (!/^\d{6}$/.test(normalised))
|
|
32
|
+
return null;
|
|
33
|
+
const totp = new TOTP({
|
|
34
|
+
issuer: ISSUER,
|
|
35
|
+
label: 'verify',
|
|
36
|
+
algorithm: ALGORITHM,
|
|
37
|
+
digits: DIGITS,
|
|
38
|
+
period: PERIOD,
|
|
39
|
+
secret: Secret.fromBase32(secretBase32),
|
|
40
|
+
});
|
|
41
|
+
const delta = totp.validate({ token: normalised, window: 1 });
|
|
42
|
+
if (delta === null)
|
|
43
|
+
return null;
|
|
44
|
+
return Math.floor(Date.now() / 1000 / PERIOD) + delta;
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=totp.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"totp.js","sourceRoot":"","sources":["../../../src/backend/services/totp.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAEvC;;;;;GAKG;AACH,MAAM,MAAM,GAAG,cAAc,CAAC;AAC9B,MAAM,MAAM,GAAG,EAAE,CAAC;AAClB,MAAM,MAAM,GAAG,CAAC,CAAC;AACjB,MAAM,SAAS,GAAG,MAAe,CAAC;AAElC,MAAM,UAAU,cAAc,CAAC,YAAoB;IAIjD,MAAM,MAAM,GAAG,IAAI,MAAM,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC;IACxC,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC;QACpB,MAAM,EAAE,MAAM;QACd,KAAK,EAAE,YAAY;QACnB,SAAS,EAAE,SAAS;QACpB,MAAM,EAAE,MAAM;QACd,MAAM,EAAE,MAAM;QACd,MAAM;KACP,CAAC,CAAC;IACH,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,CAAC,QAAQ,EAAE,EAAE,CAAC;AAChE,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,YAAoB,EAAE,IAAY;IAC/D,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IAC5C,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,UAAU,CAAC;QAAE,OAAO,IAAI,CAAC;IAC7C,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC;QACpB,MAAM,EAAE,MAAM;QACd,KAAK,EAAE,QAAQ;QACf,SAAS,EAAE,SAAS;QACpB,MAAM,EAAE,MAAM;QACd,MAAM,EAAE,MAAM;QACd,MAAM,EAAE,MAAM,CAAC,UAAU,CAAC,YAAY,CAAC;KACxC,CAAC,CAAC;IACH,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC;IAC9D,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAChC,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,GAAG,MAAM,CAAC,GAAG,KAAK,CAAC;AACxD,CAAC"}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/** Exposed for the settings ManifestReconciler (boot + test harness). */
|
|
2
|
+
export declare const mfaSettingsManifest: {
|
|
3
|
+
moduleCode: string;
|
|
4
|
+
groups: {
|
|
5
|
+
code: string;
|
|
6
|
+
name: string;
|
|
7
|
+
salesChannelCodes?: string[] | undefined;
|
|
8
|
+
isSystemProtected?: boolean | undefined;
|
|
9
|
+
}[];
|
|
10
|
+
settings: {
|
|
11
|
+
code: string;
|
|
12
|
+
name: string;
|
|
13
|
+
valueType: "string" | "number" | "boolean" | "json" | "string_list" | "secret" | "credential_ref";
|
|
14
|
+
defaultValue: unknown;
|
|
15
|
+
description?: string | undefined;
|
|
16
|
+
groupCode?: string | undefined;
|
|
17
|
+
previousDefaultValues?: unknown[] | undefined;
|
|
18
|
+
salesChannelCodes?: string[] | undefined;
|
|
19
|
+
enumOptions?: string[] | undefined;
|
|
20
|
+
configurationType?: string | undefined;
|
|
21
|
+
hidden?: boolean | undefined;
|
|
22
|
+
}[];
|
|
23
|
+
};
|
|
24
|
+
export declare const manifest: {
|
|
25
|
+
id: string;
|
|
26
|
+
name: string;
|
|
27
|
+
version: string;
|
|
28
|
+
dependencies: string[];
|
|
29
|
+
description?: string | undefined;
|
|
30
|
+
acknowledgedDependencies?: {
|
|
31
|
+
moduleId: string;
|
|
32
|
+
port: string;
|
|
33
|
+
reason: string;
|
|
34
|
+
}[] | undefined;
|
|
35
|
+
nonBindingDependencies?: {
|
|
36
|
+
moduleId: string;
|
|
37
|
+
name: string;
|
|
38
|
+
kind: "contributes-to" | "degrades-without" | "refuses-without";
|
|
39
|
+
reason: string;
|
|
40
|
+
whenAbsent?: string | undefined;
|
|
41
|
+
}[] | undefined;
|
|
42
|
+
activation?: {
|
|
43
|
+
settingCode: string;
|
|
44
|
+
default: boolean;
|
|
45
|
+
} | {
|
|
46
|
+
nonDeactivatable: true;
|
|
47
|
+
reason: string;
|
|
48
|
+
} | undefined;
|
|
49
|
+
settings?: {
|
|
50
|
+
moduleCode: string;
|
|
51
|
+
groups: {
|
|
52
|
+
code: string;
|
|
53
|
+
name: string;
|
|
54
|
+
salesChannelCodes?: string[] | undefined;
|
|
55
|
+
isSystemProtected?: boolean | undefined;
|
|
56
|
+
}[];
|
|
57
|
+
settings: {
|
|
58
|
+
code: string;
|
|
59
|
+
name: string;
|
|
60
|
+
valueType: "string" | "number" | "boolean" | "json" | "string_list" | "secret" | "credential_ref";
|
|
61
|
+
defaultValue: unknown;
|
|
62
|
+
description?: string | undefined;
|
|
63
|
+
groupCode?: string | undefined;
|
|
64
|
+
previousDefaultValues?: unknown[] | undefined;
|
|
65
|
+
salesChannelCodes?: string[] | undefined;
|
|
66
|
+
enumOptions?: string[] | undefined;
|
|
67
|
+
configurationType?: string | undefined;
|
|
68
|
+
hidden?: boolean | undefined;
|
|
69
|
+
}[];
|
|
70
|
+
} | undefined;
|
|
71
|
+
i18n?: {
|
|
72
|
+
bundlesDir: string;
|
|
73
|
+
} | undefined;
|
|
74
|
+
docs?: false | {
|
|
75
|
+
dir: string;
|
|
76
|
+
} | undefined;
|
|
77
|
+
demo?: false | {
|
|
78
|
+
summary: string;
|
|
79
|
+
seed: (context: import("@endora-commerce/contracts").ModuleDemoContext<never>) => Promise<import("@endora-commerce/contracts").DemoSeedResult>;
|
|
80
|
+
reset: (context: import("@endora-commerce/contracts").ModuleDemoContext<never>) => Promise<import("@endora-commerce/contracts").DemoResetResult>;
|
|
81
|
+
after?: readonly string[] | undefined;
|
|
82
|
+
package?: string | undefined;
|
|
83
|
+
} | undefined;
|
|
84
|
+
actions?: {
|
|
85
|
+
id: string;
|
|
86
|
+
labelKey: string;
|
|
87
|
+
icon: "Plus" | "Sparkles" | "Settings" | "Search" | "Boxes" | "Layers" | "Menu" | "PlusCircle" | "PlusSquare" | "FilePlus" | "FolderPlus" | "Upload" | "FileUp" | "CloudUpload" | "Download" | "FileDown" | "FileText" | "BookOpen" | "Rss" | "Package" | "Tag" | "ShoppingCart" | "Receipt" | "CreditCard" | "Users" | "UserPlus" | "Inbox" | "ListChecks" | "ClipboardList" | "Image" | "Video" | "LayoutDashboard" | "PanelLeft" | "KeyRound" | "ShieldCheck" | "Edit" | "Archive" | "Box" | "Truck" | "CircleDollarSign" | "Activity" | "LineChart" | "Smartphone" | "Webhook" | "Scale" | "PlugZap" | "PercentDiamond" | "Newspaper" | "Languages" | "Eraser" | "Warehouse" | "TrendingDown" | "Bell" | "PackageOpen" | "Building2" | "Store" | "ClipboardCheck";
|
|
88
|
+
targetRoute: string;
|
|
89
|
+
keywords: string[];
|
|
90
|
+
weight: number;
|
|
91
|
+
descriptionKey?: string | undefined;
|
|
92
|
+
requiredPermission?: string | undefined;
|
|
93
|
+
}[] | undefined;
|
|
94
|
+
permissions?: {
|
|
95
|
+
code: string;
|
|
96
|
+
label: string;
|
|
97
|
+
module?: string | undefined;
|
|
98
|
+
description?: string | undefined;
|
|
99
|
+
requires?: string[] | undefined;
|
|
100
|
+
}[] | undefined;
|
|
101
|
+
transactionalEmails?: {
|
|
102
|
+
code: string;
|
|
103
|
+
name: string;
|
|
104
|
+
variables: {
|
|
105
|
+
key: string;
|
|
106
|
+
label: string;
|
|
107
|
+
sampleValue?: string | undefined;
|
|
108
|
+
description?: string | undefined;
|
|
109
|
+
}[];
|
|
110
|
+
description?: string | undefined;
|
|
111
|
+
group?: string | undefined;
|
|
112
|
+
}[] | undefined;
|
|
113
|
+
capabilities?: string[] | undefined;
|
|
114
|
+
exclusiveCapabilities?: {
|
|
115
|
+
key: string;
|
|
116
|
+
errorCode: string;
|
|
117
|
+
}[] | undefined;
|
|
118
|
+
errorCodes?: {
|
|
119
|
+
code: string;
|
|
120
|
+
tokens?: string[] | undefined;
|
|
121
|
+
}[] | undefined;
|
|
122
|
+
blocks?: {
|
|
123
|
+
name: string;
|
|
124
|
+
labelKey: string;
|
|
125
|
+
category: string;
|
|
126
|
+
contexts: ("email" | "invoice" | "cms" | "newsletter")[];
|
|
127
|
+
fields: Record<string, {
|
|
128
|
+
type: "number" | "object" | "array" | "text" | "textarea" | "select" | "radio" | "external" | "uuid" | "richtext";
|
|
129
|
+
label?: string | undefined;
|
|
130
|
+
required?: boolean | undefined;
|
|
131
|
+
options?: {
|
|
132
|
+
label: string;
|
|
133
|
+
value: string | number;
|
|
134
|
+
}[] | undefined;
|
|
135
|
+
refKind?: string | undefined;
|
|
136
|
+
}>;
|
|
137
|
+
descriptionKey?: string | undefined;
|
|
138
|
+
defaultProps?: Record<string, unknown> | undefined;
|
|
139
|
+
responsiveFields?: string[] | undefined;
|
|
140
|
+
previewIcon?: string | undefined;
|
|
141
|
+
weight?: number | undefined;
|
|
142
|
+
}[] | undefined;
|
|
143
|
+
blockCategories?: {
|
|
144
|
+
key: string;
|
|
145
|
+
titleKey: string;
|
|
146
|
+
contexts: ("email" | "invoice" | "cms" | "newsletter")[];
|
|
147
|
+
weight?: number | undefined;
|
|
148
|
+
visible?: boolean | undefined;
|
|
149
|
+
}[] | undefined;
|
|
150
|
+
env?: {
|
|
151
|
+
name: string;
|
|
152
|
+
describes: {
|
|
153
|
+
en: string;
|
|
154
|
+
pl: string;
|
|
155
|
+
};
|
|
156
|
+
requirement: {
|
|
157
|
+
kind: "required";
|
|
158
|
+
} | {
|
|
159
|
+
kind: "requiredWhen";
|
|
160
|
+
input: string;
|
|
161
|
+
equals: string;
|
|
162
|
+
} | {
|
|
163
|
+
kind: "optional";
|
|
164
|
+
without: {
|
|
165
|
+
en: string;
|
|
166
|
+
pl: string;
|
|
167
|
+
};
|
|
168
|
+
};
|
|
169
|
+
secret: boolean;
|
|
170
|
+
generable: boolean;
|
|
171
|
+
owner: {
|
|
172
|
+
kind: "platform";
|
|
173
|
+
} | {
|
|
174
|
+
kind: "application";
|
|
175
|
+
application: "admin" | "backend" | "storefront";
|
|
176
|
+
} | {
|
|
177
|
+
kind: "module";
|
|
178
|
+
moduleId: string;
|
|
179
|
+
};
|
|
180
|
+
consumers: ("admin" | "backend" | "storefront")[];
|
|
181
|
+
addressOf: "admin" | "backend" | "storefront" | null;
|
|
182
|
+
}[] | undefined;
|
|
183
|
+
};
|
|
184
|
+
//# sourceMappingURL=manifest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AA2GA,yEAAyE;AACzE,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;CAAW,CAAC;AAE5C,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsKnB,CAAC"}
|
package/dist/manifest.js
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { defineModuleManifest, defineModuleSettingsManifest, MFA_SETTING_CODES, } from '@endora-commerce/contracts';
|
|
2
|
+
/**
|
|
3
|
+
* MFA module — feature 042.
|
|
4
|
+
*
|
|
5
|
+
* Owns two-factor authentication (TOTP + recovery codes) and federated
|
|
6
|
+
* sign-in (Google / Microsoft) across the Storefront and Admin UI, plus the
|
|
7
|
+
* per-surface / per-channel / per-organization enablement & enforcement
|
|
8
|
+
* policy and Platform-Admin 2FA reset.
|
|
9
|
+
*
|
|
10
|
+
* `mfa` is a singular acronym module name — a documented Principle VI
|
|
11
|
+
* carve-out (see specs/042-mfa-authentication/plan.md Complexity Tracking).
|
|
12
|
+
*
|
|
13
|
+
* Provider client id/secret are supplied via backend config/env, NOT settings.
|
|
14
|
+
*/
|
|
15
|
+
const settings = defineModuleSettingsManifest({
|
|
16
|
+
moduleCode: 'mfa',
|
|
17
|
+
groups: [{ code: 'mfa', name: 'Security / MFA' }],
|
|
18
|
+
settings: [
|
|
19
|
+
{
|
|
20
|
+
// Feature 074 — the operator's activation control. Platform-wide, and
|
|
21
|
+
// never channel-scoped even though four of the policy switches below are:
|
|
22
|
+
// activation stops at `global_value` → `default_value` by construction.
|
|
23
|
+
code: MFA_SETTING_CODES.ACTIVATION,
|
|
24
|
+
name: 'MFA enabled',
|
|
25
|
+
description: 'Switches the whole module on or off: two-factor enrolment and challenges, the federated sign-in buttons, the per-organization enforcement policy and the admin reset screen. Nothing is dropped — enrolled secrets, recovery codes and every policy value stay in the database and apply again exactly as before when you switch it back on.',
|
|
26
|
+
groupCode: 'mfa',
|
|
27
|
+
valueType: 'boolean',
|
|
28
|
+
defaultValue: true,
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
code: MFA_SETTING_CODES.ADMIN_TOTP_ENABLED,
|
|
32
|
+
name: 'Admin UI — allow 2FA',
|
|
33
|
+
description: 'When enabled, admin users may set up TOTP two-factor authentication.',
|
|
34
|
+
groupCode: 'mfa',
|
|
35
|
+
valueType: 'boolean',
|
|
36
|
+
defaultValue: false,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
code: MFA_SETTING_CODES.ADMIN_TOTP_ENFORCED,
|
|
40
|
+
name: 'Admin UI — enforce 2FA',
|
|
41
|
+
description: 'When enabled, admin users must complete 2FA setup before accessing the Admin UI.',
|
|
42
|
+
groupCode: 'mfa',
|
|
43
|
+
valueType: 'boolean',
|
|
44
|
+
defaultValue: false,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
code: MFA_SETTING_CODES.STOREFRONT_TOTP_ENABLED,
|
|
48
|
+
name: 'Storefront — allow 2FA',
|
|
49
|
+
description: 'When enabled, customers may set up TOTP two-factor authentication. Can be overridden per sales channel.',
|
|
50
|
+
groupCode: 'mfa',
|
|
51
|
+
valueType: 'boolean',
|
|
52
|
+
defaultValue: false,
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
code: MFA_SETTING_CODES.STOREFRONT_TOTP_ENFORCED,
|
|
56
|
+
name: 'Storefront — enforce 2FA',
|
|
57
|
+
description: 'When enabled, customers must complete 2FA setup before using the storefront. Can be overridden per sales channel.',
|
|
58
|
+
groupCode: 'mfa',
|
|
59
|
+
valueType: 'boolean',
|
|
60
|
+
defaultValue: false,
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
code: MFA_SETTING_CODES.ADMIN_GOOGLE_ENABLED,
|
|
64
|
+
name: 'Admin UI — Google sign-in',
|
|
65
|
+
description: 'Allow admin users to sign in with Google (existing admin users only).',
|
|
66
|
+
groupCode: 'mfa',
|
|
67
|
+
valueType: 'boolean',
|
|
68
|
+
defaultValue: false,
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
code: MFA_SETTING_CODES.ADMIN_MICROSOFT_ENABLED,
|
|
72
|
+
name: 'Admin UI — Microsoft sign-in',
|
|
73
|
+
description: 'Allow admin users to sign in with Microsoft (existing admin users only).',
|
|
74
|
+
groupCode: 'mfa',
|
|
75
|
+
valueType: 'boolean',
|
|
76
|
+
defaultValue: false,
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
code: MFA_SETTING_CODES.STOREFRONT_GOOGLE_ENABLED,
|
|
80
|
+
name: 'Storefront — Google sign-in',
|
|
81
|
+
description: 'Allow customers to sign in with Google. Can be overridden per sales channel.',
|
|
82
|
+
groupCode: 'mfa',
|
|
83
|
+
valueType: 'boolean',
|
|
84
|
+
defaultValue: false,
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
code: MFA_SETTING_CODES.STOREFRONT_MICROSOFT_ENABLED,
|
|
88
|
+
name: 'Storefront — Microsoft sign-in',
|
|
89
|
+
description: 'Allow customers to sign in with Microsoft. Can be overridden per sales channel.',
|
|
90
|
+
groupCode: 'mfa',
|
|
91
|
+
valueType: 'boolean',
|
|
92
|
+
defaultValue: false,
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
});
|
|
96
|
+
/** Exposed for the settings ManifestReconciler (boot + test harness). */
|
|
97
|
+
export const mfaSettingsManifest = settings;
|
|
98
|
+
export const manifest = defineModuleManifest({
|
|
99
|
+
id: 'mfa',
|
|
100
|
+
name: 'MFA',
|
|
101
|
+
description: 'Two-factor authentication (TOTP + recovery codes) and Google/Microsoft sign-in, with per-scope enablement/enforcement and admin reset.',
|
|
102
|
+
version: '1.0.0',
|
|
103
|
+
/**
|
|
104
|
+
* What this module needs from the environment (`specs/117-instance-bring-up/`
|
|
105
|
+
* FR-002). Only what it **owns**: its reads of platform-owned names are
|
|
106
|
+
* satisfied by `packages/platform/src/env/index.ts`.
|
|
107
|
+
*
|
|
108
|
+
* Why each of these is not a Setting is its entry in
|
|
109
|
+
* `backend/scripts/ledgers/module-environment-inputs/mfa.ts`.
|
|
110
|
+
*/
|
|
111
|
+
env: [
|
|
112
|
+
{
|
|
113
|
+
name: 'ADMIN_BASE_URL',
|
|
114
|
+
describes: {
|
|
115
|
+
en: 'Where the admin panel is, so that a link this instance mails an administrator opens their own installation.',
|
|
116
|
+
pl: 'Gdzie znajduje się panel administracyjny, aby link wysłany administratorowi otwierał jego własną instalację.',
|
|
117
|
+
},
|
|
118
|
+
requirement: {
|
|
119
|
+
kind: 'optional',
|
|
120
|
+
without: {
|
|
121
|
+
en: 'A sign-in or recovery link mailed to an administrator points nowhere they can open — which matters most when the panel is on a host of its own.',
|
|
122
|
+
pl: 'Link logowania lub odzyskiwania wysłany administratorowi prowadzi donikąd — co ma największe znaczenie, gdy panel stoi na osobnym hoście.',
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
secret: false,
|
|
126
|
+
generable: false,
|
|
127
|
+
owner: { kind: 'module', moduleId: 'mfa' },
|
|
128
|
+
consumers: ['backend'],
|
|
129
|
+
addressOf: 'admin',
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
name: 'MFA_SECRET_ENCRYPTION_KEY',
|
|
133
|
+
describes: {
|
|
134
|
+
en: 'The key that encrypts every stored second-factor secret, so a copy of the database is not a copy of everybody’s authenticator.',
|
|
135
|
+
pl: 'Klucz szyfrujący każdy zapisany sekret drugiego składnika, aby kopia bazy danych nie była kopią czyjegoś uwierzytelniacza.',
|
|
136
|
+
},
|
|
137
|
+
requirement: {
|
|
138
|
+
kind: 'optional',
|
|
139
|
+
without: {
|
|
140
|
+
en: 'No second factor can be enrolled: the module serves its screens and refuses every enrolment, because it will not store a secret it cannot encrypt.',
|
|
141
|
+
pl: 'Nie da się zarejestrować drugiego składnika: moduł udostępnia swoje ekrany i odmawia każdej rejestracji, bo nie zapisze sekretu, którego nie potrafi zaszyfrować.',
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
secret: true,
|
|
145
|
+
generable: true,
|
|
146
|
+
owner: { kind: 'module', moduleId: 'mfa' },
|
|
147
|
+
consumers: ['backend'],
|
|
148
|
+
addressOf: null,
|
|
149
|
+
},
|
|
150
|
+
],
|
|
151
|
+
// `auth` owns `authSessionPort`, `requireAdmin` and the customer guard this
|
|
152
|
+
// module resolves; feature 072 made those container resolutions rather than
|
|
153
|
+
// constructor arguments, so the edge is real now and has to be declared.
|
|
154
|
+
// Feature 075 Phase C turned the session edge into the published port: the
|
|
155
|
+
// second factor mints its session over `authSessionPort`, so no `Session`
|
|
156
|
+
// entity and no `SessionService` class crosses into this module any more.
|
|
157
|
+
dependencies: ['admin_users', 'customer_accounts', 'organizations', 'settings', 'auth'],
|
|
158
|
+
settings,
|
|
159
|
+
// Feature 074 (Constitution XVII) — a control this module never had, so an
|
|
160
|
+
// operator could not decline it at all. Two-factor authentication is a client
|
|
161
|
+
// *security policy*, not a platform floor: a deployment behind a corporate
|
|
162
|
+
// SSO gateway or a VPN may run without it. The eight policy switches above
|
|
163
|
+
// stay where they are — they choose which factor a surface offers, which is a
|
|
164
|
+
// different question from whether the module is present. Default `true` so
|
|
165
|
+
// that merging this changes no deployment's state (FR-012).
|
|
166
|
+
activation: { settingCode: MFA_SETTING_CODES.ACTIVATION, default: true },
|
|
167
|
+
/**
|
|
168
|
+
* The operator-visible error codes this module owns (feature 090, D-182,
|
|
169
|
+
* `specs/090-module-owned-error-codes/`).
|
|
170
|
+
*
|
|
171
|
+
* The first ten entries are answer-preserving, not a judgement (§6.2 and
|
|
172
|
+
* §6.5), and were not written by hand: they are the verbatim output of the
|
|
173
|
+
* runbook's step-1 derivation over the frozen capture at
|
|
174
|
+
* `backend/test/fixtures/error-code-routing/chain-answers.ts`, which records
|
|
175
|
+
* what the prefix chain in `@endora-commerce/mod-i18n` answered at
|
|
176
|
+
* `49f3c6817`. Re-routing a code to a better owner was
|
|
177
|
+
* `specs/082-error-code-ownership/rulings.md` §9's remaining work, deliberately
|
|
178
|
+
* not done in Phase 3; the last two entries are that sweep arriving.
|
|
179
|
+
*
|
|
180
|
+
* **No shadow reaches this module, and that is a conclusion rather than a
|
|
181
|
+
* premise.** The chain is an ordered `if` and an earlier rule silently claims
|
|
182
|
+
* a later one's codes — it cost `inventory` two and gave `catalog` four
|
|
183
|
+
* (trap T1). Here the whole chain was read: the `MFA_` rule is second from
|
|
184
|
+
* last, no rule above it names an `MFA_`-prefixed code, and no misc set holds
|
|
185
|
+
* one, so the source reading and the answer reading coincide. The derivation
|
|
186
|
+
* was still run from the answer.
|
|
187
|
+
*
|
|
188
|
+
* **`TWO_FACTOR_REQUIRED` and `TWO_FACTOR_REQUIRED_BY_ROLE` were the codes a
|
|
189
|
+
* reader would look for here and not find** (trap T2). Both name this
|
|
190
|
+
* module's subject and neither carries the prefix, so they fell off the end
|
|
191
|
+
* of the chain to `core`, which is what the capture records; whether that was
|
|
192
|
+
* the right owner is the question §6.5 put out of scope. D-129's remaining
|
|
193
|
+
* sweep answers it — **both are declared below since MR 4** — and the entry
|
|
194
|
+
* after this list is the argument.
|
|
195
|
+
*
|
|
196
|
+
* **All ten of those are raised, and the spelling is why that had to be
|
|
197
|
+
* measured.**
|
|
198
|
+
* Nine of them are thrown as a bare string literal in the second argument of
|
|
199
|
+
* `new HttpError` — `throw new HttpError(409, 'MFA_ALREADY_ENROLLED', …)` —
|
|
200
|
+
* and only `MFA_SOCIAL_LAST_CREDENTIAL` is thrown as `ERROR_CODES.<CODE>`.
|
|
201
|
+
* Fourteen raise sites over `routes.public.ts`, `routes.self-service.ts`,
|
|
202
|
+
* `services/mfa-enrolment-service.ts` and `services/social-link-service.ts`,
|
|
203
|
+
* every code covered by at least one. A scan keyed on the `ERROR_CODES.`
|
|
204
|
+
* spelling sees one of the fourteen.
|
|
205
|
+
*
|
|
206
|
+
* No `tokens`, and it is derived rather than assumed. `refusalToken`
|
|
207
|
+
* (`packages/platform/src/http/error-envelope.ts`) reads `details.code` and
|
|
208
|
+
* nothing else, and **not one of the fourteen sites passes a fourth argument
|
|
209
|
+
* at all**, so there is nothing for it to read. The runbook's §5 raise-site
|
|
210
|
+
* scan agrees from the whole tree — it attributes the ten token-carrying
|
|
211
|
+
* codes to `core`, `invoices` and `carts` and names none of these — and so
|
|
212
|
+
* does the bundle from the other direction: ten `errors.<CODE>` keys in `en`
|
|
213
|
+
* and ten in `pl`, exactly these ten, and no `errors.<CODE>.<token>` key.
|
|
214
|
+
* There is no dead sentence in either direction and none of the ten is an
|
|
215
|
+
* `UNTRANSLATED_ERROR_CODES` entry.
|
|
216
|
+
*
|
|
217
|
+
* **`TWO_FACTOR_REQUIRED` and `TWO_FACTOR_REQUIRED_BY_ROLE` join them in
|
|
218
|
+
* D-129's remaining sweep, MR 4** (`d129-sweep.md` §5.2, Appendix A; D-186 in
|
|
219
|
+
* `specs/080-f4-real-scope/rulings.md`). D-121 T1 decides both: the noun is
|
|
220
|
+
* the second factor, which is this module's whole subject, and `_BY_ROLE`
|
|
221
|
+
* names `MfaOrganizationPolicy` — the per-role enforcement this module owns
|
|
222
|
+
* outright. Neither was a judgement about who throws them, because **nothing
|
|
223
|
+
* in the tree throws either**: they are members of `ERROR_CODES` that no raise
|
|
224
|
+
* site produces, which is the sweep's class D.
|
|
225
|
+
*
|
|
226
|
+
* **That is also why they arrive without a sentence, and why writing one
|
|
227
|
+
* would have been the wrong call.** Both carried a placeholder in `_i18n`'s
|
|
228
|
+
* bundle — `"Two Factor Required."` / `"Błąd: two factor required."`, the code
|
|
229
|
+
* rewritten twice — and D-186 §2 deletes a placeholder rather than moving it,
|
|
230
|
+
* because in this module's own bundle it would read as this module's answer
|
|
231
|
+
* and every instrument would count the code as translated for good.
|
|
232
|
+
* `d129-sweep.md` §5.4 keeps writing real prose available as the better
|
|
233
|
+
* outcome, and it is available whenever a raise site says what the refusal
|
|
234
|
+
* means. Here there is no raise site: a sentence would have to be invented
|
|
235
|
+
* from the code's own name, which is the placeholder again in longer words,
|
|
236
|
+
* and it would render for nobody. So both are `UNTRANSLATED_ERROR_CODES`
|
|
237
|
+
* entries under `mfa`, where the debt is findable and attached to the module
|
|
238
|
+
* that will write it if the refusal is ever implemented.
|
|
239
|
+
*
|
|
240
|
+
* They carry no `tokens` for the same reason the ten above do not, arrived at
|
|
241
|
+
* from the other end: there is no raise site to put a `details.code` on the
|
|
242
|
+
* wire.
|
|
243
|
+
*/
|
|
244
|
+
errorCodes: [
|
|
245
|
+
{ code: 'MFA_ALREADY_ENROLLED' },
|
|
246
|
+
{ code: 'MFA_INVALID_CHALLENGE' },
|
|
247
|
+
{ code: 'MFA_INVALID_CODE' },
|
|
248
|
+
{ code: 'MFA_NOT_ENABLED' },
|
|
249
|
+
{ code: 'MFA_NO_ACTIVE_ENROLMENT' },
|
|
250
|
+
{ code: 'MFA_NO_PENDING_ENROLMENT' },
|
|
251
|
+
{ code: 'MFA_REAUTH_REQUIRED' },
|
|
252
|
+
{ code: 'MFA_SOCIAL_LAST_CREDENTIAL' },
|
|
253
|
+
{ code: 'MFA_TOO_MANY_ATTEMPTS' },
|
|
254
|
+
{ code: 'MFA_WRONG_SURFACE' },
|
|
255
|
+
{ code: 'TWO_FACTOR_REQUIRED' },
|
|
256
|
+
{ code: 'TWO_FACTOR_REQUIRED_BY_ROLE' },
|
|
257
|
+
],
|
|
258
|
+
i18n: { bundlesDir: 'i18n' },
|
|
259
|
+
permissions: [
|
|
260
|
+
{ code: 'mfa:reset', label: "Reset a user's 2FA" },
|
|
261
|
+
{ code: 'mfa:manage', label: 'Manage 2FA policies and organization enforcement' },
|
|
262
|
+
],
|
|
263
|
+
});
|
|
264
|
+
//# sourceMappingURL=manifest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.js","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"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"}
|