@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,407 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { z } from 'zod';
3
+ import { normalizeEmailAddress } from '@endora-commerce/contracts';
4
+ import { recordAuditFromContext } from '@endora-commerce/platform/commands';
5
+ import { withSystemScope } from '@endora-commerce/platform/tenancy';
6
+ import { lazyPort } from '@endora-commerce/platform/kernel';
7
+ import { registerCustomerAccountAuditReferences } from './services/audit-references.js';
8
+ import { effectiveState } from '@endora-commerce/platform/kernel';
9
+ // Feature 075, Phase C — a pure function, so the kernel rather than `auth`.
10
+ import { hashPassword } from '@endora-commerce/platform/kernel';
11
+ import { CustomerAccount } from './entities/customer-account.entity.js';
12
+ import { CustomerGroup } from './entities/customer-group.entity.js';
13
+ import { PasswordResetToken } from './entities/password-reset-token.entity.js';
14
+ import { registerCustomerGroupAdminRoutes } from './routes.admin.js';
15
+ import { CustomerAccountMemberWriteService, CustomerAccountReadService, CustomerPasswordStateService, CustomerPasswordVerificationService, createCustomerAuthPort, createCustomerRolePort, } from './services/customer-account-ports.js';
16
+ import { CustomerAccountAdminSearchService, CustomerAccountLifecycleWriteService, } from './services/customer-account-lifecycle-ports.js';
17
+ import { CustomerAuthService } from './services/customer-auth-service.js';
18
+ import { CustomerGroupReadService } from './services/customer-group-read-port.js';
19
+ import { CustomerGroupService } from './services/customer-group-service.js';
20
+ import { CustomerRollupScopeService } from './services/customer-rollup-scope.js';
21
+ import { PasswordResetService } from './services/password-reset-service.js';
22
+ import { RoleService } from './services/role-service.js';
23
+ export function registerModule(ctx) {
24
+ /**
25
+ * Feature 120 (FR-015) — the sales-channel membership bridge this module owns.
26
+ *
27
+ * The platform used to hold a map **total** over
28
+ * `ChannelMemberEntityTypeSchema`, naming this module's bridge table from
29
+ * inside the kernel. That is D-52/D-53's prohibition expressed in a contract
30
+ * instead of an import, and it meant a membership call for 'customer' on an
31
+ * instance that never installed this module executed SQL against a relation
32
+ * that is not there (D-226). The module whose migration creates the table is
33
+ * the one that says where it is; a member nothing registers now refuses at
34
+ * the call instead.
35
+ *
36
+ * A **contribution** hook and deliberately **unprobed** (D-62/D-68): a
37
+ * registration is an inert statement about the schema rather than a
38
+ * capability, the rows outlive an operator switching this module off, and
39
+ * probing would make a runtime activation flip require a restart. It is
40
+ * declared first in this module so any later hook of its own that binds a
41
+ * membership finds the bridge already there.
42
+ */
43
+ ctx.onBoot(() => {
44
+ const { salesChannelBridgeRegistry } = ctx.cradle();
45
+ for (const bridge of salesChannelBridges)
46
+ salesChannelBridgeRegistry.register(bridge);
47
+ });
48
+ // D-96 — resolved once, called through on every login. The gate `mfa`'s
49
+ // `providePort` put on this name is transient, so the proxy asks about the
50
+ // module's effective state at each call rather than at composition.
51
+ const mfaLoginPort = lazyPort(ctx, 'mfaLoginPort');
52
+ // The live source of `twoFactorEnabled`, on the same terms as the login port
53
+ // above and for the same reason. This module's `two_factor_confirmed_at`
54
+ // column lost its only non-null writer when the superseded TOTP path was
55
+ // deleted, so the field derived from it was a provably constant `false` —
56
+ // including on `GET /api/v1/me/customer`, which is the buyer's own account
57
+ // page telling them their account is unprotected while it is not.
58
+ const mfaEnrolmentStatePort = lazyPort(ctx, 'mfaEnrolmentStatePort');
59
+ /**
60
+ * The degrade is performed by **not resolving**, exactly as the login port's
61
+ * getter below does it — presence is decided first and there is deliberately
62
+ * no `catch` anywhere near the port. A buyer's own account page is the wrong
63
+ * place for a 503, and `false` is not a substitute for the answer here: with
64
+ * `mfa` absent no sign-in asks for a second factor, so no account is
65
+ * protected by one. The sentence an operator is shown before the flip is this
66
+ * module's `degrades-without` entry for `mfaEnrolmentStatePort`.
67
+ */
68
+ const twoFactorEnrolments = async (customerAccountIds) => {
69
+ if (!effectiveState.isPresent('mfa'))
70
+ return new Set();
71
+ return new Set(await mfaEnrolmentStatePort.activeSubjectIds('customer', customerAccountIds));
72
+ };
73
+ // ---------------------------------------------------------------------------
74
+ // Feature 075, Phase P — the published surface.
75
+ //
76
+ // The three ports below this block already existed; what they lacked was a
77
+ // contract a consumer could name without naming a file in this directory.
78
+ // One of them (`passwordResetService`) already returns a plain shape, so it
79
+ // gains nothing but a type parameter, which is now the compile-time proof
80
+ // that it still satisfies what was published.
81
+ //
82
+ // The other two return the `CustomerAccount` **entity**, and an entity
83
+ // crossing a boundary is the problem this feature exists to remove — so they
84
+ // get record-returning siblings rather than a rename. `customerAuthPort` and
85
+ // `customerRolePort` are what the consumers rewired to — `customers` was the
86
+ // last of them, in issue #195 — and `customerAuthService` /
87
+ // `customerRoleService` stay registered because those two adapters are built
88
+ // over them.
89
+ // ---------------------------------------------------------------------------
90
+ ctx.di.providePort('customerAccountReadPort', ctx
91
+ .asFunction(({ emFactory }) => new CustomerAccountReadService(emFactory, twoFactorEnrolments))
92
+ .singleton());
93
+ /**
94
+ * Feature 075, Phase C — the member lifecycle `organizations` runs over this
95
+ * table. Published when that module was cut: it created and mutated the
96
+ * entity in seven places, and the argon2 hash it needed to do so came from an
97
+ * import of `auth`. Both are on this side of the port now, and each write
98
+ * records its own audit row, as `roleService` beside it always has.
99
+ */
100
+ ctx.di.providePort('customerAccountMemberWritePort', ctx
101
+ .asFunction(({ emFactory, auditLogService }) => new CustomerAccountMemberWriteService(emFactory, twoFactorEnrolments, auditLogService))
102
+ .singleton());
103
+ /**
104
+ * Feature 075, Phase C — the account lifecycle `customers` runs over this
105
+ * table, and the list its admin screen reads it with.
106
+ *
107
+ * `customers` is the management surface over these accounts and it ran both
108
+ * by holding the entity: block, unblock, soft-delete, restore, the retention
109
+ * sweep's scrub, the organisation and group assignments, standalone
110
+ * registration and the custom-field patch, plus the paginated admin query.
111
+ * The policy — who may act on whom, and the "an organisation keeps an
112
+ * administrator" guard — stayed on that side; the statement and its one audit
113
+ * row are here.
114
+ */
115
+ ctx.di.providePort('customerAccountLifecycleWritePort', ctx
116
+ .asFunction(({ emFactory, auditLogService, commandBus }) => new CustomerAccountLifecycleWriteService(emFactory, auditLogService, twoFactorEnrolments,
117
+ // D-178 — `lazyPort` rather than a captured value: the gate stays
118
+ // live inside this singleton and the seam is resolved per call. The
119
+ // proxy is what makes the capture legal at all.
120
+ lazyPort(ctx, 'personalOrganizationProvisionApi'), commandBus))
121
+ .singleton());
122
+ ctx.di.providePort('customerAccountAdminSearchPort', ctx
123
+ .asFunction(({ emFactory }) => new CustomerAccountAdminSearchService(emFactory, twoFactorEnrolments))
124
+ .singleton());
125
+ ctx.di.providePort('customerAuthPort', ctx
126
+ .asFunction(() => createCustomerAuthPort(() => ctx.cradle().customerAuthService, twoFactorEnrolments))
127
+ .singleton());
128
+ /**
129
+ * Issue #222 — whether a password its holder can use is on record. Read by
130
+ * `mfa` before it severs an account's last federated identity; nothing else
131
+ * needs it, which is why it is its own port rather than a field on
132
+ * `customerAccountReadPort`'s record.
133
+ */
134
+ /**
135
+ * Feature 080 (T040b) — the roll-up widening the per-request tenant-context
136
+ * builder asks about, published because both composition roots used to
137
+ * import the derivation out of this module's sources.
138
+ *
139
+ * The traversal arrives as an argument rather than being resolved here: the
140
+ * caller already holds `organizations`' tree service, and reading it from
141
+ * this side would be a cross-module resolution of a container name no
142
+ * contract publishes (`check:port-shape` signal 3).
143
+ */
144
+ ctx.di.providePort('customerRollupScopePort', ctx
145
+ .asFunction(({ emFactory }) => new CustomerRollupScopeService(emFactory))
146
+ .singleton());
147
+ ctx.di.providePort('customerPasswordStatePort', ctx
148
+ .asFunction(({ emFactory }) => new CustomerPasswordStateService(emFactory))
149
+ .singleton());
150
+ /**
151
+ * Feature 080 (T052) — step-up re-verification, for `mfa`. Both composition
152
+ * roots used to read `passwordHash` off this module's entity and compare it
153
+ * with the platform hasher themselves; the column and the comparison are
154
+ * this module's, so the boolean is what crosses.
155
+ *
156
+ * Its own port rather than a method on `customerPasswordStatePort` for the
157
+ * reason that port gives for existing at all: the two questions differ, and
158
+ * only one of them takes a caller-supplied secret.
159
+ */
160
+ ctx.di.providePort('customerPasswordVerificationPort', ctx
161
+ .asFunction(({ emFactory }) => new CustomerPasswordVerificationService(emFactory))
162
+ .singleton());
163
+ ctx.di.providePort('customerRolePort', ctx
164
+ .asFunction(() => createCustomerRolePort(() => ctx.cradle().customerRoleService, twoFactorEnrolments))
165
+ .singleton());
166
+ ctx.di.providePort('customerAuthService', ctx
167
+ .asFunction(({ emFactory, auditLogService }) => new CustomerAuthService(emFactory,
168
+ // Feature 075, Phase C — `auth`'s **published** session surface,
169
+ // where this used to resolve the `sessionService` registration and
170
+ // type itself against `auth`'s class. The port hands back a plain
171
+ // cookie payload; the `Session` entity no longer crosses.
172
+ lazyPort(ctx, 'authSessionPort'),
173
+ // D-96 — the degrade is performed by **not resolving**. The port's
174
+ // gate throws `ModuleDisabledError` when `mfa` is absent, and there
175
+ // is deliberately no `catch` anywhere near this: a caught gate is a
176
+ // fail-open degrade nobody declared. Deciding presence first is the
177
+ // `auth`/`api_keys` shape, and `CustomerAuthService`'s
178
+ // `if (mfaPort)` branch — the password-only fallback of feature 042
179
+ // FR-033 — is what `undefined` selects.
180
+ () => (effectiveState.isPresent('mfa') ? mfaLoginPort : undefined), auditLogService))
181
+ .singleton());
182
+ // The type parameter is the compile-time proof that this service still
183
+ // satisfies what feature 075 published; it returns plain shapes already, so
184
+ // it needed no adapter.
185
+ ctx.di.providePort('passwordResetService', ctx
186
+ .asFunction(({ emFactory, auditLogService }) => new PasswordResetService(emFactory, auditLogService))
187
+ .singleton());
188
+ ctx.di.providePort('customerRoleService', ctx
189
+ .asFunction(({ emFactory, auditLogService }) => new RoleService(emFactory, auditLogService))
190
+ .singleton());
191
+ ctx.di.providePort('customerOrgResolver', ctx
192
+ .asFunction(({ emFactory }) => async (customerAccountId) => {
193
+ // Runs in the auth hook, before a tenant context exists, so the
194
+ // read is system-scoped — identity resolution precedes tenancy.
195
+ return withSystemScope('auth: resolve customer org', async () => {
196
+ const customer = await emFactory().findOne(CustomerAccount, {
197
+ id: customerAccountId,
198
+ });
199
+ return customer?.organizationId ?? null;
200
+ });
201
+ })
202
+ .singleton());
203
+ /**
204
+ * Federated sign-in's two reads/writes of this module's table (T143a).
205
+ *
206
+ * A **port** rather than a contribution point (D-39): `autoCreate` decides
207
+ * whether an account may exist at all and then writes one. Both halves are
208
+ * system-scoped because they run *before* a tenant context exists — identity
209
+ * resolution precedes tenancy, exactly as `customerOrgResolver` above.
210
+ *
211
+ * The refusal is the return type rather than a throw: `null` is what
212
+ * `SocialIdentityService` reads as `registration_required`, so the policy
213
+ * lands on the caller as a redirect to the login screen and not as a 500.
214
+ *
215
+ * The setting is `customers.allow_registration_without_organization`, and it
216
+ * is named here even though `customers` owns it. That module already depends
217
+ * on this one, so a port pointing the other way would close a cycle; the
218
+ * platform-wide question "may an account exist without an Organization" is in
219
+ * any case about *this* table.
220
+ */
221
+ ctx.di.providePort('customerSocialLoginPort', ctx
222
+ .asFunction(({ emFactory, auditLogService }) => ({
223
+ async resolveByEmail(email) {
224
+ return withSystemScope('mfa: resolve customer by email', async () => {
225
+ // The address arrives from the provider's claim, in whatever case
226
+ // that provider chose to send it; the row is folded, so the compare
227
+ // is too. Unfolded, a returning buyer would be offered registration
228
+ // for an account they already have — and `autoCreate` below would
229
+ // then be refused by the unique index.
230
+ const customer = await emFactory().findOne(CustomerAccount, {
231
+ email: normalizeEmailAddress(email),
232
+ deletedAt: null,
233
+ });
234
+ return customer ? { id: customer.id } : null;
235
+ });
236
+ },
237
+ async autoCreate(email) {
238
+ const cradle = ctx.cradle();
239
+ // The channel a global-scope read resolves against. Both roots used
240
+ // to pass the literal `'default'` here, which is **not** a channel
241
+ // id: `sales_channel_id` is a uuid column, so the query threw
242
+ // `invalid input syntax for type uuid` on every call, the closure's
243
+ // `catch` read that as "not allowed", and federated sign-in could
244
+ // therefore never create an account in production however the
245
+ // operator had configured it. The harness had no gate at all and
246
+ // always created one. Neither was the behaviour anybody wanted.
247
+ const salesChannelId = await cradle.settingsChannelResolver();
248
+ if (salesChannelId === null)
249
+ return null;
250
+ let allowed = false;
251
+ try {
252
+ allowed = await cradle.settingsReadPort.get('customers.allow_registration_without_organization', salesChannelId, z.boolean());
253
+ }
254
+ catch {
255
+ // An unregistered or out-of-scope setting denies. Not a bare catch
256
+ // around a module port — `settingsReadPort` is kernel-owned and
257
+ // ungated, so there is no `ModuleDisabledError` to swallow here;
258
+ // the degrade is "no answer means no", which is the safe direction
259
+ // for a gate on account creation.
260
+ allowed = false;
261
+ }
262
+ if (!allowed)
263
+ return null;
264
+ const folded = normalizeEmailAddress(email);
265
+ const customerAccountId = randomUUID();
266
+ // No password was ever chosen for this account: it signs in through
267
+ // the provider. A random one keeps the column non-null without minting
268
+ // a credential anybody could guess — and issue #222 is the other half
269
+ // of that sentence: `passwordSetAt` stays null, so a reader can tell
270
+ // this account from one whose holder really has a password. Do not
271
+ // stamp it here.
272
+ const passwordHash = await hashPassword(randomUUID() + randomUUID());
273
+ const provision = lazyPort(ctx, 'personalOrganizationProvisionApi');
274
+ return withSystemScope('mfa: auto-create customer from social login', async () => emFactory().transactional(async (em) => {
275
+ /**
276
+ * D-178 — the personal organisation, in the same transaction as
277
+ * the account.
278
+ *
279
+ * This path provisioned **none**, so every account a federated
280
+ * sign-in created carried `organization_id = NULL` permanently:
281
+ * not a window but the steady state of a shipped feature, and the
282
+ * buyer reaching checkout was told to attach an Organization they
283
+ * had no way to attach. `customers`' self-registration provisioned
284
+ * one in a second unit of work; this one is a single transaction,
285
+ * which is what closes the window that shape leaves.
286
+ *
287
+ * The provider sends no name with the claim this seam reads, so
288
+ * the organisation is named from the address' local part — which
289
+ * is exactly what `PersonalOrganizationService.personalName` falls
290
+ * back to for a nameless account.
291
+ */
292
+ const organization = await provision.provisionFor(em, {
293
+ customerAccountId,
294
+ email: folded,
295
+ });
296
+ const account = em.create(CustomerAccount, {
297
+ id: customerAccountId,
298
+ email: folded,
299
+ passwordHash,
300
+ passwordSetAt: null,
301
+ firstName: '',
302
+ lastName: '',
303
+ role: 'regular_user',
304
+ organizationId: organization.id,
305
+ emailVerifiedAt: new Date(),
306
+ });
307
+ // The other way an account is created without an admin —
308
+ // `customers`' standalone self-registration — records this same
309
+ // shape with a null actor, and this path did not record anything at
310
+ // all until the coverage scan reached `backend.ts` (issue #122). An
311
+ // account appearing out of a federated sign-in is exactly the event
312
+ // an operator later needs to explain.
313
+ recordAuditFromContext(auditLogService, em, {
314
+ action: 'customer_account.register_social',
315
+ objectType: 'customer_account',
316
+ objectId: account.id,
317
+ stateBefore: null,
318
+ stateAfter: { email: account.email },
319
+ });
320
+ await em.flush();
321
+ return { id: account.id };
322
+ }));
323
+ },
324
+ }))
325
+ .singleton());
326
+ // ---------------------------------------------------------------------------
327
+ // Feature 076, D-79 — customer groups.
328
+ //
329
+ // The entity, its service, its read port and its three admin routes moved
330
+ // here from `price_lists`. A customer group describes the customer; a price
331
+ // list refers to one by id. Under the previous owner the single real foreign
332
+ // key into `customer_groups` — `customer_accounts.customer_group_id` — forced
333
+ // this module to declare `price_lists`, which made the reverse declaration a
334
+ // cycle `test/unit/db/module-graph.test.ts` refuses. The key is intra-module now and
335
+ // the class of problem is gone rather than routed around.
336
+ //
337
+ // `customerGroupService` keeps its container name: both composition roots
338
+ // read it for the promotion Rule Builder's target picker, and renaming it
339
+ // would have churned two roots to encode an owner this file already states.
340
+ // ---------------------------------------------------------------------------
341
+ ctx.di.providePort('customerGroupService', ctx
342
+ .asFunction(({ emFactory, commandBus }) => new CustomerGroupService(emFactory, commandBus))
343
+ .singleton());
344
+ ctx.di.providePort('customerGroupReadPort', ctx
345
+ .asFunction(({ emFactory }) => new CustomerGroupReadService(emFactory))
346
+ .singleton());
347
+ // The module's only routes. They are the customer-group admin surface and
348
+ // nothing else — customer login, registration and self-service stay with
349
+ // `organizations` and `customers`, which own those screens.
350
+ ctx.routes(async (app) => {
351
+ await registerCustomerGroupAdminRoutes(app, {
352
+ customerGroupService: () => ctx.cradle().customerGroupService,
353
+ requireAdmin: (permission) => async (req, reply) => ctx.cradle().requireAdmin(permission)(req, reply),
354
+ });
355
+ });
356
+ /**
357
+ * What an audit row about an impersonated customer is called, and where the admin app shows it
358
+ * (feature 075, D-87 drain).
359
+ *
360
+ * `audit_logs` used to answer both by hand — one SQL statement naming this
361
+ * module's table, and this module's admin route spelled into its own switch.
362
+ * A read port would have been the wrong repair: `audit_logs` is a
363
+ * cross-cutting reader, and five ports into it would be five edges pointing
364
+ * from the record towards the things it records. One of the five contributors
365
+ * (`inventory`) is switchable, and `audit_logs` is `nonDeactivatable`, so that
366
+ * edge would also have taken the operator's switch away. A push costs nothing
367
+ * and reads the same for all five.
368
+ *
369
+ * A **contribution** hook: it pushes an inert resolver into
370
+ * `auditReferenceRegistry`, an ungated registry, and carries no presence probe
371
+ * (D-67/D-68). The registry's own enumeration policy is what drops this entry
372
+ * while the module is absent — probing here would make the drop survive a
373
+ * reactivation until the next restart.
374
+ */
375
+ ctx.onBoot(() => {
376
+ registerCustomerAccountAuditReferences(lazyPort(ctx, 'auditReferenceRegistry'), ctx.cradle().emFactory, lazyPort(ctx, 'organizationDetailsPort'));
377
+ });
378
+ }
379
+ /**
380
+ * The module's persisted entity classes, on the `./backend` subpath, as one
381
+ * array and **no named class export** (D-168).
382
+ *
383
+ * This is the shape the platform reads when the package is *installed*: the
384
+ * boot-time loader (`src/packages/package-runtime.ts`, `exported['entities']`)
385
+ * and the static declaration reader (`scripts/lib/package-declarations.ts`),
386
+ * which is the third source of `check:module-boundary`'s `table->owner` map and
387
+ * the package pass of `check-entity-tenant-classification`. A missing array is
388
+ * answered with `[]` — zero entities registered, no error anywhere.
389
+ *
390
+ * The order is the one `db/entities-registry.generated.ts` declared before this
391
+ * module became a package, so the registered set is the same list in the same
392
+ * sequence.
393
+ */
394
+ export const entities = [CustomerAccount, CustomerGroup, PasswordResetToken];
395
+ /**
396
+ * The sales-channel membership bridge this module owns (feature 120, FR-015).
397
+ *
398
+ * Exported as well as registered because the owning module is the only honest
399
+ * source for it: a caller that composes no platform — the bare-database test
400
+ * harness, which constructs the kernel's membership service directly — declares
401
+ * these from here rather than from a list of nine it keeps itself, which is
402
+ * exactly the total map this feature deleted.
403
+ */
404
+ export const salesChannelBridges = [
405
+ { entityType: 'customer', table: 'sales_channel_customer_accounts', entityIdColumn: 'customer_account_id' },
406
+ ];
407
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/backend/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ import type { RequireAdminFactory } from '@endora-commerce/platform/kernel';
3
+ import type { CustomerGroupService } from './services/customer-group-service.js';
4
+ /**
5
+ * The customer-group admin surface (feature 076, D-79).
6
+ *
7
+ * Three routes, at the paths they have always had — `price_lists` served them
8
+ * until this module took the entity over, and the admin SPA's pickers call the
9
+ * same URLs with the same payloads. What changed is the gate: `catalog:write`
10
+ * was the pricing module's permission and is the wrong question to ask about a
11
+ * customer's segmentation, so the surface is gated by the two codes this module
12
+ * declares in its manifest. An operator's role has to be granted them; there is
13
+ * no silent inheritance from `catalog:write`.
14
+ */
15
+ export interface CustomerGroupAdminDeps {
16
+ /**
17
+ * Resolved per call rather than captured: the registration is a gated port,
18
+ * and a gate captured at boot keeps answering whatever the effective state
19
+ * later says. The module is non-deactivatable, so the gate is unreachable —
20
+ * which is a reason to be uniform, not a reason to be careless.
21
+ */
22
+ customerGroupService: () => CustomerGroupService;
23
+ requireAdmin: RequireAdminFactory;
24
+ }
25
+ export declare function registerCustomerGroupAdminRoutes(app: FastifyInstance, deps: CustomerGroupAdminDeps): Promise<void>;
26
+ //# 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,MAAM,SAAS,CAAC;AAE/C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAE5E,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,sCAAsC,CAAC;AAEjF;;;;;;;;;;GAUG;AACH,MAAM,WAAW,sBAAsB;IACrC;;;;;OAKG;IACH,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;IACjD,YAAY,EAAE,mBAAmB,CAAC;CACnC;AAED,wBAAsB,gCAAgC,CACpD,GAAG,EAAE,eAAe,EACpB,IAAI,EAAE,sBAAsB,GAC3B,OAAO,CAAC,IAAI,CAAC,CAqCf"}
@@ -0,0 +1,35 @@
1
+ import { upsertCustomerGroupRequestSchema } from '@endora-commerce/contracts';
2
+ export async function registerCustomerGroupAdminRoutes(app, deps) {
3
+ const { customerGroupService, requireAdmin } = deps;
4
+ app.get('/api/v1/admin/customer-groups', { preHandler: requireAdmin('customer_groups:read') }, async () => {
5
+ const rows = await customerGroupService().list();
6
+ return { data: rows.map(serializeCustomerGroup) };
7
+ });
8
+ app.put('/api/v1/admin/customer-groups/:code', {
9
+ preHandler: requireAdmin('customer_groups:write'),
10
+ schema: { body: upsertCustomerGroupRequestSchema },
11
+ }, async (request) => {
12
+ const body = upsertCustomerGroupRequestSchema.parse(request.body);
13
+ const row = await customerGroupService().upsertByCode({
14
+ code: request.params.code,
15
+ name: body.name,
16
+ ...(body.description !== undefined ? { description: body.description } : {}),
17
+ });
18
+ return { data: serializeCustomerGroup(row) };
19
+ });
20
+ app.delete('/api/v1/admin/customer-groups/:id', { preHandler: requireAdmin('customer_groups:write') }, async (request, reply) => {
21
+ await customerGroupService().remove(request.params.id);
22
+ return reply.status(204).send();
23
+ });
24
+ }
25
+ function serializeCustomerGroup(row) {
26
+ return {
27
+ id: row.id,
28
+ code: row.code,
29
+ name: row.name,
30
+ description: row.description ?? null,
31
+ createdAt: row.createdAt.toISOString(),
32
+ updatedAt: row.updatedAt.toISOString(),
33
+ };
34
+ }
35
+ //# sourceMappingURL=routes.admin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.admin.js","sourceRoot":"","sources":["../../src/backend/routes.admin.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gCAAgC,EAAE,MAAM,4BAA4B,CAAC;AA2B9E,MAAM,CAAC,KAAK,UAAU,gCAAgC,CACpD,GAAoB,EACpB,IAA4B;IAE5B,MAAM,EAAE,oBAAoB,EAAE,YAAY,EAAE,GAAG,IAAI,CAAC;IAEpD,GAAG,CAAC,GAAG,CACL,+BAA+B,EAC/B,EAAE,UAAU,EAAE,YAAY,CAAC,sBAAsB,CAAC,EAAE,EACpD,KAAK,IAAI,EAAE;QACT,MAAM,IAAI,GAAG,MAAM,oBAAoB,EAAE,CAAC,IAAI,EAAE,CAAC;QACjD,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC,sBAAsB,CAAC,EAAE,CAAC;IACpD,CAAC,CACF,CAAC;IAEF,GAAG,CAAC,GAAG,CACL,qCAAqC,EACrC;QACE,UAAU,EAAE,YAAY,CAAC,uBAAuB,CAAC;QACjD,MAAM,EAAE,EAAE,IAAI,EAAE,gCAAgC,EAAE;KACnD,EACD,KAAK,EAAE,OAAO,EAAE,EAAE;QAChB,MAAM,IAAI,GAAG,gCAAgC,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QAClE,MAAM,GAAG,GAAG,MAAM,oBAAoB,EAAE,CAAC,YAAY,CAAC;YACpD,IAAI,EAAE,OAAO,CAAC,MAAM,CAAC,IAAI;YACzB,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,GAAG,CAAC,IAAI,CAAC,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAC7E,CAAC,CAAC;QACH,OAAO,EAAE,IAAI,EAAE,sBAAsB,CAAC,GAAG,CAAC,EAAE,CAAC;IAC/C,CAAC,CACF,CAAC;IAEF,GAAG,CAAC,MAAM,CACR,mCAAmC,EACnC,EAAE,UAAU,EAAE,YAAY,CAAC,uBAAuB,CAAC,EAAE,EACrD,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE;QACvB,MAAM,oBAAoB,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QACvD,OAAO,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;IAClC,CAAC,CACF,CAAC;AACJ,CAAC;AAED,SAAS,sBAAsB,CAAC,GAAkB;IAChD,OAAO;QACL,EAAE,EAAE,GAAG,CAAC,EAAE;QACV,IAAI,EAAE,GAAG,CAAC,IAAI;QACd,IAAI,EAAE,GAAG,CAAC,IAAI;QACd,WAAW,EAAE,GAAG,CAAC,WAAW,IAAI,IAAI;QACpC,SAAS,EAAE,GAAG,CAAC,SAAS,CAAC,WAAW,EAAE;QACtC,SAAS,EAAE,GAAG,CAAC,SAAS,CAAC,WAAW,EAAE;KACvC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,22 @@
1
+ import type { AuditReferenceRegistryPort, OrganizationDetailsPort } from '@endora-commerce/contracts';
2
+ import type { EntityManager } from '@mikro-orm/postgresql';
3
+ /**
4
+ * What an impersonated customer is called on an audit row (feature 075, D-87
5
+ * drain).
6
+ *
7
+ * `audit_logs` used to answer this with a `customer_accounts left join
8
+ * organizations` written into its own service — one statement crossing two
9
+ * module boundaries. The organisation name is still preferred over the account
10
+ * e-mail, because that is what an operator reading "acted as …" recognises; it
11
+ * comes from `organizationDetailsPort` now, an edge this module already declares
12
+ * and has always had a foreign key for.
13
+ *
14
+ * There is no `catch` around that port on purpose. It is gated, and a gate that
15
+ * says no must not be turned into a blank name here — the reader's own
16
+ * enumeration policy is what degrades this whole bucket, one level up.
17
+ *
18
+ * No `url`: an audit actor is a person, not a row the card links to. That was
19
+ * already true of the SQL this replaces.
20
+ */
21
+ export declare function registerCustomerAccountAuditReferences(registry: AuditReferenceRegistryPort, emFactory: () => EntityManager, organizationDetails: OrganizationDetailsPort): void;
22
+ //# sourceMappingURL=audit-references.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit-references.d.ts","sourceRoot":"","sources":["../../../src/backend/services/audit-references.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,0BAA0B,EAAE,uBAAuB,EAAE,MAAM,4BAA4B,CAAC;AACtG,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAG3D;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,sCAAsC,CACpD,QAAQ,EAAE,0BAA0B,EACpC,SAAS,EAAE,MAAM,aAAa,EAC9B,mBAAmB,EAAE,uBAAuB,GAC3C,IAAI,CA+BN"}
@@ -0,0 +1,46 @@
1
+ import { CustomerAccount } from '../entities/customer-account.entity.js';
2
+ /**
3
+ * What an impersonated customer is called on an audit row (feature 075, D-87
4
+ * drain).
5
+ *
6
+ * `audit_logs` used to answer this with a `customer_accounts left join
7
+ * organizations` written into its own service — one statement crossing two
8
+ * module boundaries. The organisation name is still preferred over the account
9
+ * e-mail, because that is what an operator reading "acted as …" recognises; it
10
+ * comes from `organizationDetailsPort` now, an edge this module already declares
11
+ * and has always had a foreign key for.
12
+ *
13
+ * There is no `catch` around that port on purpose. It is gated, and a gate that
14
+ * says no must not be turned into a blank name here — the reader's own
15
+ * enumeration policy is what degrades this whole bucket, one level up.
16
+ *
17
+ * No `url`: an audit actor is a person, not a row the card links to. That was
18
+ * already true of the SQL this replaces.
19
+ */
20
+ export function registerCustomerAccountAuditReferences(registry, emFactory, organizationDetails) {
21
+ registry.register({
22
+ ownerModuleId: 'customer_accounts',
23
+ referenceType: 'customer_account',
24
+ resolve: async (ids) => {
25
+ const accounts = await emFactory().find(CustomerAccount, { id: { $in: [...ids] } }, { fields: ['id', 'email', 'organizationId'] });
26
+ if (accounts.length === 0)
27
+ return [];
28
+ const organizationIds = [
29
+ ...new Set(accounts.map((a) => a.organizationId).filter((id) => !!id)),
30
+ ];
31
+ const names = new Map();
32
+ if (organizationIds.length > 0) {
33
+ for (const org of await organizationDetails.findByIds(organizationIds)) {
34
+ names.set(org.id, org.name);
35
+ }
36
+ }
37
+ return accounts.map((account) => ({
38
+ id: account.id,
39
+ label: (account.organizationId ? names.get(account.organizationId) : undefined) ??
40
+ account.email,
41
+ url: null,
42
+ }));
43
+ },
44
+ });
45
+ }
46
+ //# sourceMappingURL=audit-references.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit-references.js","sourceRoot":"","sources":["../../../src/backend/services/audit-references.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,eAAe,EAAE,MAAM,wCAAwC,CAAC;AAEzE;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,sCAAsC,CACpD,QAAoC,EACpC,SAA8B,EAC9B,mBAA4C;IAE5C,QAAQ,CAAC,QAAQ,CAAC;QAChB,aAAa,EAAE,mBAAmB;QAClC,aAAa,EAAE,kBAAkB;QACjC,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;YACrB,MAAM,QAAQ,GAAG,MAAM,SAAS,EAAE,CAAC,IAAI,CACrC,eAAe,EACf,EAAE,EAAE,EAAE,EAAE,GAAG,EAAE,CAAC,GAAG,GAAG,CAAC,EAAE,EAAE,EACzB,EAAE,MAAM,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,gBAAgB,CAAC,EAAE,CAC9C,CAAC;YACF,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;gBAAE,OAAO,EAAE,CAAC;YAErC,MAAM,eAAe,GAAG;gBACtB,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,MAAM,CAAC,CAAC,EAAE,EAAgB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;aACrF,CAAC;YACF,MAAM,KAAK,GAAG,IAAI,GAAG,EAAkB,CAAC;YACxC,IAAI,eAAe,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAC/B,KAAK,MAAM,GAAG,IAAI,MAAM,mBAAmB,CAAC,SAAS,CAAC,eAAe,CAAC,EAAE,CAAC;oBACvE,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,EAAE,GAAG,CAAC,IAAI,CAAC,CAAC;gBAC9B,CAAC;YACH,CAAC;YAED,OAAO,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;gBAChC,EAAE,EAAE,OAAO,CAAC,EAAE;gBACd,KAAK,EACH,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;oBACxE,OAAO,CAAC,KAAK;gBACf,GAAG,EAAE,IAAI;aACV,CAAC,CAAC,CAAC;QACN,CAAC;KACF,CAAC,CAAC;AACL,CAAC"}