@open-mercato/core 0.6.1-develop.3276.1.1fcd145078 → 0.6.1-develop.3278.1.a4d26475e4

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 (212) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/dist/generated/entities/domain_mapping/index.js +31 -0
  3. package/dist/generated/entities/domain_mapping/index.js.map +7 -0
  4. package/dist/generated/entities.ids.generated.js +2 -1
  5. package/dist/generated/entities.ids.generated.js.map +2 -2
  6. package/dist/generated/entity-fields-registry.js +16 -0
  7. package/dist/generated/entity-fields-registry.js.map +2 -2
  8. package/dist/helpers/integration/salesUi.js +18 -17
  9. package/dist/helpers/integration/salesUi.js.map +2 -2
  10. package/dist/modules/customer_accounts/acl.js +2 -1
  11. package/dist/modules/customer_accounts/acl.js.map +2 -2
  12. package/dist/modules/customer_accounts/api/admin/domain-mappings/[id]/health-check.js +110 -0
  13. package/dist/modules/customer_accounts/api/admin/domain-mappings/[id]/health-check.js.map +7 -0
  14. package/dist/modules/customer_accounts/api/admin/domain-mappings/[id]/verify.js +114 -0
  15. package/dist/modules/customer_accounts/api/admin/domain-mappings/[id]/verify.js.map +7 -0
  16. package/dist/modules/customer_accounts/api/admin/domain-mappings.js +271 -0
  17. package/dist/modules/customer_accounts/api/admin/domain-mappings.js.map +7 -0
  18. package/dist/modules/customer_accounts/api/domain-check.js +70 -0
  19. package/dist/modules/customer_accounts/api/domain-check.js.map +7 -0
  20. package/dist/modules/customer_accounts/api/domain-resolve/all.js +128 -0
  21. package/dist/modules/customer_accounts/api/domain-resolve/all.js.map +7 -0
  22. package/dist/modules/customer_accounts/api/domain-resolve.js +84 -0
  23. package/dist/modules/customer_accounts/api/domain-resolve.js.map +7 -0
  24. package/dist/modules/customer_accounts/api/login.js +14 -3
  25. package/dist/modules/customer_accounts/api/login.js.map +2 -2
  26. package/dist/modules/customer_accounts/api/magic-link/request.js +14 -3
  27. package/dist/modules/customer_accounts/api/magic-link/request.js.map +2 -2
  28. package/dist/modules/customer_accounts/api/password/reset-request.js +14 -3
  29. package/dist/modules/customer_accounts/api/password/reset-request.js.map +2 -2
  30. package/dist/modules/customer_accounts/api/signup.js +32 -5
  31. package/dist/modules/customer_accounts/api/signup.js.map +2 -2
  32. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Actions.js +48 -0
  33. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Actions.js.map +7 -0
  34. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Diagnostics.js +43 -0
  35. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Diagnostics.js.map +7 -0
  36. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/DnsConfig.js +53 -0
  37. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/DnsConfig.js.map +7 -0
  38. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/DomainPanel.js +69 -0
  39. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/DomainPanel.js.map +7 -0
  40. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/OrgSwitcher.js +31 -0
  41. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/OrgSwitcher.js.map +7 -0
  42. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/RegisterDialog.js +147 -0
  43. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/RegisterDialog.js.map +7 -0
  44. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/StatusBadgeSection.js +39 -0
  45. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/StatusBadgeSection.js.map +7 -0
  46. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Stepper.js +60 -0
  47. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Stepper.js.map +7 -0
  48. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/domainStatusMap.js +11 -0
  49. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/domainStatusMap.js.map +7 -0
  50. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/types.js +1 -0
  51. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/types.js.map +7 -0
  52. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/page.js +361 -0
  53. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/page.js.map +7 -0
  54. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/page.meta.js +28 -0
  55. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/page.meta.js.map +7 -0
  56. package/dist/modules/customer_accounts/data/enrichers.js +78 -12
  57. package/dist/modules/customer_accounts/data/enrichers.js.map +2 -2
  58. package/dist/modules/customer_accounts/data/entities.js +85 -2
  59. package/dist/modules/customer_accounts/data/entities.js.map +2 -2
  60. package/dist/modules/customer_accounts/data/guards.js +130 -0
  61. package/dist/modules/customer_accounts/data/guards.js.map +7 -0
  62. package/dist/modules/customer_accounts/data/validators.js +19 -0
  63. package/dist/modules/customer_accounts/data/validators.js.map +2 -2
  64. package/dist/modules/customer_accounts/di.js +23 -1
  65. package/dist/modules/customer_accounts/di.js.map +2 -2
  66. package/dist/modules/customer_accounts/events.js +9 -2
  67. package/dist/modules/customer_accounts/events.js.map +2 -2
  68. package/dist/modules/customer_accounts/lib/customerAuthServer.js +27 -7
  69. package/dist/modules/customer_accounts/lib/customerAuthServer.js.map +2 -2
  70. package/dist/modules/customer_accounts/lib/customerUrl.js +48 -0
  71. package/dist/modules/customer_accounts/lib/customerUrl.js.map +7 -0
  72. package/dist/modules/customer_accounts/lib/hostname.js +55 -0
  73. package/dist/modules/customer_accounts/lib/hostname.js.map +7 -0
  74. package/dist/modules/customer_accounts/lib/platformDomains.js +7 -0
  75. package/dist/modules/customer_accounts/lib/platformDomains.js.map +7 -0
  76. package/dist/modules/customer_accounts/lib/proxyRanges.js +71 -0
  77. package/dist/modules/customer_accounts/lib/proxyRanges.js.map +7 -0
  78. package/dist/modules/customer_accounts/lib/rateLimiter.js +7 -0
  79. package/dist/modules/customer_accounts/lib/rateLimiter.js.map +2 -2
  80. package/dist/modules/customer_accounts/lib/resolveTenantContext.js +63 -0
  81. package/dist/modules/customer_accounts/lib/resolveTenantContext.js.map +7 -0
  82. package/dist/modules/customer_accounts/lib/secretCompare.js +13 -0
  83. package/dist/modules/customer_accounts/lib/secretCompare.js.map +7 -0
  84. package/dist/modules/customer_accounts/migrations/Migration20260430120000_customer_accounts.js +37 -0
  85. package/dist/modules/customer_accounts/migrations/Migration20260430120000_customer_accounts.js.map +7 -0
  86. package/dist/modules/customer_accounts/notifications.client.js +76 -0
  87. package/dist/modules/customer_accounts/notifications.client.js.map +2 -2
  88. package/dist/modules/customer_accounts/notifications.js +76 -0
  89. package/dist/modules/customer_accounts/notifications.js.map +2 -2
  90. package/dist/modules/customer_accounts/services/domainMappingService.js +497 -0
  91. package/dist/modules/customer_accounts/services/domainMappingService.js.map +7 -0
  92. package/dist/modules/customer_accounts/setup.js +48 -1
  93. package/dist/modules/customer_accounts/setup.js.map +2 -2
  94. package/dist/modules/customer_accounts/subscribers/invalidateDomainCache.js +29 -0
  95. package/dist/modules/customer_accounts/subscribers/invalidateDomainCache.js.map +7 -0
  96. package/dist/modules/customer_accounts/subscribers/notifyDomainActivated.js +29 -0
  97. package/dist/modules/customer_accounts/subscribers/notifyDomainActivated.js.map +7 -0
  98. package/dist/modules/customer_accounts/subscribers/notifyDomainDnsFailed.js +30 -0
  99. package/dist/modules/customer_accounts/subscribers/notifyDomainDnsFailed.js.map +7 -0
  100. package/dist/modules/customer_accounts/subscribers/notifyDomainTlsFailed.js +31 -0
  101. package/dist/modules/customer_accounts/subscribers/notifyDomainTlsFailed.js.map +7 -0
  102. package/dist/modules/customer_accounts/subscribers/notifyDomainVerified.js +29 -0
  103. package/dist/modules/customer_accounts/subscribers/notifyDomainVerified.js.map +7 -0
  104. package/dist/modules/customer_accounts/workers/domainTlsRetryWorker.js +67 -0
  105. package/dist/modules/customer_accounts/workers/domainTlsRetryWorker.js.map +7 -0
  106. package/dist/modules/customer_accounts/workers/domainVerificationWorker.js +34 -0
  107. package/dist/modules/customer_accounts/workers/domainVerificationWorker.js.map +7 -0
  108. package/dist/modules/customers/api/companies/route.js +4 -9
  109. package/dist/modules/customers/api/companies/route.js.map +2 -2
  110. package/dist/modules/customers/api/deals/route.js +4 -9
  111. package/dist/modules/customers/api/deals/route.js.map +2 -2
  112. package/dist/modules/customers/api/people/route.js +4 -9
  113. package/dist/modules/customers/api/people/route.js.map +2 -2
  114. package/dist/modules/customers/api/utils.js +0 -12
  115. package/dist/modules/customers/api/utils.js.map +2 -2
  116. package/dist/modules/customers/backend/customers/companies/page.js +361 -284
  117. package/dist/modules/customers/backend/customers/companies/page.js.map +2 -2
  118. package/dist/modules/customers/backend/customers/deals/page.js +378 -473
  119. package/dist/modules/customers/backend/customers/deals/page.js.map +2 -2
  120. package/dist/modules/customers/backend/customers/people/page.js +368 -286
  121. package/dist/modules/customers/backend/customers/people/page.js.map +2 -2
  122. package/dist/modules/customers/components/detail/assignableStaff.js +15 -1
  123. package/dist/modules/customers/components/detail/assignableStaff.js.map +2 -2
  124. package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.js +188 -36
  125. package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.js.map +2 -2
  126. package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.js +2 -2
  127. package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.js.map +2 -2
  128. package/dist/modules/customers/widgets/injection-table.js +6 -0
  129. package/dist/modules/customers/widgets/injection-table.js.map +2 -2
  130. package/dist/modules/perspectives/services/perspectiveService.js +16 -2
  131. package/dist/modules/perspectives/services/perspectiveService.js.map +2 -2
  132. package/dist/modules/portal/frontend/[orgSlug]/portal/login/page.js +1 -0
  133. package/dist/modules/portal/frontend/[orgSlug]/portal/login/page.js.map +2 -2
  134. package/dist/modules/query_index/lib/engine.js +31 -8
  135. package/dist/modules/query_index/lib/engine.js.map +2 -2
  136. package/generated/entities/domain_mapping/index.ts +14 -0
  137. package/generated/entities.ids.generated.ts +2 -1
  138. package/generated/entity-fields-registry.ts +16 -0
  139. package/package.json +5 -5
  140. package/src/helpers/integration/salesUi.ts +27 -23
  141. package/src/modules/customer_accounts/acl.ts +1 -0
  142. package/src/modules/customer_accounts/api/admin/domain-mappings/[id]/health-check.ts +119 -0
  143. package/src/modules/customer_accounts/api/admin/domain-mappings/[id]/verify.ts +123 -0
  144. package/src/modules/customer_accounts/api/admin/domain-mappings.ts +303 -0
  145. package/src/modules/customer_accounts/api/domain-check.ts +83 -0
  146. package/src/modules/customer_accounts/api/domain-resolve/all.ts +139 -0
  147. package/src/modules/customer_accounts/api/domain-resolve.ts +91 -0
  148. package/src/modules/customer_accounts/api/login.ts +14 -3
  149. package/src/modules/customer_accounts/api/magic-link/request.ts +14 -3
  150. package/src/modules/customer_accounts/api/password/reset-request.ts +15 -3
  151. package/src/modules/customer_accounts/api/signup.ts +36 -5
  152. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Actions.tsx +70 -0
  153. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Diagnostics.tsx +66 -0
  154. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/DnsConfig.tsx +78 -0
  155. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/DomainPanel.tsx +90 -0
  156. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/OrgSwitcher.tsx +43 -0
  157. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/RegisterDialog.tsx +178 -0
  158. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/StatusBadgeSection.tsx +56 -0
  159. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Stepper.tsx +69 -0
  160. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/domainStatusMap.ts +10 -0
  161. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/types.ts +31 -0
  162. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/page.meta.ts +26 -0
  163. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/page.tsx +411 -0
  164. package/src/modules/customer_accounts/data/enrichers.ts +95 -12
  165. package/src/modules/customer_accounts/data/entities.ts +80 -1
  166. package/src/modules/customer_accounts/data/guards.ts +155 -0
  167. package/src/modules/customer_accounts/data/validators.ts +26 -0
  168. package/src/modules/customer_accounts/di.ts +35 -1
  169. package/src/modules/customer_accounts/events.ts +8 -1
  170. package/src/modules/customer_accounts/i18n/de.json +78 -0
  171. package/src/modules/customer_accounts/i18n/en.json +78 -0
  172. package/src/modules/customer_accounts/i18n/es.json +78 -0
  173. package/src/modules/customer_accounts/i18n/pl.json +78 -0
  174. package/src/modules/customer_accounts/lib/customerAuthServer.ts +44 -8
  175. package/src/modules/customer_accounts/lib/customerUrl.ts +72 -0
  176. package/src/modules/customer_accounts/lib/hostname.ts +76 -0
  177. package/src/modules/customer_accounts/lib/platformDomains.ts +14 -0
  178. package/src/modules/customer_accounts/lib/proxyRanges.ts +85 -0
  179. package/src/modules/customer_accounts/lib/rateLimiter.ts +10 -0
  180. package/src/modules/customer_accounts/lib/resolveTenantContext.ts +96 -0
  181. package/src/modules/customer_accounts/lib/secretCompare.ts +20 -0
  182. package/src/modules/customer_accounts/migrations/.snapshot-open-mercato.json +341 -0
  183. package/src/modules/customer_accounts/migrations/Migration20260430120000_customer_accounts.ts +38 -0
  184. package/src/modules/customer_accounts/notifications.client.ts +76 -0
  185. package/src/modules/customer_accounts/notifications.ts +76 -0
  186. package/src/modules/customer_accounts/services/domainMappingService.ts +632 -0
  187. package/src/modules/customer_accounts/setup.ts +81 -1
  188. package/src/modules/customer_accounts/subscribers/invalidateDomainCache.ts +35 -0
  189. package/src/modules/customer_accounts/subscribers/notifyDomainActivated.ts +32 -0
  190. package/src/modules/customer_accounts/subscribers/notifyDomainDnsFailed.ts +33 -0
  191. package/src/modules/customer_accounts/subscribers/notifyDomainTlsFailed.ts +34 -0
  192. package/src/modules/customer_accounts/subscribers/notifyDomainVerified.ts +32 -0
  193. package/src/modules/customer_accounts/workers/domainTlsRetryWorker.ts +84 -0
  194. package/src/modules/customer_accounts/workers/domainVerificationWorker.ts +45 -0
  195. package/src/modules/customers/api/companies/route.ts +4 -9
  196. package/src/modules/customers/api/deals/route.ts +4 -9
  197. package/src/modules/customers/api/people/route.ts +4 -9
  198. package/src/modules/customers/api/utils.ts +0 -14
  199. package/src/modules/customers/backend/customers/companies/page.tsx +321 -237
  200. package/src/modules/customers/backend/customers/deals/page.tsx +321 -455
  201. package/src/modules/customers/backend/customers/people/page.tsx +331 -240
  202. package/src/modules/customers/components/detail/assignableStaff.ts +21 -0
  203. package/src/modules/customers/i18n/de.json +31 -0
  204. package/src/modules/customers/i18n/en.json +31 -0
  205. package/src/modules/customers/i18n/es.json +31 -0
  206. package/src/modules/customers/i18n/pl.json +31 -0
  207. package/src/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.tsx +197 -40
  208. package/src/modules/customers/widgets/injection/ai-assistant-trigger/widget.ts +13 -7
  209. package/src/modules/customers/widgets/injection-table.ts +6 -0
  210. package/src/modules/perspectives/services/perspectiveService.ts +30 -2
  211. package/src/modules/portal/frontend/[orgSlug]/portal/login/page.tsx +1 -0
  212. package/src/modules/query_index/lib/engine.ts +46 -8
@@ -192,12 +192,90 @@
192
192
  "customer_accounts.admin.unverified": "No",
193
193
  "customer_accounts.admin.users": "Users",
194
194
  "customer_accounts.admin.verified": "Yes",
195
+ "customer_accounts.domainMapping.aRecordInstruction": "For an apex domain, add an A record for {hostname} pointing to {target}",
196
+ "customer_accounts.domainMapping.alreadyClaimed": "This domain is not available. Please choose a different hostname.",
197
+ "customer_accounts.domainMapping.autoVerify.checking": "Automatically checking every 5 minutes",
198
+ "customer_accounts.domainMapping.autoVerify.lastChecked": "Last checked: {time}",
199
+ "customer_accounts.domainMapping.autoVerify.nextCheck": "Next check in ~{minutes} minutes",
200
+ "customer_accounts.domainMapping.changeDomain": "Change Domain",
201
+ "customer_accounts.domainMapping.changeDomain.description": "Your current domain ({hostname}) will stay active until the new domain is fully set up. No downtime for your customers.",
202
+ "customer_accounts.domainMapping.changeDomain.replacing": "Replacing {oldHostname} — old domain stays live until this one is ready",
203
+ "customer_accounts.domainMapping.cnameInstruction": "Add a CNAME record for {hostname} pointing to {target} in your DNS provider",
204
+ "customer_accounts.domainMapping.copyTarget": "Copy",
205
+ "customer_accounts.domainMapping.copyTarget.copied": "Copied",
206
+ "customer_accounts.domainMapping.description": "Map your own domain to the customer portal",
207
+ "customer_accounts.domainMapping.diagnostics.detectedRecords": "Detected DNS records:",
208
+ "customer_accounts.domainMapping.diagnostics.noRecord": "No CNAME or A record found for {hostname}",
209
+ "customer_accounts.domainMapping.diagnostics.proxyDetected": "Your DNS uses a proxy ({proxy}). Either disable the proxy or configure it to forward traffic to our platform.",
210
+ "customer_accounts.domainMapping.diagnostics.suggestion": "Suggested fix:",
211
+ "customer_accounts.domainMapping.diagnostics.timeout": "DNS lookup timed out — this may be a temporary issue",
212
+ "customer_accounts.domainMapping.diagnostics.wrongARecordTarget": "Your A record points to {detected} instead of {expected}",
213
+ "customer_accounts.domainMapping.diagnostics.wrongCnameTarget": "Your CNAME record points to {detected} instead of {expected}",
214
+ "customer_accounts.domainMapping.dialog.cancel": "Cancel",
215
+ "customer_accounts.domainMapping.dialog.change.submit": "Start replacement",
216
+ "customer_accounts.domainMapping.dialog.change.title": "Change custom domain",
217
+ "customer_accounts.domainMapping.dialog.register.submit": "Register",
218
+ "customer_accounts.domainMapping.dialog.register.title": "Register custom domain",
219
+ "customer_accounts.domainMapping.dns.diagnostics.detected": "Detected target",
220
+ "customer_accounts.domainMapping.dns.diagnostics.expected": "Expected target",
221
+ "customer_accounts.domainMapping.dns.diagnostics.title": "DNS configuration issue",
222
+ "customer_accounts.domainMapping.dns.recheckButton": "Re-check DNS",
223
+ "customer_accounts.domainMapping.dnsProviderHelp": "Need help? See instructions for:",
224
+ "customer_accounts.domainMapping.dnsProviderHelp.cloudflare": "Cloudflare",
225
+ "customer_accounts.domainMapping.dnsProviderHelp.godaddy": "GoDaddy",
226
+ "customer_accounts.domainMapping.dnsProviderHelp.googleDomains": "Google Domains",
227
+ "customer_accounts.domainMapping.dnsProviderHelp.namecheap": "Namecheap",
228
+ "customer_accounts.domainMapping.dnsProviderHelp.other": "Other providers",
229
+ "customer_accounts.domainMapping.empty.cta": "Register Domain",
230
+ "customer_accounts.domainMapping.empty.description": "Connect your own domain so customers can reach the portal at a branded URL.",
231
+ "customer_accounts.domainMapping.empty.title": "No custom domain yet",
232
+ "customer_accounts.domainMapping.error.load": "Could not load custom-domain configuration",
233
+ "customer_accounts.domainMapping.errors.required": "Domain is required",
234
+ "customer_accounts.domainMapping.hostname.label": "Domain",
235
+ "customer_accounts.domainMapping.hostname.placeholder": "e.g., shop.yourdomain.com",
236
+ "customer_accounts.domainMapping.hostname.validation.invalid": "Enter a valid domain name (no protocol, path, or port)",
237
+ "customer_accounts.domainMapping.loading": "Loading domain configuration…",
238
+ "customer_accounts.domainMapping.orgSwitcher.label": "Organization",
239
+ "customer_accounts.domainMapping.preview": "Test Domain",
240
+ "customer_accounts.domainMapping.preview.description": "Opens your portal on the custom domain in a new tab to verify it's working correctly",
241
+ "customer_accounts.domainMapping.preview.notReady": "Domain is not yet active. Complete the setup steps first.",
242
+ "customer_accounts.domainMapping.propagationNote": "DNS changes can take up to 48 hours to propagate. The system checks automatically — you'll be notified when it's ready.",
243
+ "customer_accounts.domainMapping.registered": "Domain registered successfully",
244
+ "customer_accounts.domainMapping.removeConfirm": "Are you sure? Customers using this domain will no longer be able to reach your portal.",
245
+ "customer_accounts.domainMapping.removeConfirm.hasReplacement": "This will also cancel the pending replacement domain ({hostname}).",
246
+ "customer_accounts.domainMapping.removeConfirm.title": "Remove custom domain?",
247
+ "customer_accounts.domainMapping.removeDomain": "Remove Domain",
248
+ "customer_accounts.domainMapping.removed": "Domain removed",
249
+ "customer_accounts.domainMapping.replacement.label": "Replacement (waiting to activate)",
250
+ "customer_accounts.domainMapping.status.active": "Active",
251
+ "customer_accounts.domainMapping.status.dns_failed": "DNS issue — see details below",
252
+ "customer_accounts.domainMapping.status.pending": "Checking DNS...",
253
+ "customer_accounts.domainMapping.status.tls_failed": "SSL certificate issue — we're retrying",
254
+ "customer_accounts.domainMapping.status.verified": "Setting up SSL certificate...",
255
+ "customer_accounts.domainMapping.stepper.step1": "Register Domain",
256
+ "customer_accounts.domainMapping.stepper.step2": "Configure DNS",
257
+ "customer_accounts.domainMapping.stepper.step3": "SSL Certificate",
258
+ "customer_accounts.domainMapping.stepper.step4": "Live",
259
+ "customer_accounts.domainMapping.title": "Custom Domain",
260
+ "customer_accounts.domainMapping.tls.diagnostics.operatorNote": "We are retrying automatically. If this persists, contact platform support — your DNS is fine, this is on our side.",
261
+ "customer_accounts.domainMapping.tls.diagnostics.retryCount": "Retry attempts: {count}",
262
+ "customer_accounts.domainMapping.tls.diagnostics.title": "SSL certificate issue",
263
+ "customer_accounts.domainMapping.tls.retryButton": "Retry SSL",
264
+ "customer_accounts.domainMapping.verifyNow": "Check Now",
195
265
  "customer_accounts.nav.group": "Customers",
196
266
  "customer_accounts.nav.role_create": "Create Role",
197
267
  "customer_accounts.nav.role_detail": "Role Detail",
198
268
  "customer_accounts.nav.roles": "Customer Roles",
199
269
  "customer_accounts.nav.user_detail": "User Detail",
200
270
  "customer_accounts.nav.users": "Customer Users",
271
+ "customer_accounts.notifications.domain_mapping.activated.body": "Your custom domain is now active and serving traffic.",
272
+ "customer_accounts.notifications.domain_mapping.activated.title": "Custom domain is live",
273
+ "customer_accounts.notifications.domain_mapping.dns_failed.body": "DNS verification failed for your custom domain. {reason}",
274
+ "customer_accounts.notifications.domain_mapping.dns_failed.title": "DNS verification failed",
275
+ "customer_accounts.notifications.domain_mapping.tls_failed.body": "SSL certificate provisioning is having trouble for your custom domain. We're retrying — no action needed yet.",
276
+ "customer_accounts.notifications.domain_mapping.tls_failed.title": "SSL provisioning issue",
277
+ "customer_accounts.notifications.domain_mapping.verified.body": "Your custom domain has been verified. SSL certificate is being provisioned.",
278
+ "customer_accounts.notifications.domain_mapping.verified.title": "Custom domain verified",
201
279
  "customer_accounts.notifications.user.locked.body": "Customer account was locked due to too many failed login attempts.",
202
280
  "customer_accounts.notifications.user.locked.title": "Customer account locked",
203
281
  "customer_accounts.notifications.user.signup.body": "A new customer account was created.",
@@ -192,12 +192,90 @@
192
192
  "customer_accounts.admin.unverified": "No",
193
193
  "customer_accounts.admin.users": "Users",
194
194
  "customer_accounts.admin.verified": "Yes",
195
+ "customer_accounts.domainMapping.aRecordInstruction": "For an apex domain, add an A record for {hostname} pointing to {target}",
196
+ "customer_accounts.domainMapping.alreadyClaimed": "This domain is not available. Please choose a different hostname.",
197
+ "customer_accounts.domainMapping.autoVerify.checking": "Automatically checking every 5 minutes",
198
+ "customer_accounts.domainMapping.autoVerify.lastChecked": "Last checked: {time}",
199
+ "customer_accounts.domainMapping.autoVerify.nextCheck": "Next check in ~{minutes} minutes",
200
+ "customer_accounts.domainMapping.changeDomain": "Change Domain",
201
+ "customer_accounts.domainMapping.changeDomain.description": "Your current domain ({hostname}) will stay active until the new domain is fully set up. No downtime for your customers.",
202
+ "customer_accounts.domainMapping.changeDomain.replacing": "Replacing {oldHostname} — old domain stays live until this one is ready",
203
+ "customer_accounts.domainMapping.cnameInstruction": "Add a CNAME record for {hostname} pointing to {target} in your DNS provider",
204
+ "customer_accounts.domainMapping.copyTarget": "Copy",
205
+ "customer_accounts.domainMapping.copyTarget.copied": "Copied",
206
+ "customer_accounts.domainMapping.description": "Map your own domain to the customer portal",
207
+ "customer_accounts.domainMapping.diagnostics.detectedRecords": "Detected DNS records:",
208
+ "customer_accounts.domainMapping.diagnostics.noRecord": "No CNAME or A record found for {hostname}",
209
+ "customer_accounts.domainMapping.diagnostics.proxyDetected": "Your DNS uses a proxy ({proxy}). Either disable the proxy or configure it to forward traffic to our platform.",
210
+ "customer_accounts.domainMapping.diagnostics.suggestion": "Suggested fix:",
211
+ "customer_accounts.domainMapping.diagnostics.timeout": "DNS lookup timed out — this may be a temporary issue",
212
+ "customer_accounts.domainMapping.diagnostics.wrongARecordTarget": "Your A record points to {detected} instead of {expected}",
213
+ "customer_accounts.domainMapping.diagnostics.wrongCnameTarget": "Your CNAME record points to {detected} instead of {expected}",
214
+ "customer_accounts.domainMapping.dialog.cancel": "Cancel",
215
+ "customer_accounts.domainMapping.dialog.change.submit": "Start replacement",
216
+ "customer_accounts.domainMapping.dialog.change.title": "Change custom domain",
217
+ "customer_accounts.domainMapping.dialog.register.submit": "Register",
218
+ "customer_accounts.domainMapping.dialog.register.title": "Register custom domain",
219
+ "customer_accounts.domainMapping.dns.diagnostics.detected": "Detected target",
220
+ "customer_accounts.domainMapping.dns.diagnostics.expected": "Expected target",
221
+ "customer_accounts.domainMapping.dns.diagnostics.title": "DNS configuration issue",
222
+ "customer_accounts.domainMapping.dns.recheckButton": "Re-check DNS",
223
+ "customer_accounts.domainMapping.dnsProviderHelp": "Need help? See instructions for:",
224
+ "customer_accounts.domainMapping.dnsProviderHelp.cloudflare": "Cloudflare",
225
+ "customer_accounts.domainMapping.dnsProviderHelp.godaddy": "GoDaddy",
226
+ "customer_accounts.domainMapping.dnsProviderHelp.googleDomains": "Google Domains",
227
+ "customer_accounts.domainMapping.dnsProviderHelp.namecheap": "Namecheap",
228
+ "customer_accounts.domainMapping.dnsProviderHelp.other": "Other providers",
229
+ "customer_accounts.domainMapping.empty.cta": "Register Domain",
230
+ "customer_accounts.domainMapping.empty.description": "Connect your own domain so customers can reach the portal at a branded URL.",
231
+ "customer_accounts.domainMapping.empty.title": "No custom domain yet",
232
+ "customer_accounts.domainMapping.error.load": "Could not load custom-domain configuration",
233
+ "customer_accounts.domainMapping.errors.required": "Domain is required",
234
+ "customer_accounts.domainMapping.hostname.label": "Domain",
235
+ "customer_accounts.domainMapping.hostname.placeholder": "e.g., shop.yourdomain.com",
236
+ "customer_accounts.domainMapping.hostname.validation.invalid": "Enter a valid domain name (no protocol, path, or port)",
237
+ "customer_accounts.domainMapping.loading": "Loading domain configuration…",
238
+ "customer_accounts.domainMapping.orgSwitcher.label": "Organization",
239
+ "customer_accounts.domainMapping.preview": "Test Domain",
240
+ "customer_accounts.domainMapping.preview.description": "Opens your portal on the custom domain in a new tab to verify it's working correctly",
241
+ "customer_accounts.domainMapping.preview.notReady": "Domain is not yet active. Complete the setup steps first.",
242
+ "customer_accounts.domainMapping.propagationNote": "DNS changes can take up to 48 hours to propagate. The system checks automatically — you'll be notified when it's ready.",
243
+ "customer_accounts.domainMapping.registered": "Domain registered successfully",
244
+ "customer_accounts.domainMapping.removeConfirm": "Are you sure? Customers using this domain will no longer be able to reach your portal.",
245
+ "customer_accounts.domainMapping.removeConfirm.hasReplacement": "This will also cancel the pending replacement domain ({hostname}).",
246
+ "customer_accounts.domainMapping.removeConfirm.title": "Remove custom domain?",
247
+ "customer_accounts.domainMapping.removeDomain": "Remove Domain",
248
+ "customer_accounts.domainMapping.removed": "Domain removed",
249
+ "customer_accounts.domainMapping.replacement.label": "Replacement (waiting to activate)",
250
+ "customer_accounts.domainMapping.status.active": "Active",
251
+ "customer_accounts.domainMapping.status.dns_failed": "DNS issue — see details below",
252
+ "customer_accounts.domainMapping.status.pending": "Checking DNS...",
253
+ "customer_accounts.domainMapping.status.tls_failed": "SSL certificate issue — we're retrying",
254
+ "customer_accounts.domainMapping.status.verified": "Setting up SSL certificate...",
255
+ "customer_accounts.domainMapping.stepper.step1": "Register Domain",
256
+ "customer_accounts.domainMapping.stepper.step2": "Configure DNS",
257
+ "customer_accounts.domainMapping.stepper.step3": "SSL Certificate",
258
+ "customer_accounts.domainMapping.stepper.step4": "Live",
259
+ "customer_accounts.domainMapping.title": "Custom Domain",
260
+ "customer_accounts.domainMapping.tls.diagnostics.operatorNote": "We are retrying automatically. If this persists, contact platform support — your DNS is fine, this is on our side.",
261
+ "customer_accounts.domainMapping.tls.diagnostics.retryCount": "Retry attempts: {count}",
262
+ "customer_accounts.domainMapping.tls.diagnostics.title": "SSL certificate issue",
263
+ "customer_accounts.domainMapping.tls.retryButton": "Retry SSL",
264
+ "customer_accounts.domainMapping.verifyNow": "Check Now",
195
265
  "customer_accounts.nav.group": "Customers",
196
266
  "customer_accounts.nav.role_create": "Create Role",
197
267
  "customer_accounts.nav.role_detail": "Role Detail",
198
268
  "customer_accounts.nav.roles": "Roles de clientes",
199
269
  "customer_accounts.nav.user_detail": "User Detail",
200
270
  "customer_accounts.nav.users": "Usuarios de clientes",
271
+ "customer_accounts.notifications.domain_mapping.activated.body": "Your custom domain is now active and serving traffic.",
272
+ "customer_accounts.notifications.domain_mapping.activated.title": "Custom domain is live",
273
+ "customer_accounts.notifications.domain_mapping.dns_failed.body": "DNS verification failed for your custom domain. {reason}",
274
+ "customer_accounts.notifications.domain_mapping.dns_failed.title": "DNS verification failed",
275
+ "customer_accounts.notifications.domain_mapping.tls_failed.body": "SSL certificate provisioning is having trouble for your custom domain. We're retrying — no action needed yet.",
276
+ "customer_accounts.notifications.domain_mapping.tls_failed.title": "SSL provisioning issue",
277
+ "customer_accounts.notifications.domain_mapping.verified.body": "Your custom domain has been verified. SSL certificate is being provisioned.",
278
+ "customer_accounts.notifications.domain_mapping.verified.title": "Custom domain verified",
201
279
  "customer_accounts.notifications.user.locked.body": "Customer account was locked due to too many failed login attempts.",
202
280
  "customer_accounts.notifications.user.locked.title": "Customer account locked",
203
281
  "customer_accounts.notifications.user.signup.body": "A new customer account was created.",
@@ -192,12 +192,90 @@
192
192
  "customer_accounts.admin.unverified": "Nie",
193
193
  "customer_accounts.admin.users": "Użytkownicy",
194
194
  "customer_accounts.admin.verified": "Tak",
195
+ "customer_accounts.domainMapping.aRecordInstruction": "For an apex domain, add an A record for {hostname} pointing to {target}",
196
+ "customer_accounts.domainMapping.alreadyClaimed": "This domain is not available. Please choose a different hostname.",
197
+ "customer_accounts.domainMapping.autoVerify.checking": "Automatically checking every 5 minutes",
198
+ "customer_accounts.domainMapping.autoVerify.lastChecked": "Last checked: {time}",
199
+ "customer_accounts.domainMapping.autoVerify.nextCheck": "Next check in ~{minutes} minutes",
200
+ "customer_accounts.domainMapping.changeDomain": "Change Domain",
201
+ "customer_accounts.domainMapping.changeDomain.description": "Your current domain ({hostname}) will stay active until the new domain is fully set up. No downtime for your customers.",
202
+ "customer_accounts.domainMapping.changeDomain.replacing": "Replacing {oldHostname} — old domain stays live until this one is ready",
203
+ "customer_accounts.domainMapping.cnameInstruction": "Add a CNAME record for {hostname} pointing to {target} in your DNS provider",
204
+ "customer_accounts.domainMapping.copyTarget": "Copy",
205
+ "customer_accounts.domainMapping.copyTarget.copied": "Copied",
206
+ "customer_accounts.domainMapping.description": "Map your own domain to the customer portal",
207
+ "customer_accounts.domainMapping.diagnostics.detectedRecords": "Detected DNS records:",
208
+ "customer_accounts.domainMapping.diagnostics.noRecord": "No CNAME or A record found for {hostname}",
209
+ "customer_accounts.domainMapping.diagnostics.proxyDetected": "Your DNS uses a proxy ({proxy}). Either disable the proxy or configure it to forward traffic to our platform.",
210
+ "customer_accounts.domainMapping.diagnostics.suggestion": "Suggested fix:",
211
+ "customer_accounts.domainMapping.diagnostics.timeout": "DNS lookup timed out — this may be a temporary issue",
212
+ "customer_accounts.domainMapping.diagnostics.wrongARecordTarget": "Your A record points to {detected} instead of {expected}",
213
+ "customer_accounts.domainMapping.diagnostics.wrongCnameTarget": "Your CNAME record points to {detected} instead of {expected}",
214
+ "customer_accounts.domainMapping.dialog.cancel": "Cancel",
215
+ "customer_accounts.domainMapping.dialog.change.submit": "Start replacement",
216
+ "customer_accounts.domainMapping.dialog.change.title": "Change custom domain",
217
+ "customer_accounts.domainMapping.dialog.register.submit": "Register",
218
+ "customer_accounts.domainMapping.dialog.register.title": "Register custom domain",
219
+ "customer_accounts.domainMapping.dns.diagnostics.detected": "Detected target",
220
+ "customer_accounts.domainMapping.dns.diagnostics.expected": "Expected target",
221
+ "customer_accounts.domainMapping.dns.diagnostics.title": "DNS configuration issue",
222
+ "customer_accounts.domainMapping.dns.recheckButton": "Re-check DNS",
223
+ "customer_accounts.domainMapping.dnsProviderHelp": "Need help? See instructions for:",
224
+ "customer_accounts.domainMapping.dnsProviderHelp.cloudflare": "Cloudflare",
225
+ "customer_accounts.domainMapping.dnsProviderHelp.godaddy": "GoDaddy",
226
+ "customer_accounts.domainMapping.dnsProviderHelp.googleDomains": "Google Domains",
227
+ "customer_accounts.domainMapping.dnsProviderHelp.namecheap": "Namecheap",
228
+ "customer_accounts.domainMapping.dnsProviderHelp.other": "Other providers",
229
+ "customer_accounts.domainMapping.empty.cta": "Register Domain",
230
+ "customer_accounts.domainMapping.empty.description": "Connect your own domain so customers can reach the portal at a branded URL.",
231
+ "customer_accounts.domainMapping.empty.title": "No custom domain yet",
232
+ "customer_accounts.domainMapping.error.load": "Could not load custom-domain configuration",
233
+ "customer_accounts.domainMapping.errors.required": "Domain is required",
234
+ "customer_accounts.domainMapping.hostname.label": "Domain",
235
+ "customer_accounts.domainMapping.hostname.placeholder": "e.g., shop.yourdomain.com",
236
+ "customer_accounts.domainMapping.hostname.validation.invalid": "Enter a valid domain name (no protocol, path, or port)",
237
+ "customer_accounts.domainMapping.loading": "Loading domain configuration…",
238
+ "customer_accounts.domainMapping.orgSwitcher.label": "Organization",
239
+ "customer_accounts.domainMapping.preview": "Test Domain",
240
+ "customer_accounts.domainMapping.preview.description": "Opens your portal on the custom domain in a new tab to verify it's working correctly",
241
+ "customer_accounts.domainMapping.preview.notReady": "Domain is not yet active. Complete the setup steps first.",
242
+ "customer_accounts.domainMapping.propagationNote": "DNS changes can take up to 48 hours to propagate. The system checks automatically — you'll be notified when it's ready.",
243
+ "customer_accounts.domainMapping.registered": "Domain registered successfully",
244
+ "customer_accounts.domainMapping.removeConfirm": "Are you sure? Customers using this domain will no longer be able to reach your portal.",
245
+ "customer_accounts.domainMapping.removeConfirm.hasReplacement": "This will also cancel the pending replacement domain ({hostname}).",
246
+ "customer_accounts.domainMapping.removeConfirm.title": "Remove custom domain?",
247
+ "customer_accounts.domainMapping.removeDomain": "Remove Domain",
248
+ "customer_accounts.domainMapping.removed": "Domain removed",
249
+ "customer_accounts.domainMapping.replacement.label": "Replacement (waiting to activate)",
250
+ "customer_accounts.domainMapping.status.active": "Active",
251
+ "customer_accounts.domainMapping.status.dns_failed": "DNS issue — see details below",
252
+ "customer_accounts.domainMapping.status.pending": "Checking DNS...",
253
+ "customer_accounts.domainMapping.status.tls_failed": "SSL certificate issue — we're retrying",
254
+ "customer_accounts.domainMapping.status.verified": "Setting up SSL certificate...",
255
+ "customer_accounts.domainMapping.stepper.step1": "Register Domain",
256
+ "customer_accounts.domainMapping.stepper.step2": "Configure DNS",
257
+ "customer_accounts.domainMapping.stepper.step3": "SSL Certificate",
258
+ "customer_accounts.domainMapping.stepper.step4": "Live",
259
+ "customer_accounts.domainMapping.title": "Custom Domain",
260
+ "customer_accounts.domainMapping.tls.diagnostics.operatorNote": "We are retrying automatically. If this persists, contact platform support — your DNS is fine, this is on our side.",
261
+ "customer_accounts.domainMapping.tls.diagnostics.retryCount": "Retry attempts: {count}",
262
+ "customer_accounts.domainMapping.tls.diagnostics.title": "SSL certificate issue",
263
+ "customer_accounts.domainMapping.tls.retryButton": "Retry SSL",
264
+ "customer_accounts.domainMapping.verifyNow": "Check Now",
195
265
  "customer_accounts.nav.group": "Klienci",
196
266
  "customer_accounts.nav.role_create": "Utwórz rolę",
197
267
  "customer_accounts.nav.role_detail": "Szczegóły roli",
198
268
  "customer_accounts.nav.roles": "Role klientów",
199
269
  "customer_accounts.nav.user_detail": "Szczegóły użytkownika",
200
270
  "customer_accounts.nav.users": "Użytkownicy klientów",
271
+ "customer_accounts.notifications.domain_mapping.activated.body": "Your custom domain is now active and serving traffic.",
272
+ "customer_accounts.notifications.domain_mapping.activated.title": "Custom domain is live",
273
+ "customer_accounts.notifications.domain_mapping.dns_failed.body": "DNS verification failed for your custom domain. {reason}",
274
+ "customer_accounts.notifications.domain_mapping.dns_failed.title": "DNS verification failed",
275
+ "customer_accounts.notifications.domain_mapping.tls_failed.body": "SSL certificate provisioning is having trouble for your custom domain. We're retrying — no action needed yet.",
276
+ "customer_accounts.notifications.domain_mapping.tls_failed.title": "SSL provisioning issue",
277
+ "customer_accounts.notifications.domain_mapping.verified.body": "Your custom domain has been verified. SSL certificate is being provisioned.",
278
+ "customer_accounts.notifications.domain_mapping.verified.title": "Custom domain verified",
201
279
  "customer_accounts.notifications.user.locked.body": "Customer account was locked due to too many failed login attempts.",
202
280
  "customer_accounts.notifications.user.locked.title": "Customer account locked",
203
281
  "customer_accounts.notifications.user.signup.body": "A new customer account was created.",
@@ -6,8 +6,12 @@
6
6
  */
7
7
 
8
8
  import { cookies } from 'next/headers'
9
+ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
9
10
  import { verifyAudienceJwt } from '@open-mercato/shared/lib/auth/jwt'
10
- import { CUSTOMER_JWT_AUDIENCE } from '@open-mercato/core/modules/customer_accounts/services/customerSessionService'
11
+ import { CUSTOMER_JWT_AUDIENCE, CustomerSessionService } from '@open-mercato/core/modules/customer_accounts/services/customerSessionService'
12
+ import { DomainMappingService } from '@open-mercato/core/modules/customer_accounts/services/domainMappingService'
13
+ import { tryNormalizeHostname } from '@open-mercato/core/modules/customer_accounts/lib/hostname'
14
+ import { platformDomains } from '@open-mercato/core/modules/customer_accounts/lib/platformDomains'
11
15
  import { validateUserState } from './customerAuth'
12
16
  import type { CustomerAuthContext } from './customerAuth'
13
17
 
@@ -15,10 +19,6 @@ export type { CustomerAuthContext }
15
19
 
16
20
  async function assertSessionStillActive(sessionId: string): Promise<boolean> {
17
21
  try {
18
- const [{ createRequestContainer }, { CustomerSessionService }] = await Promise.all([
19
- import('@open-mercato/shared/lib/di/container'),
20
- import('@open-mercato/core/modules/customer_accounts/services/customerSessionService'),
21
- ])
22
22
  const container = await createRequestContainer()
23
23
  const service = container.resolve('customerSessionService') as InstanceType<typeof CustomerSessionService>
24
24
  const session = await service.findActiveSessionById(sessionId)
@@ -43,7 +43,9 @@ async function assertSessionStillActive(sessionId: string): Promise<boolean> {
43
43
  * if (!customerAuth) redirect('/login')
44
44
  * ```
45
45
  */
46
- export async function getCustomerAuthFromCookies(): Promise<CustomerAuthContext | null> {
46
+ export async function getCustomerAuthFromCookies(
47
+ options?: { expectedTenantId?: string },
48
+ ): Promise<CustomerAuthContext | null> {
47
49
  const cookieStore = await cookies()
48
50
  const token = cookieStore.get('customer_auth_token')?.value
49
51
  if (!token) return null
@@ -54,12 +56,19 @@ export async function getCustomerAuthFromCookies(): Promise<CustomerAuthContext
54
56
  if (payload.type !== 'customer') return null
55
57
  const sid = typeof payload.sid === 'string' ? payload.sid : ''
56
58
  if (!sid) return null
59
+ const tenantId = String(payload.tenantId)
60
+ if (options?.expectedTenantId && options.expectedTenantId !== tenantId) {
61
+ // Cross-host JWT replay defense. See spec rev 5 Customer Authentication
62
+ // section: the host-resolved tenant is the authoritative scope; mismatched
63
+ // JWTs are rejected as if unauthenticated.
64
+ return null
65
+ }
57
66
  const stillActive = await assertSessionStillActive(sid)
58
67
  if (!stillActive) return null
59
68
 
60
69
  const userState = await validateUserState(
61
70
  String(payload.sub),
62
- String(payload.tenantId),
71
+ tenantId,
63
72
  String(payload.orgId),
64
73
  payload.iat,
65
74
  )
@@ -69,7 +78,7 @@ export async function getCustomerAuthFromCookies(): Promise<CustomerAuthContext
69
78
  sub: String(payload.sub),
70
79
  sid,
71
80
  type: 'customer',
72
- tenantId: String(payload.tenantId),
81
+ tenantId,
73
82
  orgId: String(payload.orgId),
74
83
  email: String(payload.email || ''),
75
84
  displayName: String(payload.displayName || ''),
@@ -82,3 +91,30 @@ export async function getCustomerAuthFromCookies(): Promise<CustomerAuthContext
82
91
  return null
83
92
  }
84
93
  }
94
+
95
+ /**
96
+ * Convenience wrapper for custom-domain pages: resolves the host, calls
97
+ * getCustomerAuthFromCookies with the host-resolved tenant as expectedTenantId.
98
+ *
99
+ * Use from server components rendered under custom domains.
100
+ */
101
+ export async function getCustomerAuthForHost(
102
+ host: string | null | undefined,
103
+ ): Promise<CustomerAuthContext | null> {
104
+ if (!host) return getCustomerAuthFromCookies()
105
+
106
+ try {
107
+ const hostname = tryNormalizeHostname(host)
108
+ if (!hostname) return getCustomerAuthFromCookies()
109
+ if (platformDomains().includes(hostname)) return getCustomerAuthFromCookies()
110
+
111
+ const container = await createRequestContainer()
112
+ const service = container.resolve('domainMappingService') as InstanceType<typeof DomainMappingService>
113
+ const resolved = await service.resolveByHostname(hostname)
114
+ if (!resolved || resolved.status !== 'active') return null
115
+ return getCustomerAuthFromCookies({ expectedTenantId: resolved.tenantId })
116
+ } catch (err) {
117
+ console.warn('[customer_accounts] getCustomerAuthForHost: domain resolve failed; falling back to platform cookie', err)
118
+ return getCustomerAuthFromCookies()
119
+ }
120
+ }
@@ -0,0 +1,72 @@
1
+ // Build customer-portal URLs that honor the active custom domain when present.
2
+ // All customer-facing email senders (signup welcome, magic link, password reset,
3
+ // notification digests) MUST construct links via this helper instead of
4
+ // hard-coding PLATFORM_PORTAL_BASE_URL — see customer_accounts/AGENTS.md.
5
+
6
+ import type { AppContainer } from '@open-mercato/shared/lib/di/container'
7
+ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
8
+
9
+ type DomainMappingService = {
10
+ resolveActiveByOrg(orgId: string): Promise<{ hostname: string } | null>
11
+ }
12
+
13
+ type OrgService = {
14
+ findById(orgId: string): Promise<{ id: string; slug: string | null } | null>
15
+ }
16
+
17
+ function platformBaseUrl(): string {
18
+ const fromEnv = process.env.PLATFORM_PORTAL_BASE_URL?.trim()
19
+ if (fromEnv && fromEnv.length > 0) return fromEnv.replace(/\/+$/, '')
20
+ // Outbound emails (welcome, magic link, password reset) embed this URL —
21
+ // localhost in production silently sends broken links to real customers.
22
+ if (process.env.NODE_ENV === 'production') {
23
+ throw new Error(
24
+ 'PLATFORM_PORTAL_BASE_URL is required in production. Configure it before sending customer-portal emails.',
25
+ )
26
+ }
27
+ return 'http://localhost:3000'
28
+ }
29
+
30
+ function resolveContainer(container?: AppContainer): Promise<AppContainer> {
31
+ if (container) return Promise.resolve(container)
32
+ return createRequestContainer()
33
+ }
34
+
35
+ export async function urlForCustomerOrg(
36
+ orgId: string,
37
+ path: string,
38
+ options?: { container?: AppContainer },
39
+ ): Promise<string> {
40
+ const safePath = path.startsWith('/') ? path : `/${path}`
41
+ const container = await resolveContainer(options?.container)
42
+
43
+ let active: { hostname: string } | null = null
44
+ try {
45
+ const service = container.resolve('domainMappingService') as DomainMappingService
46
+ if (service && typeof service.resolveActiveByOrg === 'function') {
47
+ active = await service.resolveActiveByOrg(orgId)
48
+ }
49
+ } catch {
50
+ // domainMappingService not registered yet (fresh installs, tests) — fall through to platform URL.
51
+ active = null
52
+ }
53
+
54
+ if (active && active.hostname) {
55
+ return `https://${active.hostname}${safePath}`
56
+ }
57
+
58
+ let orgSlug: string | null = null
59
+ try {
60
+ const orgService = container.resolve('orgService') as OrgService | undefined
61
+ if (orgService && typeof orgService.findById === 'function') {
62
+ const org = await orgService.findById(orgId)
63
+ orgSlug = org?.slug ?? null
64
+ }
65
+ } catch {
66
+ orgSlug = null
67
+ }
68
+
69
+ const base = platformBaseUrl()
70
+ if (orgSlug) return `${base}/${orgSlug}/portal${safePath}`
71
+ return `${base}${safePath}`
72
+ }
@@ -0,0 +1,76 @@
1
+ // Hostname normalization for the custom-domain routing feature.
2
+ // Single canonical form (lowercase, no trailing dot, IDN→Punycode) is essential —
3
+ // the UNIQUE constraint on `domain_mappings.hostname` is meaningless without it.
4
+
5
+ const MAX_HOSTNAME_LENGTH = 253
6
+ const HOSTNAME_LABEL = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/
7
+
8
+ export class HostnameNormalizationError extends Error {
9
+ constructor(message: string) {
10
+ super(message)
11
+ this.name = 'HostnameNormalizationError'
12
+ }
13
+ }
14
+
15
+ export function normalizeHostname(input: string): string {
16
+ if (typeof input !== 'string') {
17
+ throw new HostnameNormalizationError('Hostname must be a string')
18
+ }
19
+
20
+ let host = input.trim().toLowerCase()
21
+ if (!host) throw new HostnameNormalizationError('Hostname is empty')
22
+
23
+ host = host
24
+ .replace(/^https?:\/\//, '')
25
+ .replace(/\/.*$/, '')
26
+ .replace(/\?.*$/, '')
27
+ .replace(/#.*$/, '')
28
+ .replace(/:\d+$/, '')
29
+
30
+ if (host.endsWith('.')) host = host.slice(0, -1)
31
+
32
+ if (!host) throw new HostnameNormalizationError('Hostname is empty after stripping')
33
+
34
+ // Convert IDN (Unicode) to Punycode (ASCII) via the URL constructor — this
35
+ // matches what browsers and Let's Encrypt use, without depending on the
36
+ // deprecated `node:punycode` module or pulling in `tr46`.
37
+ try {
38
+ const url = new URL(`https://${host}`)
39
+ host = url.hostname
40
+ } catch {
41
+ throw new HostnameNormalizationError(`Hostname is not a valid host: ${input}`)
42
+ }
43
+
44
+ if (!host || host.length > MAX_HOSTNAME_LENGTH) {
45
+ throw new HostnameNormalizationError(
46
+ `Hostname must be between 1 and ${MAX_HOSTNAME_LENGTH} characters after normalization`,
47
+ )
48
+ }
49
+
50
+ const labels = host.split('.')
51
+ if (labels.length < 2) {
52
+ throw new HostnameNormalizationError('Hostname must have at least two labels (e.g. "example.com")')
53
+ }
54
+ for (const label of labels) {
55
+ if (!HOSTNAME_LABEL.test(label)) {
56
+ throw new HostnameNormalizationError(`Invalid DNS label: "${label}"`)
57
+ }
58
+ }
59
+ // Reject IP-address literals (e.g. "127.0.0.1"): a custom domain must be a
60
+ // registered DNS name, and a TLD is never all-numeric. This also keeps
61
+ // loopback/test hosts (127.0.0.1) from being mistaken for a tenant's
62
+ // branded domain by the routing layer.
63
+ if (/^\d+$/.test(labels[labels.length - 1])) {
64
+ throw new HostnameNormalizationError(`Hostname looks like an IP address, not a DNS name: ${input}`)
65
+ }
66
+
67
+ return host
68
+ }
69
+
70
+ export function tryNormalizeHostname(input: string): string | null {
71
+ try {
72
+ return normalizeHostname(input)
73
+ } catch {
74
+ return null
75
+ }
76
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Parse the `PLATFORM_DOMAINS` env into a normalized list of platform hostnames.
3
+ *
4
+ * Platform hosts are the deployment's own domains (admin app, marketing site,
5
+ * loopback). Custom-domain routing MUST treat platform hosts as non-tenant
6
+ * traffic, so domain registration, resolver lookups, and tenant-context
7
+ * fallbacks all share the same view of which hosts are platform-owned.
8
+ */
9
+ export function platformDomains(): string[] {
10
+ return (process.env.PLATFORM_DOMAINS ?? 'localhost,openmercato.com')
11
+ .split(',')
12
+ .map((s) => s.trim().toLowerCase())
13
+ .filter(Boolean)
14
+ }
@@ -0,0 +1,85 @@
1
+ // CIDR matching for known reverse-proxy IP ranges (Cloudflare, Fastly, custom).
2
+ // Used by the DNS verification fallback: if a domain's A record lands inside a
3
+ // known proxy range we fall back to a reverse-resolve over HTTPS, because the
4
+ // proxy hides the real origin from us.
5
+
6
+ const DEFAULT_CLOUDFLARE_RANGES = [
7
+ '173.245.48.0/20',
8
+ '103.21.244.0/22',
9
+ '103.22.200.0/22',
10
+ '103.31.4.0/22',
11
+ '141.101.64.0/18',
12
+ '108.162.192.0/18',
13
+ '190.93.240.0/20',
14
+ '188.114.96.0/20',
15
+ '197.234.240.0/22',
16
+ '198.41.128.0/17',
17
+ '162.158.0.0/15',
18
+ '104.16.0.0/13',
19
+ '104.24.0.0/14',
20
+ '172.64.0.0/13',
21
+ '131.0.72.0/22',
22
+ ]
23
+
24
+ type ParsedCidr = { network: number; mask: number; raw: string }
25
+
26
+ let cachedRanges: ParsedCidr[] | null = null
27
+
28
+ function parseIpv4(ip: string): number | null {
29
+ const parts = ip.split('.')
30
+ if (parts.length !== 4) return null
31
+ let result = 0
32
+ for (const part of parts) {
33
+ if (!/^\d+$/.test(part)) return null
34
+ const value = Number(part)
35
+ if (value < 0 || value > 255) return null
36
+ result = (result << 8) | value
37
+ }
38
+ // Force unsigned 32-bit
39
+ return result >>> 0
40
+ }
41
+
42
+ function parseCidr(cidr: string): ParsedCidr | null {
43
+ const [ip, prefixStr] = cidr.split('/')
44
+ if (!ip || !prefixStr) return null
45
+ const prefix = Number(prefixStr)
46
+ if (!Number.isFinite(prefix) || prefix < 0 || prefix > 32) return null
47
+ const ipNum = parseIpv4(ip)
48
+ if (ipNum === null) return null
49
+ const mask = prefix === 0 ? 0 : (0xffffffff << (32 - prefix)) >>> 0
50
+ const network = (ipNum & mask) >>> 0
51
+ return { network, mask, raw: cidr }
52
+ }
53
+
54
+ function loadRanges(): ParsedCidr[] {
55
+ if (cachedRanges) return cachedRanges
56
+ const fromEnv = (process.env.KNOWN_PROXY_IP_RANGES ?? '').trim()
57
+ const list = fromEnv
58
+ ? fromEnv.split(',').map((s) => s.trim()).filter(Boolean)
59
+ : DEFAULT_CLOUDFLARE_RANGES
60
+ cachedRanges = list.map(parseCidr).filter((v): v is ParsedCidr => v !== null)
61
+ return cachedRanges
62
+ }
63
+
64
+ export function isInKnownProxyRange(ip: string): boolean {
65
+ const ipNum = parseIpv4(ip)
66
+ if (ipNum === null) return false
67
+ for (const range of loadRanges()) {
68
+ if (((ipNum & range.mask) >>> 0) === range.network) return true
69
+ }
70
+ return false
71
+ }
72
+
73
+ export function detectProxy(ip: string): string | null {
74
+ if (!isInKnownProxyRange(ip)) return null
75
+ // For now we only ship Cloudflare's published ranges by default. Operators
76
+ // using a different proxy can override KNOWN_PROXY_IP_RANGES; we still
77
+ // return 'unknown' for those so callers can phrase the diagnostic correctly.
78
+ const fromEnv = process.env.KNOWN_PROXY_IP_RANGES
79
+ if (!fromEnv) return 'cloudflare'
80
+ return 'unknown'
81
+ }
82
+
83
+ export function resetProxyRangeCacheForTests(): void {
84
+ cachedRanges = null
85
+ }
@@ -33,4 +33,14 @@ export const customerMagicLinkIpRateLimitConfig = readEndpointRateLimitConfig('C
33
33
  points: 10, duration: 60, blockDuration: 120, keyPrefix: 'customer-magic-link-ip',
34
34
  })
35
35
 
36
+ // Bulk custom-domain warm-up endpoint (`/api/customer_accounts/domain-resolve/all`).
37
+ // Single shared `DOMAIN_RESOLVE_SECRET` gates a payload that lists every active
38
+ // custom-domain mapping in the deployment, so we cap requests per IP to make
39
+ // brute-force or post-leak enumeration noisy. 30 req/min/IP comfortably covers
40
+ // the Node middleware warm-up cadence (one call per process boot) while
41
+ // staying well below useful enumeration throughput.
42
+ export const domainResolveAllIpRateLimitConfig = readEndpointRateLimitConfig('DOMAIN_RESOLVE_ALL_IP', {
43
+ points: 30, duration: 60, blockDuration: 60, keyPrefix: 'domain-resolve-all',
44
+ })
45
+
36
46
  export { checkAuthRateLimit, resetAuthRateLimit }