@endora-commerce/mod-customer-accounts 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 (127) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +59 -0
  3. package/dist/admin/api/customer-groups-client.d.ts +27 -0
  4. package/dist/admin/api/customer-groups-client.d.ts.map +1 -0
  5. package/dist/admin/api/customer-groups-client.js +17 -0
  6. package/dist/admin/api/customer-groups-client.js.map +1 -0
  7. package/dist/admin/index.d.ts +33 -0
  8. package/dist/admin/index.d.ts.map +1 -0
  9. package/dist/admin/index.js +38 -0
  10. package/dist/admin/index.js.map +1 -0
  11. package/dist/admin/pages/CustomerGroupsPage.d.ts +18 -0
  12. package/dist/admin/pages/CustomerGroupsPage.d.ts.map +1 -0
  13. package/dist/admin/pages/CustomerGroupsPage.js +108 -0
  14. package/dist/admin/pages/CustomerGroupsPage.js.map +1 -0
  15. package/dist/backend/entities/customer-account.entity.d.ts +111 -0
  16. package/dist/backend/entities/customer-account.entity.d.ts.map +1 -0
  17. package/dist/backend/entities/customer-account.entity.js +230 -0
  18. package/dist/backend/entities/customer-account.entity.js.map +1 -0
  19. package/dist/backend/entities/customer-group.entity.d.ts +29 -0
  20. package/dist/backend/entities/customer-group.entity.d.ts.map +1 -0
  21. package/dist/backend/entities/customer-group.entity.js +70 -0
  22. package/dist/backend/entities/customer-group.entity.js.map +1 -0
  23. package/dist/backend/entities/password-reset-token.entity.d.ts +16 -0
  24. package/dist/backend/entities/password-reset-token.entity.d.ts.map +1 -0
  25. package/dist/backend/entities/password-reset-token.entity.js +59 -0
  26. package/dist/backend/entities/password-reset-token.entity.js.map +1 -0
  27. package/dist/backend/index.d.ts +144 -0
  28. package/dist/backend/index.d.ts.map +1 -0
  29. package/dist/backend/index.js +407 -0
  30. package/dist/backend/index.js.map +1 -0
  31. package/dist/backend/routes.admin.d.ts +26 -0
  32. package/dist/backend/routes.admin.d.ts.map +1 -0
  33. package/dist/backend/routes.admin.js +35 -0
  34. package/dist/backend/routes.admin.js.map +1 -0
  35. package/dist/backend/services/audit-references.d.ts +22 -0
  36. package/dist/backend/services/audit-references.d.ts.map +1 -0
  37. package/dist/backend/services/audit-references.js +46 -0
  38. package/dist/backend/services/audit-references.js.map +1 -0
  39. package/dist/backend/services/customer-account-lifecycle-ports.d.ts +168 -0
  40. package/dist/backend/services/customer-account-lifecycle-ports.d.ts.map +1 -0
  41. package/dist/backend/services/customer-account-lifecycle-ports.js +420 -0
  42. package/dist/backend/services/customer-account-lifecycle-ports.js.map +1 -0
  43. package/dist/backend/services/customer-account-ports.d.ts +143 -0
  44. package/dist/backend/services/customer-account-ports.d.ts.map +1 -0
  45. package/dist/backend/services/customer-account-ports.js +422 -0
  46. package/dist/backend/services/customer-account-ports.js.map +1 -0
  47. package/dist/backend/services/customer-auth-service.d.ts +62 -0
  48. package/dist/backend/services/customer-auth-service.d.ts.map +1 -0
  49. package/dist/backend/services/customer-auth-service.js +105 -0
  50. package/dist/backend/services/customer-auth-service.js.map +1 -0
  51. package/dist/backend/services/customer-group-read-port.d.ts +24 -0
  52. package/dist/backend/services/customer-group-read-port.d.ts.map +1 -0
  53. package/dist/backend/services/customer-group-read-port.js +44 -0
  54. package/dist/backend/services/customer-group-read-port.js.map +1 -0
  55. package/dist/backend/services/customer-group-service.d.ts +21 -0
  56. package/dist/backend/services/customer-group-service.d.ts.map +1 -0
  57. package/dist/backend/services/customer-group-service.js +97 -0
  58. package/dist/backend/services/customer-group-service.js.map +1 -0
  59. package/dist/backend/services/customer-rollup-scope.d.ts +30 -0
  60. package/dist/backend/services/customer-rollup-scope.d.ts.map +1 -0
  61. package/dist/backend/services/customer-rollup-scope.js +42 -0
  62. package/dist/backend/services/customer-rollup-scope.js.map +1 -0
  63. package/dist/backend/services/password-reset-service.d.ts +15 -0
  64. package/dist/backend/services/password-reset-service.d.ts.map +1 -0
  65. package/dist/backend/services/password-reset-service.js +90 -0
  66. package/dist/backend/services/password-reset-service.js.map +1 -0
  67. package/dist/backend/services/role-service.d.ts +22 -0
  68. package/dist/backend/services/role-service.d.ts.map +1 -0
  69. package/dist/backend/services/role-service.js +83 -0
  70. package/dist/backend/services/role-service.js.map +1 -0
  71. package/dist/backend/services/two-factor-enrolments.d.ts +18 -0
  72. package/dist/backend/services/two-factor-enrolments.d.ts.map +1 -0
  73. package/dist/backend/services/two-factor-enrolments.js +2 -0
  74. package/dist/backend/services/two-factor-enrolments.js.map +1 -0
  75. package/dist/manifest.d.ts +169 -0
  76. package/dist/manifest.d.ts.map +1 -0
  77. package/dist/manifest.js +212 -0
  78. package/dist/manifest.js.map +1 -0
  79. package/dist/migrations/20260425T055041_customer_accounts_password_reset_tokens.d.ts +6 -0
  80. package/dist/migrations/20260425T055041_customer_accounts_password_reset_tokens.d.ts.map +1 -0
  81. package/dist/migrations/20260425T055041_customer_accounts_password_reset_tokens.js +25 -0
  82. package/dist/migrations/20260425T055041_customer_accounts_password_reset_tokens.js.map +1 -0
  83. package/dist/migrations/20260611T140351_customer_accounts_organization_optional.d.ts +34 -0
  84. package/dist/migrations/20260611T140351_customer_accounts_organization_optional.d.ts.map +1 -0
  85. package/dist/migrations/20260611T140351_customer_accounts_organization_optional.js +43 -0
  86. package/dist/migrations/20260611T140351_customer_accounts_organization_optional.js.map +1 -0
  87. package/dist/migrations/20260611T140403_customer_accounts_lifecycle.d.ts +54 -0
  88. package/dist/migrations/20260611T140403_customer_accounts_lifecycle.d.ts.map +1 -0
  89. package/dist/migrations/20260611T140403_customer_accounts_lifecycle.js +97 -0
  90. package/dist/migrations/20260611T140403_customer_accounts_lifecycle.js.map +1 -0
  91. package/dist/migrations/20260718T200341_customer_accounts_customer_account_custom_field_values.d.ts +12 -0
  92. package/dist/migrations/20260718T200341_customer_accounts_customer_account_custom_field_values.d.ts.map +1 -0
  93. package/dist/migrations/20260718T200341_customer_accounts_customer_account_custom_field_values.js +16 -0
  94. package/dist/migrations/20260718T200341_customer_accounts_customer_account_custom_field_values.js.map +1 -0
  95. package/dist/migrations/20260720T044255_customer_accounts_customer_subtree_rollup.d.ts +14 -0
  96. package/dist/migrations/20260720T044255_customer_accounts_customer_subtree_rollup.d.ts.map +1 -0
  97. package/dist/migrations/20260720T044255_customer_accounts_customer_subtree_rollup.js +18 -0
  98. package/dist/migrations/20260720T044255_customer_accounts_customer_subtree_rollup.js.map +1 -0
  99. package/dist/migrations/20260819T074816_customer_accounts_password_set_at.d.ts +22 -0
  100. package/dist/migrations/20260819T074816_customer_accounts_password_set_at.d.ts.map +1 -0
  101. package/dist/migrations/20260819T074816_customer_accounts_password_set_at.js +26 -0
  102. package/dist/migrations/20260819T074816_customer_accounts_password_set_at.js.map +1 -0
  103. package/dist/migrations/20260819T142837_customer_accounts_fold_email_case.d.ts +46 -0
  104. package/dist/migrations/20260819T142837_customer_accounts_fold_email_case.d.ts.map +1 -0
  105. package/dist/migrations/20260819T142837_customer_accounts_fold_email_case.js +97 -0
  106. package/dist/migrations/20260819T142837_customer_accounts_fold_email_case.js.map +1 -0
  107. package/dist/migrations/20260825T124759_customer_accounts_drop_legacy_two_factor_secret.d.ts +31 -0
  108. package/dist/migrations/20260825T124759_customer_accounts_drop_legacy_two_factor_secret.d.ts.map +1 -0
  109. package/dist/migrations/20260825T124759_customer_accounts_drop_legacy_two_factor_secret.js +35 -0
  110. package/dist/migrations/20260825T124759_customer_accounts_drop_legacy_two_factor_secret.js.map +1 -0
  111. package/dist/migrations/20260825T141659_customer_accounts_organization_required.d.ts +58 -0
  112. package/dist/migrations/20260825T141659_customer_accounts_organization_required.d.ts.map +1 -0
  113. package/dist/migrations/20260825T141659_customer_accounts_organization_required.js +133 -0
  114. package/dist/migrations/20260825T141659_customer_accounts_organization_required.js.map +1 -0
  115. package/dist/migrations/20260912T094701_customer_accounts_sales_channel_customer_accounts.d.ts +27 -0
  116. package/dist/migrations/20260912T094701_customer_accounts_sales_channel_customer_accounts.d.ts.map +1 -0
  117. package/dist/migrations/20260912T094701_customer_accounts_sales_channel_customer_accounts.js +44 -0
  118. package/dist/migrations/20260912T094701_customer_accounts_sales_channel_customer_accounts.js.map +1 -0
  119. package/dist/migrations/index.d.ts +36 -0
  120. package/dist/migrations/index.d.ts.map +1 -0
  121. package/dist/migrations/index.js +47 -0
  122. package/dist/migrations/index.js.map +1 -0
  123. package/docs/customer_accounts.md +38 -0
  124. package/i18n/en.json +30 -0
  125. package/i18n/pl.json +30 -0
  126. package/package.json +94 -0
  127. package/tailwind.css +14 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"customer-auth-service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/customer-auth-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAGL,KAAK,eAAe,EACpB,KAAK,YAAY,EAClB,MAAM,4BAA4B,CAAC;AAGpC,OAAO,EAAE,eAAe,EAAE,MAAM,wCAAwC,CAAC;AAEzE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kCAAkC,CAAC;AAElE;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,WAAW,WAAW;IAC1B,eAAe,EAAE,eAAe,CAAC;IACjC,+CAA+C;IAC/C,kBAAkB,EAAE,MAAM,CAAC;IAC3B,gBAAgB,EAAE,IAAI,CAAC;CACxB;AAED,mEAAmE;AACnE,MAAM,MAAM,oBAAoB,GAC5B,CAAC;IAAE,MAAM,EAAE,eAAe,CAAA;CAAE,GAAG,WAAW,CAAC,GAC3C;IAAE,MAAM,EAAE,aAAa,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GAC9C;IAAE,MAAM,EAAE,kBAAkB,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,CAAC;AAExD,qBAAa,mBAAmB;IAE5B,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,cAAc;IAC/B,mEAAmE;IACnE,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAC;IACjC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC;gBAJT,SAAS,EAAE,MAAM,aAAa,EAC9B,cAAc,EAAE,eAAe;IAChD,mEAAmE;IAClD,eAAe,CAAC,GAAE,MAAM,YAAY,GAAG,SAAS,aAAA,EAChD,QAAQ,CAAC,EAAE,SAAS,YAAA;IAGjC,KAAK,CAAC,KAAK,EAAE;QACjB,KAAK,EAAE,MAAM,CAAC;QACd,QAAQ,EAAE,MAAM,CAAC;QACjB,EAAE,CAAC,EAAE,MAAM,CAAC;QACZ,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;KAChC,GAAG,OAAO,CAAC,oBAAoB,CAAC;IAqE3B,cAAc,CAClB,iBAAiB,EAAE,MAAM,EACzB,eAAe,EAAE,MAAM,EACvB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,IAAI,CAAC;IA8BV,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAG/C"}
@@ -0,0 +1,105 @@
1
+ import { ERROR_CODES, normalizeEmailAddress, } from '@endora-commerce/contracts';
2
+ import { HttpError } from '@endora-commerce/platform/http';
3
+ import { hashPassword, verifyPassword } from '@endora-commerce/platform/kernel';
4
+ import { CustomerAccount } from '../entities/customer-account.entity.js';
5
+ import { recordAuditFromContext } from '@endora-commerce/platform/commands';
6
+ export class CustomerAuthService {
7
+ emFactory;
8
+ sessionService;
9
+ getMfaLoginPort;
10
+ auditLog;
11
+ constructor(emFactory, sessionService,
12
+ /** Lazily resolved so composition can late-bind the MFA module. */
13
+ getMfaLoginPort, auditLog) {
14
+ this.emFactory = emFactory;
15
+ this.sessionService = sessionService;
16
+ this.getMfaLoginPort = getMfaLoginPort;
17
+ this.auditLog = auditLog;
18
+ }
19
+ async login(input) {
20
+ const em = this.emFactory();
21
+ // Folded, because the row is. `=` on `text` is case-sensitive in Postgres,
22
+ // so comparing the address exactly as typed refused every account whose
23
+ // holder had capitalised anything — with the generic error above, which
24
+ // says nothing they or support could act on.
25
+ const customer = await em.findOne(CustomerAccount, {
26
+ email: normalizeEmailAddress(input.email),
27
+ });
28
+ if (!customer) {
29
+ // Generic error to avoid account enumeration.
30
+ throw new HttpError(401, ERROR_CODES.INVALID_CREDENTIALS, 'Invalid email or password.');
31
+ }
32
+ if (customer.deletedAt) {
33
+ throw new HttpError(401, ERROR_CODES.INVALID_CREDENTIALS, 'Invalid email or password.');
34
+ }
35
+ // Feature 040 — a blocked account cannot log in (FR-012/FR-016). The
36
+ // distinct error lets the storefront show a clear "account blocked" message.
37
+ if (customer.blockedAt) {
38
+ throw new HttpError(403, ERROR_CODES.ACCOUNT_BLOCKED, 'This account has been blocked. Please contact support.');
39
+ }
40
+ const ok = await verifyPassword(customer.passwordHash, input.password);
41
+ if (!ok) {
42
+ throw new HttpError(401, ERROR_CODES.INVALID_CREDENTIALS, 'Invalid email or password.');
43
+ }
44
+ // Second factor (feature 042). No session is issued until it succeeds.
45
+ const mfaPort = this.getMfaLoginPort?.();
46
+ if (mfaPort) {
47
+ const decision = await mfaPort.beginLogin({ subjectType: 'customer', subjectId: customer.id }, {
48
+ salesChannelId: input.salesChannelId ?? null,
49
+ organizationId: customer.organizationId ?? null,
50
+ });
51
+ if (decision.kind === 'challenge') {
52
+ return { status: 'mfaRequired', challengeId: decision.challengeId };
53
+ }
54
+ if (decision.kind === 'setup') {
55
+ return { status: 'mfaSetupRequired', setupTicket: decision.setupTicket };
56
+ }
57
+ }
58
+ const session = await this.sessionService.createSession({
59
+ kind: 'customer',
60
+ customerAccountId: customer.id,
61
+ ...(input.ip !== undefined ? { ipAddress: input.ip } : {}),
62
+ ...(input.userAgent !== undefined ? { userAgent: input.userAgent } : {}),
63
+ });
64
+ // command-coverage-ignore: stamps lastLoginAt for the session — high-volume
65
+ // auth bookkeeping (session lifecycle is owned by SessionService), not an
66
+ // audited domain-state mutation.
67
+ customer.lastLoginAt = new Date();
68
+ await em.flush();
69
+ return {
70
+ status: 'authenticated',
71
+ customerAccount: customer,
72
+ sessionCookieValue: session.cookieValue,
73
+ sessionExpiresAt: session.expiresAt,
74
+ };
75
+ }
76
+ async changePassword(customerAccountId, currentPassword, newPassword) {
77
+ const em = this.emFactory();
78
+ const customer = await em.findOne(CustomerAccount, { id: customerAccountId });
79
+ if (!customer) {
80
+ throw new HttpError(401, ERROR_CODES.UNAUTHORIZED, 'Customer session required.');
81
+ }
82
+ const ok = await verifyPassword(customer.passwordHash, currentPassword);
83
+ if (!ok) {
84
+ throw new HttpError(401, ERROR_CODES.CURRENT_PASSWORD_INVALID, 'Current password is incorrect.');
85
+ }
86
+ customer.passwordHash = await hashPassword(newPassword);
87
+ // Issue #222 — the holder proved the current password and chose the new
88
+ // one, so the account has a password on record whatever it had before.
89
+ customer.passwordSetAt = new Date();
90
+ if (this.auditLog) {
91
+ recordAuditFromContext(this.auditLog, em, {
92
+ action: 'customer_account.change_password',
93
+ objectType: 'customer_account',
94
+ objectId: customer.id,
95
+ stateBefore: null,
96
+ stateAfter: { via: 'self_service' },
97
+ });
98
+ }
99
+ await em.flush();
100
+ }
101
+ async logout(sessionId) {
102
+ await this.sessionService.destroySession(sessionId);
103
+ }
104
+ }
105
+ //# sourceMappingURL=customer-auth-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"customer-auth-service.js","sourceRoot":"","sources":["../../../src/backend/services/customer-auth-service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql';
2
+ import type { CustomerGroupReadPort, CustomerGroupRecord } from '@endora-commerce/contracts';
3
+ import { CustomerGroup } from '../entities/customer-group.entity.js';
4
+ /**
5
+ * The row-level read model `customer_accounts` publishes over customer groups.
6
+ *
7
+ * It arrived here with the entity (feature 076, D-79) from
8
+ * `price_lists/services/price-list-read-port.ts`, unchanged in shape: the
9
+ * container name, the interface and its three methods are what they were, so
10
+ * `pwa/backend.ts` did not have to be touched and the port check's before and
11
+ * after are directly comparable.
12
+ *
13
+ * `price_lists` is now a consumer rather than the provider — the pricing
14
+ * rule-target picker and the rule-target validation read it.
15
+ */
16
+ export declare class CustomerGroupReadService implements CustomerGroupReadPort {
17
+ private readonly emFactory;
18
+ constructor(emFactory: () => EntityManager);
19
+ findById(id: string): Promise<CustomerGroupRecord | null>;
20
+ findByIds(ids: readonly string[]): Promise<CustomerGroupRecord[]>;
21
+ listAll(): Promise<CustomerGroupRecord[]>;
22
+ }
23
+ export declare function toCustomerGroupRecord(group: CustomerGroup): CustomerGroupRecord;
24
+ //# sourceMappingURL=customer-group-read-port.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"customer-group-read-port.d.ts","sourceRoot":"","sources":["../../../src/backend/services/customer-group-read-port.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EAAE,qBAAqB,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAC7F,OAAO,EAAE,aAAa,EAAE,MAAM,sCAAsC,CAAC;AAErE;;;;;;;;;;;GAWG;AACH,qBAAa,wBAAyB,YAAW,qBAAqB;IACxD,OAAO,CAAC,QAAQ,CAAC,SAAS;gBAAT,SAAS,EAAE,MAAM,aAAa;IAErD,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC;IAKzD,SAAS,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,mBAAmB,EAAE,CAAC;IAMjE,OAAO,IAAI,OAAO,CAAC,mBAAmB,EAAE,CAAC;CAIhD;AAED,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,aAAa,GAAG,mBAAmB,CAS/E"}
@@ -0,0 +1,44 @@
1
+ import { CustomerGroup } from '../entities/customer-group.entity.js';
2
+ /**
3
+ * The row-level read model `customer_accounts` publishes over customer groups.
4
+ *
5
+ * It arrived here with the entity (feature 076, D-79) from
6
+ * `price_lists/services/price-list-read-port.ts`, unchanged in shape: the
7
+ * container name, the interface and its three methods are what they were, so
8
+ * `pwa/backend.ts` did not have to be touched and the port check's before and
9
+ * after are directly comparable.
10
+ *
11
+ * `price_lists` is now a consumer rather than the provider — the pricing
12
+ * rule-target picker and the rule-target validation read it.
13
+ */
14
+ export class CustomerGroupReadService {
15
+ emFactory;
16
+ constructor(emFactory) {
17
+ this.emFactory = emFactory;
18
+ }
19
+ async findById(id) {
20
+ const group = await this.emFactory().findOne(CustomerGroup, { id });
21
+ return group ? toCustomerGroupRecord(group) : null;
22
+ }
23
+ async findByIds(ids) {
24
+ if (ids.length === 0)
25
+ return [];
26
+ const groups = await this.emFactory().find(CustomerGroup, { id: { $in: [...ids] } });
27
+ return groups.map(toCustomerGroupRecord);
28
+ }
29
+ async listAll() {
30
+ const groups = await this.emFactory().find(CustomerGroup, {}, { orderBy: { code: 'asc' } });
31
+ return groups.map(toCustomerGroupRecord);
32
+ }
33
+ }
34
+ export function toCustomerGroupRecord(group) {
35
+ return {
36
+ id: group.id,
37
+ code: group.code,
38
+ name: group.name,
39
+ description: group.description ?? null,
40
+ createdAt: group.createdAt,
41
+ updatedAt: group.updatedAt,
42
+ };
43
+ }
44
+ //# sourceMappingURL=customer-group-read-port.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"customer-group-read-port.js","sourceRoot":"","sources":["../../../src/backend/services/customer-group-read-port.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAE,MAAM,sCAAsC,CAAC;AAErE;;;;;;;;;;;GAWG;AACH,MAAM,OAAO,wBAAwB;IACN;IAA7B,YAA6B,SAA8B;QAA9B,cAAS,GAAT,SAAS,CAAqB;IAAG,CAAC;IAE/D,KAAK,CAAC,QAAQ,CAAC,EAAU;QACvB,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,SAAS,EAAE,CAAC,OAAO,CAAC,aAAa,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC;QACpE,OAAO,KAAK,CAAC,CAAC,CAAC,qBAAqB,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACrD,CAAC;IAED,KAAK,CAAC,SAAS,CAAC,GAAsB;QACpC,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,EAAE,CAAC;QAChC,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,SAAS,EAAE,CAAC,IAAI,CAAC,aAAa,EAAE,EAAE,EAAE,EAAE,EAAE,GAAG,EAAE,CAAC,GAAG,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC;QACrF,OAAO,MAAM,CAAC,GAAG,CAAC,qBAAqB,CAAC,CAAC;IAC3C,CAAC;IAED,KAAK,CAAC,OAAO;QACX,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,SAAS,EAAE,CAAC,IAAI,CAAC,aAAa,EAAE,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC;QAC5F,OAAO,MAAM,CAAC,GAAG,CAAC,qBAAqB,CAAC,CAAC;IAC3C,CAAC;CACF;AAED,MAAM,UAAU,qBAAqB,CAAC,KAAoB;IACxD,OAAO;QACL,EAAE,EAAE,KAAK,CAAC,EAAE;QACZ,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,WAAW,EAAE,KAAK,CAAC,WAAW,IAAI,IAAI;QACtC,SAAS,EAAE,KAAK,CAAC,SAAS;QAC1B,SAAS,EAAE,KAAK,CAAC,SAAS;KAC3B,CAAC;AACJ,CAAC"}
@@ -0,0 +1,21 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql';
2
+ import type { CommandBus } from '@endora-commerce/platform/commands';
3
+ import { CustomerGroup } from '../entities/customer-group.entity.js';
4
+ export declare class CustomerGroupService {
5
+ #private;
6
+ private readonly emFactory;
7
+ /** Feature 054 — audits customer-group writes co-transactionally when provided. */
8
+ private readonly commandBus?;
9
+ constructor(emFactory: () => EntityManager,
10
+ /** Feature 054 — audits customer-group writes co-transactionally when provided. */
11
+ commandBus?: CommandBus | undefined);
12
+ list(): Promise<CustomerGroup[]>;
13
+ getById(id: string): Promise<CustomerGroup>;
14
+ upsertByCode(input: {
15
+ code: string;
16
+ name: string;
17
+ description?: string | null | undefined;
18
+ }): Promise<CustomerGroup>;
19
+ remove(id: string): Promise<void>;
20
+ }
21
+ //# sourceMappingURL=customer-group-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"customer-group-service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/customer-group-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAG3D,OAAO,KAAK,EAAW,UAAU,EAAE,MAAM,oCAAoC,CAAC;AAC9E,OAAO,EAAE,aAAa,EAAE,MAAM,sCAAsC,CAAC;AAErE,qBAAa,oBAAoB;;IAE7B,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,mFAAmF;IACnF,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC;gBAFX,SAAS,EAAE,MAAM,aAAa;IAC/C,mFAAmF;IAClE,UAAU,CAAC,EAAE,UAAU,YAAA;IAGpC,IAAI,IAAI,OAAO,CAAC,aAAa,EAAE,CAAC;IAIhC,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;IAM3C,YAAY,CAAC,KAAK,EAAE;QACxB,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,MAAM,CAAC;QACb,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;KACzC,GAAG,OAAO,CAAC,aAAa,CAAC;IA4DpB,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAyBxC"}
@@ -0,0 +1,97 @@
1
+ import { ERROR_CODES } from '@endora-commerce/contracts';
2
+ import { HttpError } from '@endora-commerce/platform/http';
3
+ import { CustomerGroup } from '../entities/customer-group.entity.js';
4
+ export class CustomerGroupService {
5
+ emFactory;
6
+ commandBus;
7
+ constructor(emFactory,
8
+ /** Feature 054 — audits customer-group writes co-transactionally when provided. */
9
+ commandBus) {
10
+ this.emFactory = emFactory;
11
+ this.commandBus = commandBus;
12
+ }
13
+ async list() {
14
+ return this.emFactory().find(CustomerGroup, {}, { orderBy: { code: 'asc' } });
15
+ }
16
+ async getById(id) {
17
+ const row = await this.emFactory().findOne(CustomerGroup, { id });
18
+ if (!row)
19
+ throw new HttpError(404, ERROR_CODES.NOT_FOUND, `Customer group ${id} not found.`);
20
+ return row;
21
+ }
22
+ async upsertByCode(input) {
23
+ if (this.commandBus)
24
+ return this.commandBus.run(this.#upsertCommand(input));
25
+ const em = this.emFactory();
26
+ const r = await this.#applyUpsert(em, input);
27
+ await em.flush();
28
+ return r.row;
29
+ }
30
+ #upsertCommand(input) {
31
+ return {
32
+ action: 'customer_group.upsert',
33
+ objectType: 'customer_group',
34
+ objectId: input.code,
35
+ run: async ({ em }) => {
36
+ const r = await this.#applyUpsert(em, input);
37
+ return { result: r.row, before: r.before, after: r.after };
38
+ },
39
+ };
40
+ }
41
+ /** Pure upsert on the given em — no flush, no audit. */
42
+ async #applyUpsert(em, input) {
43
+ const existing = await em.findOne(CustomerGroup, { code: input.code });
44
+ if (existing) {
45
+ const before = {
46
+ code: existing.code,
47
+ name: existing.name,
48
+ description: existing.description ?? null,
49
+ };
50
+ existing.name = input.name;
51
+ if (input.description !== undefined)
52
+ existing.description = input.description ?? null;
53
+ return {
54
+ row: existing,
55
+ before,
56
+ after: { code: existing.code, name: existing.name, description: existing.description ?? null },
57
+ };
58
+ }
59
+ const row = em.create(CustomerGroup, {
60
+ code: input.code,
61
+ name: input.name,
62
+ ...(input.description !== undefined ? { description: input.description ?? null } : {}),
63
+ });
64
+ return {
65
+ row,
66
+ before: null,
67
+ after: { code: row.code, name: row.name, description: row.description ?? null },
68
+ };
69
+ }
70
+ async remove(id) {
71
+ if (this.commandBus) {
72
+ await this.commandBus.run(this.#removeCommand(id));
73
+ return;
74
+ }
75
+ const em = this.emFactory();
76
+ const row = await em.findOne(CustomerGroup, { id });
77
+ if (!row)
78
+ return;
79
+ await em.removeAndFlush(row);
80
+ }
81
+ #removeCommand(id) {
82
+ return {
83
+ action: 'customer_group.delete',
84
+ objectType: 'customer_group',
85
+ objectId: id,
86
+ run: async ({ em }) => {
87
+ const row = await em.findOne(CustomerGroup, { id });
88
+ if (!row)
89
+ return { result: undefined, skipAudit: true };
90
+ const before = { code: row.code, name: row.name };
91
+ em.remove(row);
92
+ return { result: undefined, before, after: null };
93
+ },
94
+ };
95
+ }
96
+ }
97
+ //# sourceMappingURL=customer-group-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"customer-group-service.js","sourceRoot":"","sources":["../../../src/backend/services/customer-group-service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,30 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql';
2
+ import type { CustomerRollupScopePort } from '@endora-commerce/contracts';
3
+ /**
4
+ * Feature 056 (T032) — customer roll-up scope derivation.
5
+ *
6
+ * Resolves, server-side, whether a customer login widens from single-org to its
7
+ * organization's subtree (Principle XI — derived from the authenticated actor,
8
+ * never request inputs). Returns the subtree id set when the customer holds the
9
+ * `subtreeRollupEnabled` capability, else `undefined` (stay single-org).
10
+ *
11
+ * Lives in the `customer_accounts` module (owner of `CustomerAccount`) and takes
12
+ * the tree traversal as an injected argument (`subtreeIds`) so it never reaches
13
+ * into the organizations module's internals (Principle I). The capability read
14
+ * runs under a system scope because the tenant context is being *established*
15
+ * by the caller — it does not exist yet (mirrors the auth org resolver).
16
+ *
17
+ * **Feature 080 (T040b) — a published port rather than an exported function.**
18
+ * Its only two callers are the composition roots, which used to import this
19
+ * file: a value import of a module's source, which is the spelling that stops
20
+ * existing the day the module becomes a package (D-160.6.1). The rule is
21
+ * unchanged and so is the query, including the single-column projection and the
22
+ * system scope; what changed is that the roots resolve it from the container
23
+ * they already composed.
24
+ */
25
+ export declare class CustomerRollupScopeService implements CustomerRollupScopePort {
26
+ private readonly emFactory;
27
+ constructor(emFactory: () => EntityManager);
28
+ resolveSubtreeIds(customerAccountId: string, organizationId: string | null, subtreeIds: (organizationId: string) => Promise<string[]>): Promise<string[] | undefined>;
29
+ }
30
+ //# sourceMappingURL=customer-rollup-scope.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"customer-rollup-scope.d.ts","sourceRoot":"","sources":["../../../src/backend/services/customer-rollup-scope.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,4BAA4B,CAAC;AAI1E;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,qBAAa,0BAA2B,YAAW,uBAAuB;IAC5D,OAAO,CAAC,QAAQ,CAAC,SAAS;gBAAT,SAAS,EAAE,MAAM,aAAa;IAErD,iBAAiB,CACrB,iBAAiB,EAAE,MAAM,EACzB,cAAc,EAAE,MAAM,GAAG,IAAI,EAC7B,UAAU,EAAE,CAAC,cAAc,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,GACxD,OAAO,CAAC,MAAM,EAAE,GAAG,SAAS,CAAC;CAajC"}
@@ -0,0 +1,42 @@
1
+ import { withSystemScope } from '@endora-commerce/platform/tenancy';
2
+ import { CustomerAccount } from '../entities/customer-account.entity.js';
3
+ /**
4
+ * Feature 056 (T032) — customer roll-up scope derivation.
5
+ *
6
+ * Resolves, server-side, whether a customer login widens from single-org to its
7
+ * organization's subtree (Principle XI — derived from the authenticated actor,
8
+ * never request inputs). Returns the subtree id set when the customer holds the
9
+ * `subtreeRollupEnabled` capability, else `undefined` (stay single-org).
10
+ *
11
+ * Lives in the `customer_accounts` module (owner of `CustomerAccount`) and takes
12
+ * the tree traversal as an injected argument (`subtreeIds`) so it never reaches
13
+ * into the organizations module's internals (Principle I). The capability read
14
+ * runs under a system scope because the tenant context is being *established*
15
+ * by the caller — it does not exist yet (mirrors the auth org resolver).
16
+ *
17
+ * **Feature 080 (T040b) — a published port rather than an exported function.**
18
+ * Its only two callers are the composition roots, which used to import this
19
+ * file: a value import of a module's source, which is the spelling that stops
20
+ * existing the day the module becomes a package (D-160.6.1). The rule is
21
+ * unchanged and so is the query, including the single-column projection and the
22
+ * system scope; what changed is that the roots resolve it from the container
23
+ * they already composed.
24
+ */
25
+ export class CustomerRollupScopeService {
26
+ emFactory;
27
+ constructor(emFactory) {
28
+ this.emFactory = emFactory;
29
+ }
30
+ async resolveSubtreeIds(customerAccountId, organizationId, subtreeIds) {
31
+ if (!organizationId)
32
+ return undefined;
33
+ const enabled = await withSystemScope('tenant: resolve customer roll-up flag', async () => {
34
+ const account = await this.emFactory().findOne(CustomerAccount, { id: customerAccountId }, { fields: ['subtreeRollupEnabled'] });
35
+ return account?.subtreeRollupEnabled ?? false;
36
+ });
37
+ if (!enabled)
38
+ return undefined;
39
+ return subtreeIds(organizationId);
40
+ }
41
+ }
42
+ //# sourceMappingURL=customer-rollup-scope.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"customer-rollup-scope.js","sourceRoot":"","sources":["../../../src/backend/services/customer-rollup-scope.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AACpE,OAAO,EAAE,eAAe,EAAE,MAAM,wCAAwC,CAAC;AAEzE;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,OAAO,0BAA0B;IACR;IAA7B,YAA6B,SAA8B;QAA9B,cAAS,GAAT,SAAS,CAAqB;IAAG,CAAC;IAE/D,KAAK,CAAC,iBAAiB,CACrB,iBAAyB,EACzB,cAA6B,EAC7B,UAAyD;QAEzD,IAAI,CAAC,cAAc;YAAE,OAAO,SAAS,CAAC;QACtC,MAAM,OAAO,GAAG,MAAM,eAAe,CAAC,uCAAuC,EAAE,KAAK,IAAI,EAAE;YACxF,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,SAAS,EAAE,CAAC,OAAO,CAC5C,eAAe,EACf,EAAE,EAAE,EAAE,iBAAiB,EAAE,EACzB,EAAE,MAAM,EAAE,CAAC,sBAAsB,CAAC,EAAE,CACrC,CAAC;YACF,OAAO,OAAO,EAAE,oBAAoB,IAAI,KAAK,CAAC;QAChD,CAAC,CAAC,CAAC;QACH,IAAI,CAAC,OAAO;YAAE,OAAO,SAAS,CAAC;QAC/B,OAAO,UAAU,CAAC,cAAc,CAAC,CAAC;IACpC,CAAC;CACF"}
@@ -0,0 +1,15 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql';
2
+ import type { AuditPort } from '@endora-commerce/platform/kernel';
3
+ export declare class PasswordResetService {
4
+ private readonly emFactory;
5
+ private readonly auditLog?;
6
+ constructor(emFactory: () => EntityManager, auditLog?: AuditPort | undefined);
7
+ /** Returns the raw token only when the email matched a real account. Caller emails it. */
8
+ requestReset(email: string): Promise<{
9
+ rawToken: string | null;
10
+ }>;
11
+ confirmReset(rawToken: string, newPassword: string): Promise<void>;
12
+ /** Test-only — returns the latest unconsumed token id for probe tests. */
13
+ latestUnconsumedTokenId(): Promise<string | null>;
14
+ }
15
+ //# sourceMappingURL=password-reset-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"password-reset-service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/password-reset-service.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAS3D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kCAAkC,CAAC;AAclE,qBAAa,oBAAoB;IAE7B,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC;gBADT,SAAS,EAAE,MAAM,aAAa,EAC9B,QAAQ,CAAC,EAAE,SAAS,YAAA;IAGvC,0FAA0F;IACpF,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;IAyBjE,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAqCxE,0EAA0E;IACpE,uBAAuB,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;CASxD"}
@@ -0,0 +1,90 @@
1
+ import { createHash, randomBytes } from 'crypto';
2
+ import { ERROR_CODES, normalizeEmailAddress } from '@endora-commerce/contracts';
3
+ import { HttpError } from '@endora-commerce/platform/http';
4
+ // Feature 075, Phase C — a pure function over its argument, so it lives in the
5
+ // kernel rather than behind a gate that would answer 503 to "hash this string".
6
+ import { hashPassword } from '@endora-commerce/platform/kernel';
7
+ import { CustomerAccount } from '../entities/customer-account.entity.js';
8
+ import { PasswordResetToken } from '../entities/password-reset-token.entity.js';
9
+ import { recordAuditFromContext } from '@endora-commerce/platform/commands';
10
+ /**
11
+ * Password reset flow (FR-045 / T119).
12
+ *
13
+ * - request: always returns success regardless of account existence
14
+ * (account-enumeration defense). Issues a sha256-hashed token with a
15
+ * short TTL (1 hour). The raw token is returned for the email body.
16
+ * - confirm: validates token (one-shot, expiry), rehashes the new password,
17
+ * marks the token consumed.
18
+ */
19
+ const TOKEN_TTL_HOURS = 1;
20
+ export class PasswordResetService {
21
+ emFactory;
22
+ auditLog;
23
+ constructor(emFactory, auditLog) {
24
+ this.emFactory = emFactory;
25
+ this.auditLog = auditLog;
26
+ }
27
+ /** Returns the raw token only when the email matched a real account. Caller emails it. */
28
+ async requestReset(email) {
29
+ // command-coverage-ignore: issues a short-TTL, one-shot reset token (rate-
30
+ // limited, account-enumeration-safe); the actual password change is audited
31
+ // at confirmReset (customer_account.password_reset).
32
+ const em = this.emFactory();
33
+ // Folded like every other lookup on this table (see `normalizeEmailAddress`).
34
+ // An unmatched address is indistinguishable from an unknown one here — the
35
+ // method answers `{ rawToken: null }` to both — so a case-sensitive compare
36
+ // silently denied a reset to exactly the accounts that most needed one.
37
+ const customer = await em.findOne(CustomerAccount, {
38
+ email: normalizeEmailAddress(email),
39
+ deletedAt: null,
40
+ });
41
+ if (!customer)
42
+ return { rawToken: null };
43
+ const rawToken = randomBytes(32).toString('base64url');
44
+ const token = em.create(PasswordResetToken, {
45
+ customerAccountId: customer.id,
46
+ tokenHash: sha256Hex(rawToken),
47
+ expiresAt: new Date(Date.now() + TOKEN_TTL_HOURS * 60 * 60 * 1_000),
48
+ });
49
+ await em.persistAndFlush(token);
50
+ return { rawToken };
51
+ }
52
+ async confirmReset(rawToken, newPassword) {
53
+ const em = this.emFactory();
54
+ const token = await em.findOne(PasswordResetToken, { tokenHash: sha256Hex(rawToken) });
55
+ if (!token || token.consumedAt || token.expiresAt.getTime() <= Date.now()) {
56
+ throw new HttpError(400, ERROR_CODES.TOKEN_INVALID_OR_EXPIRED, 'Reset token is invalid or has expired.');
57
+ }
58
+ const customer = await em.findOne(CustomerAccount, { id: token.customerAccountId });
59
+ if (!customer) {
60
+ throw new HttpError(400, ERROR_CODES.TOKEN_INVALID_OR_EXPIRED, 'Associated account no longer exists.');
61
+ }
62
+ customer.passwordHash = await hashPassword(newPassword);
63
+ // Issue #222 — the holder proved control of the address and chose the
64
+ // password themselves, so the account has one on record from here on. This
65
+ // is also the one route into that state for an account federated sign-in
66
+ // created: it never knew a current password to change.
67
+ customer.passwordSetAt = new Date();
68
+ token.consumedAt = new Date();
69
+ if (this.auditLog) {
70
+ recordAuditFromContext(this.auditLog, em, {
71
+ action: 'customer_account.password_reset',
72
+ objectType: 'customer_account',
73
+ objectId: customer.id,
74
+ stateBefore: null,
75
+ stateAfter: { via: 'reset_token' },
76
+ });
77
+ }
78
+ await em.flush();
79
+ }
80
+ /** Test-only — returns the latest unconsumed token id for probe tests. */
81
+ async latestUnconsumedTokenId() {
82
+ const em = this.emFactory();
83
+ const row = await em.findOne(PasswordResetToken, { consumedAt: null }, { orderBy: { createdAt: 'desc' } });
84
+ return row?.id ?? null;
85
+ }
86
+ }
87
+ function sha256Hex(input) {
88
+ return createHash('sha256').update(input, 'utf8').digest('hex');
89
+ }
90
+ //# sourceMappingURL=password-reset-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"password-reset-service.js","sourceRoot":"","sources":["../../../src/backend/services/password-reset-service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql';
2
+ import { CustomerAccount } from '../entities/customer-account.entity.js';
3
+ import type { AuditPort } from '@endora-commerce/platform/kernel';
4
+ /**
5
+ * RoleService (T174).
6
+ *
7
+ * Members management on the buyer side. Two safety guards:
8
+ * - CANNOT_REMOVE_LAST_ADMIN — DELETE on the only `organization_admin`
9
+ * member of an org returns 409.
10
+ * - CANNOT_DEMOTE_LAST_ADMIN — PATCH /role downgrading the only
11
+ * `organization_admin` returns 409.
12
+ */
13
+ export declare class RoleService {
14
+ #private;
15
+ private readonly emFactory;
16
+ private readonly auditLog?;
17
+ constructor(emFactory: () => EntityManager, auditLog?: AuditPort | undefined);
18
+ listMembers(organizationId: string): Promise<CustomerAccount[]>;
19
+ changeRole(organizationId: string, targetCustomerAccountId: string, newRole: 'organization_admin' | 'regular_user'): Promise<CustomerAccount>;
20
+ removeMember(organizationId: string, targetCustomerAccountId: string): Promise<void>;
21
+ }
22
+ //# sourceMappingURL=role-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"role-service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/role-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAG3D,OAAO,EAAE,eAAe,EAAE,MAAM,wCAAwC,CAAC;AAEzE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kCAAkC,CAAC;AAElE;;;;;;;;GAQG;AACH,qBAAa,WAAW;;IAEpB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC;gBADT,SAAS,EAAE,MAAM,aAAa,EAC9B,QAAQ,CAAC,EAAE,SAAS,YAAA;IAqBjC,WAAW,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC;IAS/D,UAAU,CACd,cAAc,EAAE,MAAM,EACtB,uBAAuB,EAAE,MAAM,EAC/B,OAAO,EAAE,oBAAoB,GAAG,cAAc,GAC7C,OAAO,CAAC,eAAe,CAAC;IA4BrB,YAAY,CAAC,cAAc,EAAE,MAAM,EAAE,uBAAuB,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAgC3F"}