@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,23 @@
1
+ import type { FastifyInstance, FastifyRequest } from 'fastify';
2
+ import type { AuditPort } from '@endora-commerce/platform/kernel';
3
+ import type { MfaEnrolmentService } from './services/mfa-enrolment-service.js';
4
+ import type { MfaOrgPolicyService } from './services/mfa-org-policy-service.js';
5
+ import type { RequireAdminFactory } from '@endora-commerce/platform/kernel';
6
+ /**
7
+ * Admin MFA management endpoints (feature 042). 2FA reset is a Platform-Admin
8
+ * capability (`mfa:reset`, US6); per-organization enforcement is `mfa:manage`
9
+ * (US3). Every mutation is audited.
10
+ */
11
+ export interface MfaAdminDeps {
12
+ enrolmentService: MfaEnrolmentService;
13
+ orgPolicyService: MfaOrgPolicyService;
14
+ auditLogService: AuditPort;
15
+ requireAdmin: RequireAdminFactory;
16
+ resolveAdminActor: (req: FastifyRequest) => {
17
+ adminUserId: string;
18
+ };
19
+ /** Lists the customer-account ids belonging to an organization (US6 bulk). */
20
+ resolveOrganizationCustomerIds: (organizationId: string) => Promise<string[]>;
21
+ }
22
+ export declare function registerMfaAdminRoutes(app: FastifyInstance, deps: MfaAdminDeps): Promise<void>;
23
+ //# sourceMappingURL=routes.admin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.admin.d.ts","sourceRoot":"","sources":["../../src/backend/routes.admin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAQ/D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kCAAkC,CAAC;AAClE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qCAAqC,CAAC;AAC/E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sCAAsC,CAAC;AAChF,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAE5E;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,gBAAgB,EAAE,mBAAmB,CAAC;IACtC,gBAAgB,EAAE,mBAAmB,CAAC;IACtC,eAAe,EAAE,SAAS,CAAC;IAC3B,YAAY,EAAE,mBAAmB,CAAC;IAClC,iBAAiB,EAAE,CAAC,GAAG,EAAE,cAAc,KAAK;QAAE,WAAW,EAAE,MAAM,CAAA;KAAE,CAAC;IACpE,8EAA8E;IAC9E,8BAA8B,EAAE,CAAC,cAAc,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;CAC/E;AAED,wBAAsB,sBAAsB,CAC1C,GAAG,EAAE,eAAe,EACpB,IAAI,EAAE,YAAY,GACjB,OAAO,CAAC,IAAI,CAAC,CAyGf"}
@@ -0,0 +1,93 @@
1
+ import { ERROR_CODES, mfaResetBulkRequestSchema, mfaOrgPolicyRequestSchema, } from '@endora-commerce/contracts';
2
+ import { HttpError } from '@endora-commerce/platform/http';
3
+ import { isOrgInScope } from '@endora-commerce/platform/tenancy';
4
+ export async function registerMfaAdminRoutes(app, deps) {
5
+ const { enrolmentService, auditLogService, requireAdmin, resolveAdminActor } = deps;
6
+ // Single-account reset (FR-025).
7
+ app.post('/api/v1/admin/customers/:customerId/mfa/reset', { preHandler: requireAdmin('mfa:reset') }, async (request) => {
8
+ const { customerId } = request.params;
9
+ const affected = await enrolmentService.reset({
10
+ subjectType: 'customer',
11
+ subjectId: customerId,
12
+ });
13
+ await auditLogService.record({
14
+ actorAdminUserId: resolveAdminActor(request).adminUserId,
15
+ action: 'mfa.reset',
16
+ objectType: 'customer_account',
17
+ objectId: customerId,
18
+ stateAfter: { affected },
19
+ ...(request.ip ? { ipAddress: request.ip } : {}),
20
+ });
21
+ return { data: { affected } };
22
+ });
23
+ // Bulk reset by explicit ids or by organization (FR-026/028).
24
+ app.post('/api/v1/admin/mfa/reset-bulk', { preHandler: requireAdmin('mfa:reset'), schema: { body: mfaResetBulkRequestSchema } }, async (request) => {
25
+ const body = mfaResetBulkRequestSchema.parse(request.body);
26
+ const actorAdminUserId = resolveAdminActor(request).adminUserId;
27
+ const customerIds = 'customerIds' in body
28
+ ? body.customerIds
29
+ : await deps.resolveOrganizationCustomerIds(body.organizationId);
30
+ let affected = 0;
31
+ for (const customerId of customerIds) {
32
+ const did = await enrolmentService.reset({
33
+ subjectType: 'customer',
34
+ subjectId: customerId,
35
+ });
36
+ if (did) {
37
+ affected += 1;
38
+ await auditLogService.record({
39
+ actorAdminUserId,
40
+ action: 'mfa.reset',
41
+ objectType: 'customer_account',
42
+ objectId: customerId,
43
+ ...(request.ip ? { ipAddress: request.ip } : {}),
44
+ });
45
+ }
46
+ }
47
+ const requested = customerIds.length;
48
+ await auditLogService.record({
49
+ actorAdminUserId,
50
+ action: 'mfa.reset_bulk',
51
+ objectType: 'customer_account',
52
+ objectId: 'organizationId' in body ? body.organizationId : 'selection',
53
+ stateAfter: { requested, affected, skipped: requested - affected },
54
+ ...(request.ip ? { ipAddress: request.ip } : {}),
55
+ });
56
+ return { data: { requested, affected, skipped: requested - affected } };
57
+ });
58
+ // Per-organization enforcement (FR-014, US3).
59
+ app.post('/api/v1/admin/organizations/:organizationId/mfa-policy', { preHandler: requireAdmin('mfa:manage'), schema: { body: mfaOrgPolicyRequestSchema } }, async (request) => {
60
+ const body = mfaOrgPolicyRequestSchema.parse(request.body);
61
+ const { organizationId } = request.params;
62
+ // D-260/B — the organization id arrives from the caller and selects whose
63
+ // 2FA enforcement is written, so no column filter reaches it:
64
+ // `MfaOrganizationPolicy` is `@GlobalEntity()` and correctly so (D-259
65
+ // category 2 — the row *is* the per-organization rule, keyed by an id it
66
+ // does not own). A `sales_representative` holding `mfa:manage` therefore
67
+ // forced and lifted TOTP for an organization it was never assigned;
68
+ // measured at 200 on both.
69
+ //
70
+ // 404 rather than 403 because the id **addresses the resource** — it is
71
+ // the path parameter and the row's own key — which is FR-008's
72
+ // indistinguishable answer, modelled on `credit_limits/routes.ts:135-141`.
73
+ // This is not the `mode: 'all'` arm of D-260's criterion: that arm is for
74
+ // a write that changes the *actor's* own authority, and this one changes
75
+ // an organization's login policy while leaving `allowedOrganizationIds`
76
+ // untouched. A scoped admin keeps the capability for its own assignments.
77
+ if (!isOrgInScope(organizationId)) {
78
+ throw new HttpError(404, ERROR_CODES.NOT_FOUND, 'Organization not found.');
79
+ }
80
+ const actor = resolveAdminActor(request).adminUserId;
81
+ const res = await deps.orgPolicyService.setEnforcement(organizationId, body.enforceTotp, actor);
82
+ await auditLogService.record({
83
+ actorAdminUserId: actor,
84
+ action: 'mfa.org_enforced',
85
+ objectType: 'organization',
86
+ objectId: organizationId,
87
+ stateAfter: { enforceTotp: body.enforceTotp },
88
+ ...(request.ip ? { ipAddress: request.ip } : {}),
89
+ });
90
+ return { data: res };
91
+ });
92
+ }
93
+ //# sourceMappingURL=routes.admin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.admin.js","sourceRoot":"","sources":["../../src/backend/routes.admin.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ import type { AuthSessionPort } from '@endora-commerce/contracts';
3
+ import type { ChallengeStore } from './services/challenge-store.js';
4
+ import type { MfaPolicyResolver } from './services/mfa-policy-resolver.js';
5
+ import type { SocialIdentityService } from './services/social-identity-service.js';
6
+ import type { OAuthProviderPort } from './services/oauth-provider-service.js';
7
+ /**
8
+ * Federated sign-in routes (feature 042, US4 customer / US5 admin). Per surface
9
+ * + provider: a `start` endpoint that redirects to the IdP, and a `callback`
10
+ * that validates the transaction, exchanges the code, resolves the account, and
11
+ * issues a session. A social-authenticated session is granted directly — no
12
+ * additional TOTP step (R10).
13
+ */
14
+ export interface MfaOAuthDeps {
15
+ oauthProvider: OAuthProviderPort;
16
+ challengeStore: ChallengeStore;
17
+ policyResolver: MfaPolicyResolver;
18
+ socialIdentityService: SocialIdentityService;
19
+ sessionService: AuthSessionPort;
20
+ /** Public base URL of the backend (for the provider redirect_uri). */
21
+ backendBaseUrl: string;
22
+ storefrontBaseUrl: string;
23
+ adminBaseUrl: string;
24
+ }
25
+ export declare function registerMfaOAuthRoutes(app: FastifyInstance, deps: MfaOAuthDeps): Promise<void>;
26
+ //# sourceMappingURL=routes.oauth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.oauth.d.ts","sourceRoot":"","sources":["../../src/backend/routes.oauth.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,eAAe,EAAgB,MAAM,SAAS,CAAC;AAG7D,OAAO,KAAK,EAAE,eAAe,EAAkC,MAAM,4BAA4B,CAAC;AAElG,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AACpE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAC;AAC3E,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,uCAAuC,CAAC;AACnF,OAAO,KAAK,EAEV,iBAAiB,EAClB,MAAM,sCAAsC,CAAC;AAE9C;;;;;;GAMG;AACH,MAAM,WAAW,YAAY;IAC3B,aAAa,EAAE,iBAAiB,CAAC;IACjC,cAAc,EAAE,cAAc,CAAC;IAC/B,cAAc,EAAE,iBAAiB,CAAC;IAClC,qBAAqB,EAAE,qBAAqB,CAAC;IAC7C,cAAc,EAAE,eAAe,CAAC;IAChC,sEAAsE;IACtE,cAAc,EAAE,MAAM,CAAC;IACvB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,YAAY,EAAE,MAAM,CAAC;CACtB;AAID,wBAAsB,sBAAsB,CAC1C,GAAG,EAAE,eAAe,EACpB,IAAI,EAAE,YAAY,GACjB,OAAO,CAAC,IAAI,CAAC,CAMf"}
@@ -0,0 +1,155 @@
1
+ import { randomBytes } from 'crypto';
2
+ import { SESSION_COOKIE_NAME, ADMIN_SESSION_COOKIE_NAME } from '@endora-commerce/contracts';
3
+ import { currentSalesChannel } from '@endora-commerce/platform/kernel';
4
+ export async function registerMfaOAuthRoutes(app, deps) {
5
+ for (const surface of ['customer', 'admin']) {
6
+ registerProviders(app, deps, surface);
7
+ registerStart(app, deps, surface);
8
+ registerCallback(app, deps, surface);
9
+ }
10
+ }
11
+ function frontendBase(deps, surface) {
12
+ return surface === 'customer' ? deps.storefrontBaseUrl : deps.adminBaseUrl;
13
+ }
14
+ function parseProvider(value) {
15
+ return value === 'google' || value === 'microsoft' ? value : null;
16
+ }
17
+ function sanitiseNext(input) {
18
+ if (!input || !input.startsWith('/') || input.startsWith('//'))
19
+ return '/account';
20
+ return input;
21
+ }
22
+ function loginRedirect(deps, surface, message) {
23
+ const base = frontendBase(deps, surface);
24
+ return `${base}/login?error=${encodeURIComponent(message)}`;
25
+ }
26
+ /**
27
+ * The providers a login screen may actually offer, for this surface and — for
28
+ * the customer surface — this sales channel.
29
+ *
30
+ * **Pre-auth and public, deliberately.** The login screens are the callers, so
31
+ * there is no actor to gate on; and the answer is exactly what the buttons
32
+ * themselves announce, so it reveals nothing a visitor could not read off the
33
+ * page. It carries no client id, no redirect URI, and nothing per-account —
34
+ * asking it about a particular user is not possible, which is what keeps it
35
+ * from becoming a probe for whether an address has a federated identity.
36
+ *
37
+ * The body is the same conjunction `registerStart` applies below before it will
38
+ * redirect anywhere: the operator's setting for this surface and channel, AND
39
+ * the provider actually being configured. Publishing it stops the frontend from
40
+ * guessing at a decision this module already makes — it used to render both
41
+ * buttons unconditionally, which on a default deployment (both settings
42
+ * `false`) advertised two sign-in routes that could not work (#193).
43
+ *
44
+ * `mfa` owns it, so `ctx.routes` gates it like everything else the module owns:
45
+ * with the module off it answers 503 `MODULE_DISABLED`, and both frontends fail
46
+ * closed on that — no module, no buttons.
47
+ */
48
+ function registerProviders(app, deps, surface) {
49
+ app.get(`/api/v1/auth/${surface}/oauth/providers`, async (_request, reply) => {
50
+ const salesChannelId = surface === 'customer' ? (currentSalesChannel()?.id ?? null) : null;
51
+ const policy = await deps.policyResolver.resolve({ subjectType: surface, subjectId: '' }, { salesChannelId });
52
+ const providers = ['google', 'microsoft'].filter((provider) => {
53
+ const settingEnabled = provider === 'google' ? policy.googleEnabled : policy.microsoftEnabled;
54
+ return settingEnabled && deps.oauthProvider.isEnabled(provider);
55
+ });
56
+ const body = { providers };
57
+ return reply.send(body);
58
+ });
59
+ }
60
+ function registerStart(app, deps, surface) {
61
+ app.get(`/api/v1/auth/${surface}/oauth/:provider/start`, async (request, reply) => {
62
+ const provider = parseProvider(request.params.provider);
63
+ if (!provider)
64
+ return reply.redirect(loginRedirect(deps, surface, 'Unknown sign-in provider.'));
65
+ const salesChannelId = surface === 'customer' ? (currentSalesChannel()?.id ?? null) : null;
66
+ const policy = await deps.policyResolver.resolve({ subjectType: surface, subjectId: '' }, { salesChannelId });
67
+ const settingEnabled = provider === 'google' ? policy.googleEnabled : policy.microsoftEnabled;
68
+ if (!settingEnabled || !deps.oauthProvider.isEnabled(provider)) {
69
+ return reply.redirect(loginRedirect(deps, surface, 'Sign-in with this provider is not available.'));
70
+ }
71
+ const next = sanitiseNext(request.query.next);
72
+ const codeVerifier = randomBytes(32).toString('base64url');
73
+ const nonce = randomBytes(16).toString('base64url');
74
+ const state = await deps.challengeStore.issueOAuthTransaction({
75
+ surface,
76
+ provider,
77
+ salesChannelId,
78
+ pkceVerifier: codeVerifier,
79
+ nonce,
80
+ next,
81
+ });
82
+ const redirectUri = `${deps.backendBaseUrl}/api/v1/auth/${surface}/oauth/${provider}/callback`;
83
+ const url = await deps.oauthProvider.buildAuthorizationUrl(provider, {
84
+ state,
85
+ codeVerifier,
86
+ nonce,
87
+ redirectUri,
88
+ });
89
+ return reply.redirect(url);
90
+ });
91
+ }
92
+ function registerCallback(app, deps, surface) {
93
+ app.get(`/api/v1/auth/${surface}/oauth/:provider/callback`, async (request, reply) => {
94
+ const provider = parseProvider(request.params.provider);
95
+ const { code, state, error } = request.query;
96
+ if (!provider || error || !code || !state) {
97
+ return reply.redirect(loginRedirect(deps, surface, 'Sign-in was cancelled or failed.'));
98
+ }
99
+ const tx = await deps.challengeStore.getOAuthTransaction(state);
100
+ if (!tx || tx.surface !== surface || tx.provider !== provider) {
101
+ return reply.redirect(loginRedirect(deps, surface, 'This sign-in attempt has expired.'));
102
+ }
103
+ await deps.challengeStore.consumeOAuthTransaction(state);
104
+ const redirectUri = `${deps.backendBaseUrl}/api/v1/auth/${surface}/oauth/${provider}/callback`;
105
+ let identity;
106
+ try {
107
+ identity = await deps.oauthProvider.exchangeCode(provider, {
108
+ code,
109
+ state,
110
+ codeVerifier: tx.pkceVerifier,
111
+ nonce: tx.nonce,
112
+ redirectUri,
113
+ });
114
+ }
115
+ catch {
116
+ return reply.redirect(loginRedirect(deps, surface, 'Sign-in failed. Please try again.'));
117
+ }
118
+ const result = surface === 'customer'
119
+ ? await deps.socialIdentityService.signInCustomer(identity)
120
+ : await deps.socialIdentityService.signInAdmin(identity);
121
+ if (!result.ok) {
122
+ const message = result.reason === 'unverified'
123
+ ? 'Your provider email is not verified.'
124
+ : result.reason === 'no_account'
125
+ ? 'No account matches this provider. Please contact an administrator.'
126
+ : 'No account found. Please create an account first.';
127
+ return reply.redirect(loginRedirect(deps, surface, message));
128
+ }
129
+ const session = await deps.sessionService.createSession(surface === 'customer'
130
+ ? { kind: 'customer', customerAccountId: result.subjectId, ...ipUa(request) }
131
+ : { kind: 'admin', adminUserId: result.subjectId, ...ipUa(request) });
132
+ setSessionCookie(reply, surface === 'customer' ? SESSION_COOKIE_NAME : ADMIN_SESSION_COOKIE_NAME, session.cookieValue, session.expiresAt);
133
+ const dest = surface === 'customer' ? `${deps.storefrontBaseUrl}${sanitiseNext(tx.next)}` : `${deps.adminBaseUrl}/`;
134
+ return reply.redirect(dest);
135
+ });
136
+ }
137
+ function ipUa(request) {
138
+ return {
139
+ ...(request.ip ? { ipAddress: request.ip } : {}),
140
+ ...(typeof request.headers['user-agent'] === 'string'
141
+ ? { userAgent: request.headers['user-agent'] }
142
+ : {}),
143
+ };
144
+ }
145
+ function setSessionCookie(reply, cookieName, value, expiresAt) {
146
+ reply.setCookie(cookieName, value, {
147
+ path: '/',
148
+ httpOnly: true,
149
+ sameSite: 'lax',
150
+ secure: process.env['NODE_ENV'] === 'production',
151
+ expires: expiresAt,
152
+ signed: false,
153
+ });
154
+ }
155
+ //# sourceMappingURL=routes.oauth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.oauth.js","sourceRoot":"","sources":["../../src/backend/routes.oauth.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
2
+ import type { AuditPort } from '@endora-commerce/platform/kernel';
3
+ import type { MfaOrgPolicyService } from './services/mfa-org-policy-service.js';
4
+ /**
5
+ * Storefront org-admin 2FA enforcement (feature 042, US3, FR-014). An
6
+ * Organization Administrator (customer role `organization_admin`) can require
7
+ * 2FA for their own organization's members. The guard + organization id are
8
+ * resolved by an injected port so the module stays decoupled.
9
+ */
10
+ export interface MfaOrgRoutesDeps {
11
+ orgPolicyService: MfaOrgPolicyService;
12
+ auditLogService: AuditPort;
13
+ requireCustomer: (req: FastifyRequest, reply: FastifyReply) => Promise<void>;
14
+ /** Resolves the caller's org and asserts they are an org admin (throws 403). */
15
+ resolveOrgAdmin: (req: FastifyRequest) => Promise<{
16
+ organizationId: string;
17
+ actor: string;
18
+ }>;
19
+ }
20
+ export declare function registerMfaOrgRoutes(app: FastifyInstance, deps: MfaOrgRoutesDeps): Promise<void>;
21
+ //# sourceMappingURL=routes.org.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.org.d.ts","sourceRoot":"","sources":["../../src/backend/routes.org.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAE7E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kCAAkC,CAAC;AAClE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sCAAsC,CAAC;AAEhF;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAC/B,gBAAgB,EAAE,mBAAmB,CAAC;IACtC,eAAe,EAAE,SAAS,CAAC;IAC3B,eAAe,EAAE,CAAC,GAAG,EAAE,cAAc,EAAE,KAAK,EAAE,YAAY,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7E,gFAAgF;IAChF,eAAe,EAAE,CAAC,GAAG,EAAE,cAAc,KAAK,OAAO,CAAC;QAAE,cAAc,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC9F;AAED,wBAAsB,oBAAoB,CACxC,GAAG,EAAE,eAAe,EACpB,IAAI,EAAE,gBAAgB,GACrB,OAAO,CAAC,IAAI,CAAC,CAkBf"}
@@ -0,0 +1,17 @@
1
+ import { mfaOrgPolicyRequestSchema } from '@endora-commerce/contracts';
2
+ export async function registerMfaOrgRoutes(app, deps) {
3
+ app.put('/api/v1/account/organization/mfa-policy', { preHandler: deps.requireCustomer, schema: { body: mfaOrgPolicyRequestSchema } }, async (request) => {
4
+ const body = mfaOrgPolicyRequestSchema.parse(request.body);
5
+ const { organizationId, actor } = await deps.resolveOrgAdmin(request);
6
+ const res = await deps.orgPolicyService.setEnforcement(organizationId, body.enforceTotp, actor);
7
+ await deps.auditLogService.record({
8
+ action: 'mfa.org_enforced',
9
+ objectType: 'organization',
10
+ objectId: organizationId,
11
+ stateAfter: { enforceTotp: body.enforceTotp },
12
+ ...(request.ip ? { ipAddress: request.ip } : {}),
13
+ });
14
+ return { data: res };
15
+ });
16
+ }
17
+ //# sourceMappingURL=routes.org.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.org.js","sourceRoot":"","sources":["../../src/backend/routes.org.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,yBAAyB,EAAE,MAAM,4BAA4B,CAAC;AAkBvE,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,GAAoB,EACpB,IAAsB;IAEtB,GAAG,CAAC,GAAG,CACL,yCAAyC,EACzC,EAAE,UAAU,EAAE,IAAI,CAAC,eAAe,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,yBAAyB,EAAE,EAAE,EACjF,KAAK,EAAE,OAAO,EAAE,EAAE;QAChB,MAAM,IAAI,GAAG,yBAAyB,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QAC3D,MAAM,EAAE,cAAc,EAAE,KAAK,EAAE,GAAG,MAAM,IAAI,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC;QACtE,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,gBAAgB,CAAC,cAAc,CAAC,cAAc,EAAE,IAAI,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC;QAChG,MAAM,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC;YAChC,MAAM,EAAE,kBAAkB;YAC1B,UAAU,EAAE,cAAc;YAC1B,QAAQ,EAAE,cAAc;YACxB,UAAU,EAAE,EAAE,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE;YAC7C,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACjD,CAAC,CAAC;QACH,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC;IACvB,CAAC,CACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,21 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ import type { AuthSessionPort } from '@endora-commerce/contracts';
3
+ import type { AuditPort } from '@endora-commerce/platform/kernel';
4
+ import type { MfaLoginService } from './services/mfa-login-service.js';
5
+ import type { ChallengeStore } from './services/challenge-store.js';
6
+ import type { MfaEnrolmentService } from './services/mfa-enrolment-service.js';
7
+ /**
8
+ * Public MFA endpoints (feature 042). Per surface (customer US1 / admin US2):
9
+ * the second-step verify, and the enforced-but-unenrolled setup-ticket flow
10
+ * (US3). They differ only in the expected subject surface, session kind, and
11
+ * cookie name.
12
+ */
13
+ export interface MfaPublicDeps {
14
+ loginService: MfaLoginService;
15
+ sessionService: AuthSessionPort;
16
+ challengeStore: ChallengeStore;
17
+ enrolmentService: MfaEnrolmentService;
18
+ auditLogService: AuditPort;
19
+ }
20
+ export declare function registerMfaPublicRoutes(app: FastifyInstance, deps: MfaPublicDeps): Promise<void>;
21
+ //# sourceMappingURL=routes.public.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.public.d.ts","sourceRoot":"","sources":["../../src/backend/routes.public.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,eAAe,EAAgB,MAAM,SAAS,CAAC;AAQ7D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAElE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kCAAkC,CAAC;AAClE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AACvE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AACpE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qCAAqC,CAAC;AAE/E;;;;;GAKG;AACH,MAAM,WAAW,aAAa;IAC5B,YAAY,EAAE,eAAe,CAAC;IAC9B,cAAc,EAAE,eAAe,CAAC;IAChC,cAAc,EAAE,cAAc,CAAC;IAC/B,gBAAgB,EAAE,mBAAmB,CAAC;IACtC,eAAe,EAAE,SAAS,CAAC;CAC5B;AA2BD,wBAAsB,uBAAuB,CAC3C,GAAG,EAAE,eAAe,EACpB,IAAI,EAAE,aAAa,GAClB,OAAO,CAAC,IAAI,CAAC,CAKf"}
@@ -0,0 +1,118 @@
1
+ import { mfaVerifyRequestSchema, mfaSetupTicketBeginSchema, mfaSetupTicketCompleteSchema, SESSION_COOKIE_NAME, ADMIN_SESSION_COOKIE_NAME, } from '@endora-commerce/contracts';
2
+ import { HttpError } from '@endora-commerce/platform/http';
3
+ const SURFACES = [
4
+ {
5
+ subjectType: 'customer',
6
+ cookieName: SESSION_COOKIE_NAME,
7
+ verifyPath: '/api/v1/auth/customer/mfa/verify',
8
+ setupBeginPath: '/api/v1/auth/customer/mfa/setup-ticket/begin',
9
+ setupCompletePath: '/api/v1/auth/customer/mfa/setup-ticket/complete',
10
+ },
11
+ {
12
+ subjectType: 'admin',
13
+ cookieName: ADMIN_SESSION_COOKIE_NAME,
14
+ verifyPath: '/api/v1/auth/admin/mfa/verify',
15
+ setupBeginPath: '/api/v1/auth/admin/mfa/setup-ticket/begin',
16
+ setupCompletePath: '/api/v1/auth/admin/mfa/setup-ticket/complete',
17
+ },
18
+ ];
19
+ export async function registerMfaPublicRoutes(app, deps) {
20
+ for (const cfg of SURFACES) {
21
+ registerVerifyRoute(app, deps, { path: cfg.verifyPath, subjectType: cfg.subjectType, cookieName: cfg.cookieName });
22
+ registerSetupTicketRoutes(app, deps, cfg);
23
+ }
24
+ }
25
+ /** Enforced-but-unenrolled flow: enrol using a setup ticket, then get a session. */
26
+ function registerSetupTicketRoutes(app, deps, cfg) {
27
+ app.post(cfg.setupBeginPath, { schema: { body: mfaSetupTicketBeginSchema } }, async (request) => {
28
+ const body = mfaSetupTicketBeginSchema.parse(request.body);
29
+ const ticket = await resolveTicket(deps, body.setupTicket, cfg.subjectType);
30
+ return {
31
+ data: await deps.enrolmentService.setup({ subjectType: cfg.subjectType, subjectId: ticket.subjectId }, ticket.subjectId),
32
+ };
33
+ });
34
+ app.post(cfg.setupCompletePath, { schema: { body: mfaSetupTicketCompleteSchema } }, async (request, reply) => {
35
+ const body = mfaSetupTicketCompleteSchema.parse(request.body);
36
+ const ticket = await resolveTicket(deps, body.setupTicket, cfg.subjectType);
37
+ const subject = { subjectType: cfg.subjectType, subjectId: ticket.subjectId };
38
+ const { recoveryCodes } = await deps.enrolmentService.activate(subject, body.code);
39
+ await deps.challengeStore.consumeSetupTicket(body.setupTicket);
40
+ const session = await deps.sessionService.createSession(cfg.subjectType === 'customer'
41
+ ? { kind: 'customer', customerAccountId: ticket.subjectId, ...ipUa(request) }
42
+ : { kind: 'admin', adminUserId: ticket.subjectId, ...ipUa(request) });
43
+ setSessionCookie(reply, cfg.cookieName, session.cookieValue, session.expiresAt);
44
+ return { data: { status: 'authenticated', recoveryCodes } };
45
+ });
46
+ }
47
+ async function resolveTicket(deps, setupTicket, subjectType) {
48
+ const ticket = await deps.challengeStore.getSetupTicket(setupTicket);
49
+ if (!ticket || ticket.subjectType !== subjectType) {
50
+ throw new HttpError(400, 'MFA_INVALID_CHALLENGE', 'This setup session has expired. Please sign in again.');
51
+ }
52
+ return { subjectId: ticket.subjectId };
53
+ }
54
+ function registerVerifyRoute(app, deps, cfg) {
55
+ app.post(cfg.path, { schema: { body: mfaVerifyRequestSchema } }, async (request, reply) => {
56
+ const body = mfaVerifyRequestSchema.parse(request.body);
57
+ const objectType = cfg.subjectType === 'customer' ? 'customer_account' : 'admin_user';
58
+ const result = await deps.loginService.verifyChallenge(body.challengeId, body.code);
59
+ if (!result.ok) {
60
+ if (result.error === 'invalid_challenge') {
61
+ throw new HttpError(400, 'MFA_INVALID_CHALLENGE', 'This login attempt has expired. Please sign in again.');
62
+ }
63
+ if (result.error === 'locked') {
64
+ await deps.auditLogService.record({
65
+ action: 'mfa.second_step_locked',
66
+ objectType,
67
+ objectId: result.subject.subjectId,
68
+ ...(request.ip ? { ipAddress: request.ip } : {}),
69
+ });
70
+ throw new HttpError(429, 'MFA_TOO_MANY_ATTEMPTS', 'Too many incorrect codes. Please sign in again.');
71
+ }
72
+ throw new HttpError(401, 'MFA_INVALID_CODE', 'The code is invalid or expired.');
73
+ }
74
+ if (result.subject.subjectType !== cfg.subjectType) {
75
+ throw new HttpError(400, 'MFA_WRONG_SURFACE', 'This challenge is not valid here.');
76
+ }
77
+ if (result.factor === 'recovery') {
78
+ await deps.auditLogService.record({
79
+ action: 'mfa.recovery_code_used',
80
+ objectType,
81
+ objectId: result.subject.subjectId,
82
+ ...(request.ip ? { ipAddress: request.ip } : {}),
83
+ });
84
+ }
85
+ const session = await deps.sessionService.createSession(cfg.subjectType === 'customer'
86
+ ? {
87
+ kind: 'customer',
88
+ customerAccountId: result.subject.subjectId,
89
+ ...ipUa(request),
90
+ }
91
+ : {
92
+ kind: 'admin',
93
+ adminUserId: result.subject.subjectId,
94
+ ...ipUa(request),
95
+ });
96
+ setSessionCookie(reply, cfg.cookieName, session.cookieValue, session.expiresAt);
97
+ return { data: { status: 'authenticated' } };
98
+ });
99
+ }
100
+ function ipUa(request) {
101
+ return {
102
+ ...(request.ip ? { ipAddress: request.ip } : {}),
103
+ ...(typeof request.headers['user-agent'] === 'string'
104
+ ? { userAgent: request.headers['user-agent'] }
105
+ : {}),
106
+ };
107
+ }
108
+ function setSessionCookie(reply, cookieName, value, expiresAt) {
109
+ reply.setCookie(cookieName, value, {
110
+ path: '/',
111
+ httpOnly: true,
112
+ sameSite: 'lax',
113
+ secure: process.env['NODE_ENV'] === 'production',
114
+ expires: expiresAt,
115
+ signed: false,
116
+ });
117
+ }
118
+ //# sourceMappingURL=routes.public.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.public.js","sourceRoot":"","sources":["../../src/backend/routes.public.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,34 @@
1
+ import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
2
+ import type { AuditPort } from '@endora-commerce/platform/kernel';
3
+ import type { MfaEnrolmentService } from './services/mfa-enrolment-service.js';
4
+ import type { MfaPolicyResolver } from './services/mfa-policy-resolver.js';
5
+ import type { SocialLinkService } from './services/social-link-service.js';
6
+ /**
7
+ * Surface-agnostic self-service 2FA endpoints (feature 042). Mounted twice by
8
+ * the plugin: once for storefront customers (`/api/v1/account/mfa/*`, US1) and
9
+ * once for admin users (`/api/v1/admin/account/mfa/*`, US2). The only
10
+ * differences are the path prefix, the auth guard, how the subject id is
11
+ * resolved, and the audit `objectType`.
12
+ */
13
+ export interface MfaSelfServiceOptions {
14
+ pathPrefix: string;
15
+ subjectType: 'customer' | 'admin';
16
+ auditObjectType: 'customer_account' | 'admin_user';
17
+ requireGuard: (req: FastifyRequest, reply: FastifyReply) => Promise<void>;
18
+ resolveSubjectId: (req: FastifyRequest) => string;
19
+ resolveOrganizationId?: (req: FastifyRequest) => string | null;
20
+ enrolmentService: MfaEnrolmentService;
21
+ policyResolver: MfaPolicyResolver;
22
+ socialLinkService: SocialLinkService;
23
+ auditLogService: AuditPort;
24
+ /**
25
+ * `specs/110-instance-repository/` T118c — required, both of them. `mfa`
26
+ * resolves each from its owner's published port rather than taking whatever a
27
+ * composition root happened to contribute, and while they were optional the
28
+ * two roots disagreed: production answered both, the test harness neither.
29
+ */
30
+ resolveAccountEmail: (subjectType: 'customer' | 'admin', subjectId: string) => Promise<string | null>;
31
+ verifyAccountPassword: (subjectType: 'customer' | 'admin', subjectId: string, password: string) => Promise<boolean>;
32
+ }
33
+ export declare function registerMfaSelfServiceRoutes(app: FastifyInstance, opts: MfaSelfServiceOptions): Promise<void>;
34
+ //# sourceMappingURL=routes.self-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.self-service.d.ts","sourceRoot":"","sources":["../../src/backend/routes.self-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAS7E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kCAAkC,CAAC;AAElE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qCAAqC,CAAC;AAC/E,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAC;AAC3E,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAC;AAE3E;;;;;;GAMG;AACH,MAAM,WAAW,qBAAqB;IACpC,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,UAAU,GAAG,OAAO,CAAC;IAClC,eAAe,EAAE,kBAAkB,GAAG,YAAY,CAAC;IACnD,YAAY,EAAE,CAAC,GAAG,EAAE,cAAc,EAAE,KAAK,EAAE,YAAY,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1E,gBAAgB,EAAE,CAAC,GAAG,EAAE,cAAc,KAAK,MAAM,CAAC;IAClD,qBAAqB,CAAC,EAAE,CAAC,GAAG,EAAE,cAAc,KAAK,MAAM,GAAG,IAAI,CAAC;IAC/D,gBAAgB,EAAE,mBAAmB,CAAC;IACtC,cAAc,EAAE,iBAAiB,CAAC;IAClC,iBAAiB,EAAE,iBAAiB,CAAC;IACrC,eAAe,EAAE,SAAS,CAAC;IAC3B;;;;;OAKG;IACH,mBAAmB,EAAE,CACnB,WAAW,EAAE,UAAU,GAAG,OAAO,EACjC,SAAS,EAAE,MAAM,KACd,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC5B,qBAAqB,EAAE,CACrB,WAAW,EAAE,UAAU,GAAG,OAAO,EACjC,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,KACb,OAAO,CAAC,OAAO,CAAC,CAAC;CACvB;AAED,wBAAsB,4BAA4B,CAChD,GAAG,EAAE,eAAe,EACpB,IAAI,EAAE,qBAAqB,GAC1B,OAAO,CAAC,IAAI,CAAC,CA6Hf"}