@open-mercato/core 0.6.1-develop.3260.1.dc719cac84 → 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
@@ -1,6 +1,7 @@
1
1
  import type { ResponseEnricher, EnricherContext } from '@open-mercato/shared/lib/crud/response-enricher'
2
2
  import type { EntityManager } from '@mikro-orm/postgresql'
3
- import { CustomerUser } from './entities'
3
+ import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
4
+ import { CustomerUser, DomainMapping, type DomainStatus } from './entities'
4
5
 
5
6
  type EntityRecord = Record<string, unknown> & { id: string }
6
7
  type EnricherScope = EnricherContext & { em: EntityManager }
@@ -42,11 +43,17 @@ const personAccountStatusEnricher: ResponseEnricher<EntityRecord, PersonAccountE
42
43
  const em = context.em.fork()
43
44
  const recordIds = records.map((r) => r.id)
44
45
 
45
- const users = await em.find(CustomerUser, {
46
- personEntityId: { $in: recordIds },
47
- tenantId: context.tenantId,
48
- deletedAt: null,
49
- })
46
+ const users = await findWithDecryption(
47
+ em,
48
+ CustomerUser,
49
+ {
50
+ personEntityId: { $in: recordIds },
51
+ tenantId: context.tenantId,
52
+ deletedAt: null,
53
+ },
54
+ undefined,
55
+ { tenantId: context.tenantId, organizationId: context.organizationId },
56
+ )
50
57
 
51
58
  const userByPersonId = new Map<string, CustomerUser>()
52
59
  for (const user of users) {
@@ -92,11 +99,17 @@ const companyUserCountEnricher: ResponseEnricher<EntityRecord, CompanyUserCountE
92
99
  const em = context.em.fork()
93
100
  const recordIds = records.map((r) => r.id)
94
101
 
95
- const users = await em.find(CustomerUser, {
96
- customerEntityId: { $in: recordIds },
97
- tenantId: context.tenantId,
98
- deletedAt: null,
99
- })
102
+ const users = await findWithDecryption(
103
+ em,
104
+ CustomerUser,
105
+ {
106
+ customerEntityId: { $in: recordIds },
107
+ tenantId: context.tenantId,
108
+ deletedAt: null,
109
+ },
110
+ undefined,
111
+ { tenantId: context.tenantId, organizationId: context.organizationId },
112
+ )
100
113
 
101
114
  const countByCompanyId = new Map<string, number>()
102
115
  for (const user of users) {
@@ -114,4 +127,74 @@ const companyUserCountEnricher: ResponseEnricher<EntityRecord, CompanyUserCountE
114
127
  },
115
128
  }
116
129
 
117
- export const enrichers: ResponseEnricher[] = [personAccountStatusEnricher, companyUserCountEnricher]
130
+ type OrgCustomDomainEnrichment = {
131
+ _customDomain: { hostname: string; status: DomainStatus } | null
132
+ }
133
+
134
+ const ORG_DOMAIN_FALLBACK: OrgCustomDomainEnrichment = { _customDomain: null }
135
+
136
+ const ACTIVE_DOMAIN_PRIORITY: Record<DomainStatus, number> = {
137
+ active: 0,
138
+ verified: 1,
139
+ pending: 2,
140
+ tls_failed: 3,
141
+ dns_failed: 4,
142
+ }
143
+
144
+ function pickPrimaryDomain(records: DomainMapping[]): DomainMapping | null {
145
+ if (records.length === 0) return null
146
+ return records
147
+ .slice()
148
+ .sort(
149
+ (a, b) =>
150
+ (ACTIVE_DOMAIN_PRIORITY[a.status] ?? 99) - (ACTIVE_DOMAIN_PRIORITY[b.status] ?? 99) ||
151
+ (b.createdAt?.getTime?.() ?? 0) - (a.createdAt?.getTime?.() ?? 0),
152
+ )[0]
153
+ }
154
+
155
+ const organizationCustomDomainEnricher: ResponseEnricher<EntityRecord, OrgCustomDomainEnrichment> = {
156
+ id: 'customer_accounts.domain-status:directory:organization',
157
+ targetEntity: 'directory:organization',
158
+ features: ['customer_accounts.domain.manage'],
159
+ priority: 10,
160
+ timeout: 1_000,
161
+ critical: false,
162
+ fallback: ORG_DOMAIN_FALLBACK,
163
+
164
+ async enrichOne(record, context: EnricherScope) {
165
+ return (await this.enrichMany!([record], context))[0]
166
+ },
167
+
168
+ async enrichMany(records, context: EnricherScope) {
169
+ if (records.length === 0) return records as (EntityRecord & OrgCustomDomainEnrichment)[]
170
+
171
+ const em = context.em.fork()
172
+ const recordIds = records.map((r) => r.id)
173
+ const domains = await em.find(DomainMapping, {
174
+ organizationId: { $in: recordIds },
175
+ tenantId: context.tenantId,
176
+ } as never)
177
+
178
+ const byOrg = new Map<string, DomainMapping[]>()
179
+ for (const d of domains) {
180
+ const list = byOrg.get(d.organizationId) ?? []
181
+ list.push(d)
182
+ byOrg.set(d.organizationId, list)
183
+ }
184
+
185
+ return records.map((record) => {
186
+ const matches = byOrg.get(record.id) ?? []
187
+ const primary = pickPrimaryDomain(matches)
188
+ return {
189
+ ...record,
190
+ _customDomain: primary ? { hostname: primary.hostname, status: primary.status } : null,
191
+ }
192
+ })
193
+ },
194
+ }
195
+
196
+ export const enrichers: ResponseEnricher[] = [
197
+ personAccountStatusEnricher,
198
+ companyUserCountEnricher,
199
+ organizationCustomDomainEnricher,
200
+ ]
@@ -1,4 +1,16 @@
1
- import { Entity, Index, ManyToOne, PrimaryKey, Property, Unique } from '@mikro-orm/decorators/legacy'
1
+ import { Check, Entity, Enum, Index, ManyToOne, PrimaryKey, Property, Unique } from '@mikro-orm/decorators/legacy'
2
+
3
+ export type DomainProvider = 'traefik'
4
+ export type DomainStatus = 'pending' | 'verified' | 'active' | 'dns_failed' | 'tls_failed'
5
+
6
+ export const DOMAIN_PROVIDERS: readonly DomainProvider[] = ['traefik'] as const
7
+ export const DOMAIN_STATUSES: readonly DomainStatus[] = [
8
+ 'pending',
9
+ 'verified',
10
+ 'active',
11
+ 'dns_failed',
12
+ 'tls_failed',
13
+ ] as const
2
14
 
3
15
  @Entity({ tableName: 'customer_users' })
4
16
  @Unique({ properties: ['tenantId', 'emailHash'], name: 'customer_users_tenant_email_hash_uniq' })
@@ -303,3 +315,70 @@ export class CustomerUserInvitation {
303
315
  @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })
304
316
  createdAt: Date = new Date()
305
317
  }
318
+
319
+ @Entity({ tableName: 'domain_mappings' })
320
+ @Unique({ properties: ['hostname'], name: 'domain_mappings_hostname_unique' })
321
+ @Index({ properties: ['organizationId'], name: 'domain_mappings_organization_id_idx' })
322
+ @Index({ properties: ['tenantId'], name: 'domain_mappings_tenant_id_idx' })
323
+ @Unique({
324
+ name: 'domain_mappings_replaces_domain_id_unique',
325
+ expression:
326
+ `create unique index "domain_mappings_replaces_domain_id_unique" on "domain_mappings" ("replaces_domain_id") where "replaces_domain_id" is not null`,
327
+ })
328
+ @Index({
329
+ name: 'domain_mappings_pending_verification_idx',
330
+ expression:
331
+ `create index "domain_mappings_pending_verification_idx" on "domain_mappings" ("status", "last_dns_check_at") where "status" in ('pending', 'dns_failed')`,
332
+ })
333
+ @Index({
334
+ name: 'domain_mappings_pending_tls_idx',
335
+ expression:
336
+ `create index "domain_mappings_pending_tls_idx" on "domain_mappings" ("status", "updated_at") where "status" in ('verified', 'tls_failed')`,
337
+ })
338
+ @Check({
339
+ name: 'domain_mappings_hostname_normalized_chk',
340
+ expression: `"hostname" = lower("hostname") and "hostname" not like '%.'`,
341
+ })
342
+ export class DomainMapping {
343
+ @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })
344
+ id!: string
345
+
346
+ @Property({ type: 'text' })
347
+ hostname!: string
348
+
349
+ @Property({ name: 'tenant_id', type: 'uuid' })
350
+ tenantId!: string
351
+
352
+ @Property({ name: 'organization_id', type: 'uuid' })
353
+ organizationId!: string
354
+
355
+ @ManyToOne(() => DomainMapping, { fieldName: 'replaces_domain_id', nullable: true })
356
+ replacesDomain?: DomainMapping | null
357
+
358
+ @Enum({ items: () => DOMAIN_PROVIDERS as unknown as string[], type: 'text', name: 'provider' })
359
+ provider: DomainProvider = 'traefik'
360
+
361
+ @Enum({ items: () => DOMAIN_STATUSES as unknown as string[], type: 'text', name: 'status' })
362
+ status: DomainStatus = 'pending'
363
+
364
+ @Property({ name: 'verified_at', type: Date, nullable: true })
365
+ verifiedAt?: Date | null
366
+
367
+ @Property({ name: 'last_dns_check_at', type: Date, nullable: true })
368
+ lastDnsCheckAt?: Date | null
369
+
370
+ @Property({ name: 'dns_failure_reason', type: 'text', nullable: true, length: 500 })
371
+ dnsFailureReason?: string | null
372
+
373
+ @Property({ name: 'tls_failure_reason', type: 'text', nullable: true, length: 500 })
374
+ tlsFailureReason?: string | null
375
+
376
+ @Property({ name: 'tls_retry_count', type: 'int', default: 0 })
377
+ tlsRetryCount: number = 0
378
+
379
+ @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })
380
+ createdAt: Date = new Date()
381
+
382
+ @Property({ name: 'updated_at', type: Date, onUpdate: () => new Date(), nullable: true })
383
+ updatedAt?: Date | null
384
+ }
@@ -0,0 +1,155 @@
1
+ import type { MutationGuard } from '@open-mercato/shared/lib/crud/mutation-guard-registry'
2
+ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
3
+ import { tryNormalizeHostname } from '@open-mercato/core/modules/customer_accounts/lib/hostname'
4
+ import type { DomainMappingService } from '@open-mercato/core/modules/customer_accounts/services/domainMappingService'
5
+
6
+ const DOMAIN_MAPPING_ENTITY = 'customer_accounts.domain_mapping'
7
+ const MAX_DOMAINS_PER_ORG = 2
8
+
9
+ function readHostnameField(payload: Record<string, unknown> | null | undefined): string | null {
10
+ if (!payload || typeof payload !== 'object') return null
11
+ const raw = (payload as Record<string, unknown>).hostname
12
+ return typeof raw === 'string' ? raw : null
13
+ }
14
+
15
+ function readOrgIdField(payload: Record<string, unknown> | null | undefined): string | null {
16
+ if (!payload || typeof payload !== 'object') return null
17
+ const raw = (payload as Record<string, unknown>).organizationId ?? (payload as Record<string, unknown>).organization_id
18
+ return typeof raw === 'string' ? raw : null
19
+ }
20
+
21
+ const hostnameFormatGuard: MutationGuard = {
22
+ id: 'customer_accounts.domain_mapping.hostname-format',
23
+ targetEntity: DOMAIN_MAPPING_ENTITY,
24
+ operations: ['create'],
25
+ priority: 10,
26
+
27
+ async validate(input) {
28
+ const raw = readHostnameField(input.mutationPayload)
29
+ if (!raw) {
30
+ return { ok: false, status: 422, message: 'Hostname is required' }
31
+ }
32
+ const normalized = tryNormalizeHostname(raw)
33
+ if (!normalized) {
34
+ return {
35
+ ok: false,
36
+ status: 422,
37
+ message: 'Hostname is not a valid DNS name (must be at least two labels, ASCII or IDN)',
38
+ }
39
+ }
40
+ return {
41
+ ok: true,
42
+ modifiedPayload: { hostname: normalized },
43
+ }
44
+ },
45
+ }
46
+
47
+ const hostnameUniqueGuard: MutationGuard = {
48
+ id: 'customer_accounts.domain_mapping.hostname-unique',
49
+ targetEntity: DOMAIN_MAPPING_ENTITY,
50
+ operations: ['create'],
51
+ priority: 20,
52
+
53
+ async validate(input) {
54
+ const raw = readHostnameField(input.mutationPayload)
55
+ if (!raw) return { ok: true }
56
+ const hostname = tryNormalizeHostname(raw)
57
+ if (!hostname) return { ok: true }
58
+
59
+ const container = await createRequestContainer()
60
+ let service: DomainMappingService | null = null
61
+ try {
62
+ service = container.resolve('domainMappingService') as DomainMappingService
63
+ } catch {
64
+ return { ok: true }
65
+ }
66
+
67
+ const existing = await service.resolveByHostname(hostname)
68
+ if (!existing) return { ok: true }
69
+
70
+ // Intentionally tenant-agnostic message: do not disclose whether the
71
+ // collision is intra-tenant or cross-tenant. Distinguishing them would
72
+ // let any caller probe deployment-wide hostname registration.
73
+ const sameOrg = existing.organizationId === readOrgIdField(input.mutationPayload)
74
+ if (existing.tenantId !== input.tenantId || !sameOrg) {
75
+ return {
76
+ ok: false,
77
+ status: 409,
78
+ message: 'This domain is not available. Please choose a different hostname.',
79
+ }
80
+ }
81
+ return { ok: true }
82
+ },
83
+ }
84
+
85
+ const orgLimitGuard: MutationGuard = {
86
+ id: 'customer_accounts.domain_mapping.org-limit',
87
+ targetEntity: DOMAIN_MAPPING_ENTITY,
88
+ operations: ['create'],
89
+ priority: 30,
90
+
91
+ async validate(input) {
92
+ const orgId = readOrgIdField(input.mutationPayload) ?? input.organizationId
93
+ if (!orgId) {
94
+ return { ok: false, status: 422, message: 'Organization is required' }
95
+ }
96
+
97
+ const container = await createRequestContainer()
98
+ let service: DomainMappingService | null = null
99
+ try {
100
+ service = container.resolve('domainMappingService') as DomainMappingService
101
+ } catch {
102
+ return { ok: true }
103
+ }
104
+
105
+ const existing = await service.findByOrganization(orgId, { tenantId: input.tenantId })
106
+ if (existing.length >= MAX_DOMAINS_PER_ORG) {
107
+ return {
108
+ ok: false,
109
+ status: 409,
110
+ message: `Each organization can have at most ${MAX_DOMAINS_PER_ORG} custom domains (one active and one pending replacement). Remove an existing domain or finish the swap first.`,
111
+ }
112
+ }
113
+ return { ok: true }
114
+ },
115
+ }
116
+
117
+ // Mirrors the in-progress-lock pattern used by other modules' update/delete
118
+ // guards: ensure the target record exists and belongs to the caller's tenant
119
+ // before any destructive or lifecycle-changing mutation runs. Verification
120
+ // and health-check endpoints invoke this via operation: 'update' since they
121
+ // transition the record's lifecycle status.
122
+ const recordScopeGuard: MutationGuard = {
123
+ id: 'customer_accounts.domain_mapping.record-scope',
124
+ targetEntity: DOMAIN_MAPPING_ENTITY,
125
+ operations: ['update', 'delete'],
126
+ priority: 10,
127
+
128
+ async validate(input) {
129
+ const resourceId = input.resourceId
130
+ if (!resourceId) {
131
+ return { ok: false, status: 422, message: 'Domain mapping id is required' }
132
+ }
133
+
134
+ const container = await createRequestContainer()
135
+ let service: DomainMappingService | null = null
136
+ try {
137
+ service = container.resolve('domainMappingService') as DomainMappingService
138
+ } catch {
139
+ return { ok: true }
140
+ }
141
+
142
+ const record = await service.findById(resourceId, { tenantId: input.tenantId })
143
+ if (!record) {
144
+ return { ok: false, status: 404, message: 'Domain mapping not found' }
145
+ }
146
+ return { ok: true }
147
+ },
148
+ }
149
+
150
+ export const guards: MutationGuard[] = [
151
+ hostnameFormatGuard,
152
+ hostnameUniqueGuard,
153
+ orgLimitGuard,
154
+ recordScopeGuard,
155
+ ]
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod'
2
+ import { normalizeHostname } from '@open-mercato/core/modules/customer_accounts/lib/hostname'
2
3
 
3
4
  const emailField = z.string().email().max(255)
4
5
  const passwordField = z.string().min(8).max(128)
@@ -126,3 +127,28 @@ export type UpdateRoleAclInput = z.infer<typeof updateRoleAclSchema>
126
127
  export type InviteUserInput = z.infer<typeof inviteUserSchema>
127
128
  export type AssignRolesInput = z.infer<typeof assignRolesSchema>
128
129
  export type AdminUpdateUserInput = z.infer<typeof adminUpdateUserSchema>
130
+
131
+ // Custom domain mapping
132
+ export const hostnameSchema = z
133
+ .string()
134
+ .min(1, 'Hostname is required')
135
+ .max(512, 'Hostname is too long')
136
+ .transform((value, ctx) => {
137
+ try {
138
+ return normalizeHostname(value)
139
+ } catch (err) {
140
+ ctx.addIssue({
141
+ code: z.ZodIssueCode.custom,
142
+ message: err instanceof Error ? err.message : 'Invalid hostname',
143
+ })
144
+ return z.NEVER
145
+ }
146
+ })
147
+
148
+ export const registerDomainSchema = z.object({
149
+ hostname: hostnameSchema,
150
+ organizationId: z.string().uuid(),
151
+ replacesDomainId: z.string().uuid().optional(),
152
+ })
153
+
154
+ export type RegisterDomainInput = z.infer<typeof registerDomainSchema>
@@ -1,10 +1,24 @@
1
- import { asClass } from 'awilix'
1
+ import { asClass, asFunction } from 'awilix'
2
+ import type { EntityManager } from '@mikro-orm/postgresql'
2
3
  import type { AppContainer } from '@open-mercato/shared/lib/di/container'
3
4
  import { CustomerUserService } from '@open-mercato/core/modules/customer_accounts/services/customerUserService'
4
5
  import { CustomerSessionService } from '@open-mercato/core/modules/customer_accounts/services/customerSessionService'
5
6
  import { CustomerTokenService } from '@open-mercato/core/modules/customer_accounts/services/customerTokenService'
6
7
  import { CustomerRbacService } from '@open-mercato/core/modules/customer_accounts/services/customerRbacService'
7
8
  import { CustomerInvitationService } from '@open-mercato/core/modules/customer_accounts/services/customerInvitationService'
9
+ import { DomainMappingService } from '@open-mercato/core/modules/customer_accounts/services/domainMappingService'
10
+
11
+ type DomainCacheService = {
12
+ get: (key: string, options?: unknown) => Promise<unknown>
13
+ set: (key: string, value: unknown, options?: { ttl?: number; tags?: string[] }) => Promise<void>
14
+ deleteByTags: (tags: string[]) => Promise<number>
15
+ }
16
+
17
+ function isDomainCacheService(candidate: unknown): candidate is DomainCacheService {
18
+ if (!candidate || typeof candidate !== 'object') return false
19
+ const c = candidate as Record<string, unknown>
20
+ return typeof c.get === 'function' && typeof c.set === 'function' && typeof c.deleteByTags === 'function'
21
+ }
8
22
 
9
23
  export function register(container: AppContainer) {
10
24
  container.register({ customerUserService: asClass(CustomerUserService).scoped() })
@@ -12,4 +26,24 @@ export function register(container: AppContainer) {
12
26
  container.register({ customerTokenService: asClass(CustomerTokenService).scoped() })
13
27
  container.register({ customerRbacService: asClass(CustomerRbacService).scoped() })
14
28
  container.register({ customerInvitationService: asClass(CustomerInvitationService).scoped() })
29
+ container.register({
30
+ domainMappingService: asFunction(
31
+ function domainMappingServiceFactory(em: EntityManager) {
32
+ // Resolve the cache lazily so registrations from `bootstrap(container)` (which runs after
33
+ // module DI registrars) are visible. Awilix CLASSIC mode would fail to resolve a
34
+ // destructured `cache` param if the key is not yet registered; using `hasRegistration`
35
+ // keeps construction safe for CLI/test contexts where `cache` may never be wired.
36
+ let cacheService: DomainCacheService | undefined
37
+ try {
38
+ if (container.hasRegistration('cache')) {
39
+ const candidate = container.resolve('cache')
40
+ if (isDomainCacheService(candidate)) cacheService = candidate
41
+ }
42
+ } catch {
43
+ cacheService = undefined
44
+ }
45
+ return new DomainMappingService(em, cacheService ? { cacheService } : undefined)
46
+ },
47
+ ).scoped(),
48
+ })
15
49
  }
@@ -17,8 +17,15 @@ const events = [
17
17
  { id: 'customer_accounts.role.updated', label: 'Customer Role Updated', entity: 'role', category: 'crud', portalBroadcast: true },
18
18
  { id: 'customer_accounts.role.deleted', label: 'Customer Role Deleted', entity: 'role', category: 'crud' },
19
19
  { id: 'customer_accounts.invitation.accepted', label: 'Customer Invitation Accepted', category: 'lifecycle', clientBroadcast: true },
20
- { id: 'customer_accounts.magic_link.requested', label: 'Customer Magic Link Requested', category: 'lifecycle' },
21
20
  { id: 'customer_accounts.password_reset.requested', label: 'Customer Password Reset Requested', category: 'lifecycle' },
21
+ // Custom domain mapping lifecycle (see .ai/specs/2026-04-08-portal-custom-domain-routing.md)
22
+ { id: 'customer_accounts.domain_mapping.created', label: 'Custom Domain Registered', entity: 'domain_mapping', category: 'crud', clientBroadcast: true },
23
+ { id: 'customer_accounts.domain_mapping.verified', label: 'Custom Domain DNS Verified', entity: 'domain_mapping', category: 'lifecycle', clientBroadcast: true },
24
+ { id: 'customer_accounts.domain_mapping.activated', label: 'Custom Domain Active', entity: 'domain_mapping', category: 'lifecycle', clientBroadcast: true },
25
+ { id: 'customer_accounts.domain_mapping.dns_failed', label: 'Custom Domain DNS Verification Failed', entity: 'domain_mapping', category: 'lifecycle', clientBroadcast: true },
26
+ { id: 'customer_accounts.domain_mapping.tls_failed', label: 'Custom Domain SSL Provisioning Failed', entity: 'domain_mapping', category: 'lifecycle', clientBroadcast: true },
27
+ { id: 'customer_accounts.domain_mapping.deleted', label: 'Custom Domain Removed', entity: 'domain_mapping', category: 'crud', clientBroadcast: true },
28
+ { id: 'customer_accounts.domain_mapping.replaced', label: 'Custom Domain Replaced', entity: 'domain_mapping', category: 'lifecycle', clientBroadcast: true },
22
29
  ] as const
23
30
 
24
31
  export const eventsConfig = createModuleEvents({
@@ -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": "Kundenrollen",
199
269
  "customer_accounts.nav.user_detail": "User Detail",
200
270
  "customer_accounts.nav.users": "Kundenbenutzer",
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.",