@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
@@ -15,6 +15,76 @@ interface SeedScope {
15
15
  organizationId: string
16
16
  }
17
17
 
18
+ type SchedulerServiceLike = {
19
+ register: (registration: {
20
+ id: string
21
+ name: string
22
+ description?: string
23
+ scopeType: 'system'
24
+ scheduleType: 'interval'
25
+ scheduleValue: string
26
+ timezone?: string
27
+ targetType: 'queue'
28
+ targetQueue: string
29
+ targetPayload?: Record<string, unknown>
30
+ sourceType: 'module'
31
+ sourceModule: string
32
+ isEnabled?: boolean
33
+ }) => Promise<void>
34
+ }
35
+
36
+ // Stable, deterministic UUIDs for the two domain-routing system schedules so
37
+ // re-running setup re-upserts the same rows instead of creating duplicates.
38
+ const DOMAIN_VERIFICATION_SCHEDULE_ID = '5e9ef5fc-1f4d-5b3d-8b58-8e1a5a4d1001'
39
+ const DOMAIN_TLS_RETRY_SCHEDULE_ID = '5e9ef5fc-1f4d-5b3d-8b58-8e1a5a4d1002'
40
+
41
+ function intervalSecondsFromEnv(envName: string, fallbackSeconds: number): string {
42
+ const parsed = Number.parseInt(process.env[envName] ?? '', 10)
43
+ const seconds = Number.isFinite(parsed) && parsed > 0 ? parsed : fallbackSeconds
44
+ return `${seconds}s`
45
+ }
46
+
47
+ async function registerDomainSchedules(
48
+ container: { hasRegistration?: (name: string) => boolean; resolve: (name: string) => unknown },
49
+ ): Promise<void> {
50
+ if (typeof container.hasRegistration !== 'function' || !container.hasRegistration('schedulerService')) {
51
+ return
52
+ }
53
+ const schedulerService = container.resolve('schedulerService') as SchedulerServiceLike
54
+
55
+ await schedulerService.register({
56
+ id: DOMAIN_VERIFICATION_SCHEDULE_ID,
57
+ name: 'Custom domain — DNS auto-verification',
58
+ description:
59
+ 'Re-runs DNS verification for custom-domain mappings stuck in pending or dns_failed, and triggers TLS health checks on success.',
60
+ scopeType: 'system',
61
+ scheduleType: 'interval',
62
+ scheduleValue: intervalSecondsFromEnv('DOMAIN_AUTO_VERIFY_INTERVAL_SECONDS', 300),
63
+ timezone: 'UTC',
64
+ targetType: 'queue',
65
+ targetQueue: 'domain-verification',
66
+ sourceType: 'module',
67
+ sourceModule: 'customer_accounts',
68
+ isEnabled: true,
69
+ })
70
+
71
+ await schedulerService.register({
72
+ id: DOMAIN_TLS_RETRY_SCHEDULE_ID,
73
+ name: 'Custom domain — TLS retry',
74
+ description:
75
+ 'Retries TLS certificate provisioning for verified or tls_failed custom-domain mappings, with batch cap and adaptive backoff on Let\'s Encrypt rate limits.',
76
+ scopeType: 'system',
77
+ scheduleType: 'interval',
78
+ scheduleValue: intervalSecondsFromEnv('DOMAIN_TLS_RETRY_INTERVAL_SECONDS', 1800),
79
+ timezone: 'UTC',
80
+ targetType: 'queue',
81
+ targetQueue: 'domain-tls-retry',
82
+ sourceType: 'module',
83
+ sourceModule: 'customer_accounts',
84
+ isEnabled: true,
85
+ })
86
+ }
87
+
18
88
  const DEFAULT_ROLES = [
19
89
  {
20
90
  name: 'Portal Admin',
@@ -155,7 +225,7 @@ export const setup: ModuleSetupConfig = {
155
225
  await seedDefaultRoles(em, { tenantId, organizationId })
156
226
  },
157
227
 
158
- async seedDefaults({ em, tenantId, organizationId }) {
228
+ async seedDefaults({ em, tenantId, organizationId, container }) {
159
229
  await seedDefaultRoles(em, { tenantId, organizationId })
160
230
  // Merge defaultCustomerRoleFeatures from all enabled modules
161
231
  try {
@@ -165,6 +235,16 @@ export const setup: ModuleSetupConfig = {
165
235
  } catch {
166
236
  // Modules may not be registered yet during initial setup
167
237
  }
238
+
239
+ // System-scoped schedules: register is idempotent, so re-running setup
240
+ // (or running it for additional tenants) just upserts the same two rows.
241
+ try {
242
+ await registerDomainSchedules(
243
+ container as { hasRegistration?: (name: string) => boolean; resolve: (name: string) => unknown },
244
+ )
245
+ } catch {
246
+ // Scheduler may not be installed in this deployment; ignore.
247
+ }
168
248
  },
169
249
 
170
250
  async seedExamples({ em, tenantId, organizationId }) {
@@ -0,0 +1,35 @@
1
+ type CacheService = {
2
+ deleteByTags(tags: string[]): Promise<number>
3
+ }
4
+
5
+ export const metadata = {
6
+ event: 'customer_accounts.domain_mapping.*',
7
+ persistent: false,
8
+ id: 'customer_accounts:invalidate-domain-cache',
9
+ }
10
+
11
+ export default async function handle(
12
+ payload: unknown,
13
+ ctx: { resolve: <T = unknown>(name: string) => T },
14
+ ): Promise<void> {
15
+ const data = (payload ?? {}) as Record<string, unknown>
16
+ const hostname = typeof data.hostname === 'string' ? data.hostname : null
17
+ const organizationId = typeof data.organizationId === 'string' ? data.organizationId : null
18
+
19
+ let cache: CacheService | null = null
20
+ try {
21
+ cache = ctx.resolve<CacheService>('cache')
22
+ } catch {
23
+ return
24
+ }
25
+ if (!cache) return
26
+
27
+ const tags = ['domain_routing']
28
+ if (hostname) tags.push(`domain_routing:${hostname}`)
29
+ if (organizationId) tags.push(`domain_routing:org:${organizationId}`)
30
+ try {
31
+ await cache.deleteByTags(tags)
32
+ } catch {
33
+ // Best-effort cache invalidation. TTL is the backstop.
34
+ }
35
+ }
@@ -0,0 +1,32 @@
1
+ import type { EventBus } from '@open-mercato/events/types'
2
+
3
+ export const metadata = {
4
+ event: 'customer_accounts.domain_mapping.activated',
5
+ persistent: true,
6
+ id: 'customer_accounts:notify-domain-activated',
7
+ }
8
+
9
+ export default async function handle(
10
+ payload: unknown,
11
+ ctx: { resolve: <T = unknown>(name: string) => T },
12
+ ): Promise<void> {
13
+ const data = (payload ?? {}) as Record<string, unknown>
14
+ const id = typeof data.id === 'string' ? data.id : null
15
+ const tenantId = typeof data.tenantId === 'string' ? data.tenantId : null
16
+ const organizationId = typeof data.organizationId === 'string' ? data.organizationId : null
17
+ const hostname = typeof data.hostname === 'string' ? data.hostname : ''
18
+ if (!id || !tenantId || !organizationId) return
19
+
20
+ try {
21
+ const eventBus = ctx.resolve<EventBus>('eventBus')
22
+ await eventBus.emitEvent('notifications.create', {
23
+ type: 'customer_accounts.domain_mapping.activated',
24
+ tenantId,
25
+ organizationId,
26
+ sourceEntityId: id,
27
+ data: { hostname },
28
+ })
29
+ } catch {
30
+ // Notifications module may be absent — best-effort.
31
+ }
32
+ }
@@ -0,0 +1,33 @@
1
+ import type { EventBus } from '@open-mercato/events/types'
2
+
3
+ export const metadata = {
4
+ event: 'customer_accounts.domain_mapping.dns_failed',
5
+ persistent: true,
6
+ id: 'customer_accounts:notify-domain-dns-failed',
7
+ }
8
+
9
+ export default async function handle(
10
+ payload: unknown,
11
+ ctx: { resolve: <T = unknown>(name: string) => T },
12
+ ): Promise<void> {
13
+ const data = (payload ?? {}) as Record<string, unknown>
14
+ const id = typeof data.id === 'string' ? data.id : null
15
+ const tenantId = typeof data.tenantId === 'string' ? data.tenantId : null
16
+ const organizationId = typeof data.organizationId === 'string' ? data.organizationId : null
17
+ const hostname = typeof data.hostname === 'string' ? data.hostname : ''
18
+ const reason = typeof data.reason === 'string' ? data.reason : ''
19
+ if (!id || !tenantId || !organizationId) return
20
+
21
+ try {
22
+ const eventBus = ctx.resolve<EventBus>('eventBus')
23
+ await eventBus.emitEvent('notifications.create', {
24
+ type: 'customer_accounts.domain_mapping.dns_failed',
25
+ tenantId,
26
+ organizationId,
27
+ sourceEntityId: id,
28
+ data: { hostname, reason },
29
+ })
30
+ } catch {
31
+ // Notifications module may be absent — best-effort.
32
+ }
33
+ }
@@ -0,0 +1,34 @@
1
+ import type { EventBus } from '@open-mercato/events/types'
2
+
3
+ export const metadata = {
4
+ event: 'customer_accounts.domain_mapping.tls_failed',
5
+ persistent: true,
6
+ id: 'customer_accounts:notify-domain-tls-failed',
7
+ }
8
+
9
+ export default async function handle(
10
+ payload: unknown,
11
+ ctx: { resolve: <T = unknown>(name: string) => T },
12
+ ): Promise<void> {
13
+ const data = (payload ?? {}) as Record<string, unknown>
14
+ const id = typeof data.id === 'string' ? data.id : null
15
+ const tenantId = typeof data.tenantId === 'string' ? data.tenantId : null
16
+ const organizationId = typeof data.organizationId === 'string' ? data.organizationId : null
17
+ const hostname = typeof data.hostname === 'string' ? data.hostname : ''
18
+ const reason = typeof data.reason === 'string' ? data.reason : ''
19
+ const retryCount = typeof data.retryCount === 'number' ? data.retryCount : 0
20
+ if (!id || !tenantId || !organizationId) return
21
+
22
+ try {
23
+ const eventBus = ctx.resolve<EventBus>('eventBus')
24
+ await eventBus.emitEvent('notifications.create', {
25
+ type: 'customer_accounts.domain_mapping.tls_failed',
26
+ tenantId,
27
+ organizationId,
28
+ sourceEntityId: id,
29
+ data: { hostname, reason, retryCount },
30
+ })
31
+ } catch {
32
+ // Notifications module may be absent — best-effort.
33
+ }
34
+ }
@@ -0,0 +1,32 @@
1
+ import type { EventBus } from '@open-mercato/events/types'
2
+
3
+ export const metadata = {
4
+ event: 'customer_accounts.domain_mapping.verified',
5
+ persistent: true,
6
+ id: 'customer_accounts:notify-domain-verified',
7
+ }
8
+
9
+ export default async function handle(
10
+ payload: unknown,
11
+ ctx: { resolve: <T = unknown>(name: string) => T },
12
+ ): Promise<void> {
13
+ const data = (payload ?? {}) as Record<string, unknown>
14
+ const id = typeof data.id === 'string' ? data.id : null
15
+ const tenantId = typeof data.tenantId === 'string' ? data.tenantId : null
16
+ const organizationId = typeof data.organizationId === 'string' ? data.organizationId : null
17
+ const hostname = typeof data.hostname === 'string' ? data.hostname : ''
18
+ if (!id || !tenantId || !organizationId) return
19
+
20
+ try {
21
+ const eventBus = ctx.resolve<EventBus>('eventBus')
22
+ await eventBus.emitEvent('notifications.create', {
23
+ type: 'customer_accounts.domain_mapping.verified',
24
+ tenantId,
25
+ organizationId,
26
+ sourceEntityId: id,
27
+ data: { hostname },
28
+ })
29
+ } catch {
30
+ // Notifications module may be absent — best-effort.
31
+ }
32
+ }
@@ -0,0 +1,84 @@
1
+ import type { JobContext, QueuedJob, WorkerMeta } from '@open-mercato/queue'
2
+ import type { DomainMappingService } from '@open-mercato/core/modules/customer_accounts/services/domainMappingService'
3
+
4
+ export const metadata: WorkerMeta = {
5
+ queue: 'domain-tls-retry',
6
+ id: 'customer_accounts:domain-tls-retry',
7
+ concurrency: 1,
8
+ }
9
+
10
+ type HandlerContext = JobContext & {
11
+ resolve: <T = unknown>(name: string) => T
12
+ }
13
+
14
+ function parseInt32(value: string | undefined, fallback: number): number {
15
+ const parsed = Number.parseInt(value ?? '', 10)
16
+ if (!Number.isFinite(parsed) || parsed <= 0) return fallback
17
+ return parsed
18
+ }
19
+
20
+ function parseFloat01(value: string | undefined, fallback: number): number {
21
+ const parsed = Number.parseFloat(value ?? '')
22
+ if (!Number.isFinite(parsed) || parsed <= 0 || parsed > 1) return fallback
23
+ return parsed
24
+ }
25
+
26
+ // In-process adaptive-backoff state. Worker is concurrency=1, so a single
27
+ // process owns this state. Surviving restarts is unnecessary because the
28
+ // scheduler will simply re-evaluate on the next tick. See spec
29
+ // "Phase 5 → Worker-level backoff" for the contract.
30
+ let backoffMultiplier = 1
31
+ let nextEligibleAt = 0
32
+
33
+ const BACKOFF_CAP_HOURS = 6
34
+
35
+ export default async function handle(_job: QueuedJob, ctx: HandlerContext): Promise<void> {
36
+ const baseIntervalSeconds = parseInt32(process.env.DOMAIN_TLS_RETRY_INTERVAL_SECONDS, 1800)
37
+ const batchSize = parseInt32(process.env.DOMAIN_TLS_RETRY_BATCH, 50)
38
+ const maxRetries = parseInt32(process.env.DOMAIN_TLS_MAX_RETRIES, 6)
39
+ const failureThreshold = parseFloat01(process.env.DOMAIN_TLS_RETRY_FAILURE_THRESHOLD, 0.8)
40
+ const capSeconds = BACKOFF_CAP_HOURS * 3600
41
+ const maxMultiplier = Math.max(1, Math.floor(capSeconds / baseIntervalSeconds))
42
+
43
+ if (Date.now() < nextEligibleAt) return
44
+
45
+ const service = ctx.resolve<DomainMappingService>('domainMappingService')
46
+ const candidates = await service.findPendingTls({ maxRetries, batchSize })
47
+ if (candidates.length === 0) {
48
+ backoffMultiplier = 1
49
+ nextEligibleAt = 0
50
+ return
51
+ }
52
+
53
+ let attempted = 0
54
+ let failed = 0
55
+ for (const mapping of candidates) {
56
+ attempted += 1
57
+ try {
58
+ const updated = await service.healthCheck(mapping.id)
59
+ if (updated.status !== 'active') failed += 1
60
+ } catch {
61
+ failed += 1
62
+ }
63
+ }
64
+
65
+ if (attempted === 0) return
66
+ const failureRate = failed / attempted
67
+
68
+ if (failureRate >= failureThreshold) {
69
+ backoffMultiplier = Math.min(backoffMultiplier * 2, maxMultiplier)
70
+ nextEligibleAt = Date.now() + backoffMultiplier * baseIntervalSeconds * 1000
71
+ } else {
72
+ backoffMultiplier = 1
73
+ nextEligibleAt = 0
74
+ }
75
+ }
76
+
77
+ // Test-only export so unit tests can reset state between runs.
78
+ export const __testing__ = {
79
+ reset: () => {
80
+ backoffMultiplier = 1
81
+ nextEligibleAt = 0
82
+ },
83
+ getState: () => ({ backoffMultiplier, nextEligibleAt }),
84
+ }
@@ -0,0 +1,45 @@
1
+ import type { JobContext, QueuedJob, WorkerMeta } from '@open-mercato/queue'
2
+ import type { DomainMappingService } from '@open-mercato/core/modules/customer_accounts/services/domainMappingService'
3
+
4
+ export const metadata: WorkerMeta = {
5
+ queue: 'domain-verification',
6
+ id: 'customer_accounts:domain-verification',
7
+ concurrency: 1,
8
+ }
9
+
10
+ type HandlerContext = JobContext & {
11
+ resolve: <T = unknown>(name: string) => T
12
+ }
13
+
14
+ function parseSeconds(value: string | undefined, fallback: number): number {
15
+ const parsed = Number.parseInt(value ?? '', 10)
16
+ if (!Number.isFinite(parsed) || parsed <= 0) return fallback
17
+ return parsed
18
+ }
19
+
20
+ export default async function handle(_job: QueuedJob, ctx: HandlerContext): Promise<void> {
21
+ const service = ctx.resolve<DomainMappingService>('domainMappingService')
22
+
23
+ const intervalSeconds = parseSeconds(process.env.DOMAIN_AUTO_VERIFY_INTERVAL_SECONDS, 300)
24
+ const olderThanMs = intervalSeconds * 1000
25
+
26
+ const candidates = await service.findPendingVerification({ olderThanMs })
27
+ if (candidates.length === 0) return
28
+
29
+ for (const mapping of candidates) {
30
+ try {
31
+ const result = await service.verify(mapping.id)
32
+ if (result.domainMapping.status === 'verified') {
33
+ try {
34
+ await service.healthCheck(mapping.id)
35
+ } catch {
36
+ // healthCheck emits its own events on failure; swallow here so one
37
+ // bad domain doesn't poison the rest of the batch.
38
+ }
39
+ }
40
+ } catch {
41
+ // verify() emits dns_failed events on negative results. A thrown error
42
+ // here is unexpected (e.g. DB blip) — skip and let the next tick retry.
43
+ }
44
+ }
45
+ }
@@ -14,7 +14,6 @@ import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
14
14
  import {
15
15
  applyEntityIdExclusion,
16
16
  applyEntityIdRestriction,
17
- consumeAdvancedFilterState,
18
17
  findMatchingEntityIdsWithQueryEngine,
19
18
  findMatchingEntityIdsBySearchTokensAcrossSources,
20
19
  withScopedPayload,
@@ -27,7 +26,7 @@ import {
27
26
  import { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'
28
27
  import { parseBooleanToken } from '@open-mercato/shared/lib/boolean'
29
28
  import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
30
- import { mergeAdvancedFilters } from '@open-mercato/shared/lib/crud/advanced-filter-integration'
29
+ import { consumeAdvancedFilterState, mergeAdvancedFilterTree } from '@open-mercato/shared/lib/crud/advanced-filter-integration'
31
30
  import {
32
31
  createCustomersCrudOpenApi,
33
32
  createPagedListResponseSchema,
@@ -119,8 +118,7 @@ const crud = makeCrudRoute({
119
118
  updatedAt: 'updated_at',
120
119
  },
121
120
  buildFilters: async (query, ctx) => {
122
- const advancedQuery = { ...query }
123
- const advancedFilterState = consumeAdvancedFilterState(query)
121
+ const advancedFilterTree = consumeAdvancedFilterState(query)
124
122
  const filters: Record<string, unknown> = { kind: { $eq: 'company' } }
125
123
  if (query.id) filters.id = { $eq: query.id }
126
124
  if (query.search) {
@@ -313,11 +311,8 @@ const crud = makeCrudRoute({
313
311
  console.warn('[customers.companies.list] custom field filter resolution failed; falling back to base filters', err)
314
312
  }
315
313
  }
316
- if (ctx && advancedFilterState) {
317
- const advancedFilters = mergeAdvancedFilters(
318
- { ...filters },
319
- advancedQuery as Record<string, unknown>,
320
- )
314
+ if (ctx && advancedFilterTree) {
315
+ const advancedFilters = mergeAdvancedFilterTree({ ...filters }, advancedFilterTree)
321
316
  const matchedIds = await findMatchingEntityIdsWithQueryEngine({
322
317
  ctx,
323
318
  entityId: E.customers.customer_entity,
@@ -7,7 +7,6 @@ import { E } from '#generated/entities.ids.generated'
7
7
  import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
8
8
  import {
9
9
  applyEntityIdRestriction,
10
- consumeAdvancedFilterState,
11
10
  findMatchingEntityIdsWithQueryEngine,
12
11
  findMatchingEntityIdsBySearchTokensAcrossSources,
13
12
  parseScopedCommandInput,
@@ -20,7 +19,7 @@ import {
20
19
  } from '../openapi'
21
20
  import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
22
21
  import { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'
23
- import { mergeAdvancedFilters } from '@open-mercato/shared/lib/crud/advanced-filter-integration'
22
+ import { consumeAdvancedFilterState, mergeAdvancedFilterTree } from '@open-mercato/shared/lib/crud/advanced-filter-integration'
24
23
 
25
24
  const rawBodySchema = z.object({}).passthrough()
26
25
 
@@ -133,8 +132,7 @@ const crud = makeCrudRoute<unknown, unknown, DealListQuery>({
133
132
  },
134
133
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
135
134
  buildFilters: async (query: any, ctx) => {
136
- const advancedQuery = { ...query }
137
- const advancedFilterState = consumeAdvancedFilterState(query)
135
+ const advancedFilterTree = consumeAdvancedFilterState(query)
138
136
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
139
137
  const filters: Record<string, any> = {}
140
138
  if (query.search) {
@@ -182,11 +180,8 @@ const crud = makeCrudRoute<unknown, unknown, DealListQuery>({
182
180
  if (query.pipelineStageId) {
183
181
  filters.pipeline_stage_id = { $eq: query.pipelineStageId }
184
182
  }
185
- if (ctx && advancedFilterState) {
186
- const advancedFilters = mergeAdvancedFilters(
187
- { ...filters },
188
- advancedQuery as Record<string, unknown>,
189
- )
183
+ if (ctx && advancedFilterTree) {
184
+ const advancedFilters = mergeAdvancedFilterTree({ ...filters }, advancedFilterTree)
190
185
  const matchedIds = await findMatchingEntityIdsWithQueryEngine({
191
186
  ctx,
192
187
  entityId: E.customers.customer_deal,
@@ -14,7 +14,6 @@ import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
14
14
  import {
15
15
  applyEntityIdExclusion,
16
16
  applyEntityIdRestriction,
17
- consumeAdvancedFilterState,
18
17
  findMatchingEntityIdsWithQueryEngine,
19
18
  findMatchingEntityIdsBySearchTokensAcrossSources,
20
19
  withScopedPayload,
@@ -23,7 +22,7 @@ import { buildCustomFieldFiltersFromQuery, extractAllCustomFieldEntries, splitCu
23
22
  import { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'
24
23
  import { parseBooleanToken } from '@open-mercato/shared/lib/boolean'
25
24
  import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
26
- import { mergeAdvancedFilters } from '@open-mercato/shared/lib/crud/advanced-filter-integration'
25
+ import { consumeAdvancedFilterState, mergeAdvancedFilterTree } from '@open-mercato/shared/lib/crud/advanced-filter-integration'
27
26
  import {
28
27
  createCustomersCrudOpenApi,
29
28
  createPagedListResponseSchema,
@@ -112,8 +111,7 @@ const crud = makeCrudRoute({
112
111
  updatedAt: 'updated_at',
113
112
  },
114
113
  buildFilters: async (query, ctx) => {
115
- const advancedQuery = { ...query }
116
- const advancedFilterState = consumeAdvancedFilterState(query)
114
+ const advancedFilterTree = consumeAdvancedFilterState(query)
117
115
  const filters: Record<string, unknown> = { kind: { $eq: 'person' } }
118
116
  if (query.id) filters.id = { $eq: query.id }
119
117
  if (query.search) {
@@ -304,11 +302,8 @@ const crud = makeCrudRoute({
304
302
  console.warn('[customers.people.list] custom field filter resolution failed; falling back to base filters', err)
305
303
  }
306
304
  }
307
- if (ctx && advancedFilterState) {
308
- const advancedFilters = mergeAdvancedFilters(
309
- { ...filters },
310
- advancedQuery as Record<string, unknown>,
311
- )
305
+ if (ctx && advancedFilterTree) {
306
+ const advancedFilters = mergeAdvancedFilterTree({ ...filters }, advancedFilterTree)
312
307
  const matchedIds = await findMatchingEntityIdsWithQueryEngine({
313
308
  ctx,
314
309
  entityId: E.customers.customer_entity,
@@ -6,7 +6,6 @@ import type { EntityId } from '@open-mercato/shared/modules/entities'
6
6
  import type { QueryCustomFieldSource, QueryJoinEdge, QueryEngine } from '@open-mercato/shared/lib/query/types'
7
7
  import { resolveSearchConfig } from '@open-mercato/shared/lib/search/config'
8
8
  import { tokenizeText } from '@open-mercato/shared/lib/search/tokenize'
9
- import { deserializeAdvancedFilter } from '@open-mercato/shared/lib/query/advanced-filter'
10
9
  import { SortDir } from '@open-mercato/shared/lib/query/types'
11
10
 
12
11
  const { withScopedPayload, parseScopedCommandInput } = createScopedApiHelpers({
@@ -278,19 +277,6 @@ export function applyEntityIdExclusion(
278
277
  }
279
278
  }
280
279
 
281
- export function consumeAdvancedFilterState(query: Record<string, unknown>) {
282
- const state = deserializeAdvancedFilter(query)
283
- if (!state) return null
284
-
285
- for (const key of Object.keys(query)) {
286
- if (key.startsWith('filter[')) {
287
- delete query[key]
288
- }
289
- }
290
-
291
- return state
292
- }
293
-
294
280
  export async function findMatchingEntityIdsWithQueryEngine({
295
281
  ctx,
296
282
  entityId,