@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
@@ -0,0 +1,303 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { z } from 'zod'
3
+ import { UniqueConstraintViolationException } from '@mikro-orm/core'
4
+ import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
5
+ import type { EntityManager } from '@mikro-orm/postgresql'
6
+ import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
7
+ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
8
+ import { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
9
+ import { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'
10
+ import {
11
+ validateCrudMutationGuard,
12
+ runCrudMutationGuardAfterSuccess,
13
+ } from '@open-mercato/shared/lib/crud/mutation-guard'
14
+ import { registerDomainSchema } from '@open-mercato/core/modules/customer_accounts/data/validators'
15
+ import {
16
+ DomainMappingService,
17
+ type ResolveResult,
18
+ } from '@open-mercato/core/modules/customer_accounts/services/domainMappingService'
19
+ import { DomainMapping } from '@open-mercato/core/modules/customer_accounts/data/entities'
20
+
21
+ const FEATURE = 'customer_accounts.domain.manage'
22
+
23
+ export const metadata = {
24
+ GET: { requireAuth: true, requireFeatures: [FEATURE] },
25
+ POST: { requireAuth: true, requireFeatures: [FEATURE] },
26
+ DELETE: { requireAuth: true, requireFeatures: [FEATURE] },
27
+ }
28
+
29
+ function isUniqueViolation(error: unknown): boolean {
30
+ if (error instanceof UniqueConstraintViolationException) return true
31
+ if (!error || typeof error !== 'object') return false
32
+ const code = (error as { code?: string }).code
33
+ if (code === '23505') return true
34
+ const messageRaw = (error as { message?: string }).message
35
+ const message = typeof messageRaw === 'string' ? messageRaw : ''
36
+ return message.toLowerCase().includes('duplicate key')
37
+ }
38
+
39
+ function serializeRecord(record: DomainMapping) {
40
+ return {
41
+ id: record.id,
42
+ hostname: record.hostname,
43
+ organizationId: record.organizationId,
44
+ tenantId: record.tenantId,
45
+ provider: record.provider,
46
+ status: record.status,
47
+ verifiedAt: record.verifiedAt?.toISOString() ?? null,
48
+ lastDnsCheckAt: record.lastDnsCheckAt?.toISOString() ?? null,
49
+ dnsFailureReason: record.dnsFailureReason ?? null,
50
+ tlsFailureReason: record.tlsFailureReason ?? null,
51
+ tlsRetryCount: record.tlsRetryCount,
52
+ cnameTarget: process.env.CUSTOM_DOMAIN_CNAME_TARGET ?? null,
53
+ aRecordTarget: process.env.CUSTOM_DOMAIN_A_RECORD_TARGET ?? null,
54
+ createdAt: record.createdAt.toISOString(),
55
+ updatedAt: record.updatedAt?.toISOString() ?? null,
56
+ }
57
+ }
58
+
59
+ export async function GET(req: Request) {
60
+ const auth = await getAuthFromRequest(req)
61
+ if (!auth) return NextResponse.json({ ok: false, error: 'Unauthorized' }, { status: 401 })
62
+
63
+ const container = await createRequestContainer()
64
+ const rbac = container.resolve('rbacService') as RbacService
65
+ const allowed = await rbac.userHasAllFeatures(auth.sub, [FEATURE], {
66
+ tenantId: auth.tenantId,
67
+ organizationId: auth.orgId,
68
+ })
69
+ if (!allowed) return NextResponse.json({ ok: false, error: 'Forbidden' }, { status: 403 })
70
+
71
+ const url = new URL(req.url)
72
+ const orgFilter = url.searchParams.get('organizationId')
73
+
74
+ const em = container.resolve('em') as EntityManager
75
+ const where: Record<string, unknown> = { tenantId: auth.tenantId }
76
+ if (orgFilter) where.organizationId = orgFilter
77
+ const records = await em.find(DomainMapping, where as never, { orderBy: { createdAt: 'desc' } })
78
+
79
+ return NextResponse.json({
80
+ ok: true,
81
+ domainMappings: records.map(serializeRecord),
82
+ config: {
83
+ cnameTarget: process.env.CUSTOM_DOMAIN_CNAME_TARGET ?? null,
84
+ aRecordTarget: process.env.CUSTOM_DOMAIN_A_RECORD_TARGET ?? null,
85
+ },
86
+ })
87
+ }
88
+
89
+ export async function POST(req: Request) {
90
+ const auth = await getAuthFromRequest(req)
91
+ if (!auth || !auth.tenantId) return NextResponse.json({ ok: false, error: 'Unauthorized' }, { status: 401 })
92
+
93
+ const container = await createRequestContainer()
94
+ const rbac = container.resolve('rbacService') as RbacService
95
+ const allowed = await rbac.userHasAllFeatures(auth.sub, [FEATURE], {
96
+ tenantId: auth.tenantId,
97
+ organizationId: auth.orgId,
98
+ })
99
+ if (!allowed) return NextResponse.json({ ok: false, error: 'Forbidden' }, { status: 403 })
100
+
101
+ const body = await readJsonSafe(req, {})
102
+ const parsed = registerDomainSchema.safeParse(body)
103
+ if (!parsed.success) {
104
+ return NextResponse.json(
105
+ { ok: false, error: 'Invalid request', issues: parsed.error.flatten() },
106
+ { status: 400 },
107
+ )
108
+ }
109
+
110
+ const guardResult = await validateCrudMutationGuard(container, {
111
+ tenantId: auth.tenantId,
112
+ organizationId: parsed.data.organizationId,
113
+ userId: auth.sub,
114
+ resourceKind: 'customer_accounts.domain_mapping',
115
+ resourceId: parsed.data.organizationId,
116
+ operation: 'create',
117
+ requestMethod: req.method,
118
+ requestHeaders: req.headers,
119
+ mutationPayload: parsed.data as unknown as Record<string, unknown>,
120
+ })
121
+ if (guardResult && !guardResult.ok) {
122
+ return NextResponse.json(guardResult.body, { status: guardResult.status })
123
+ }
124
+
125
+ const service = container.resolve('domainMappingService') as DomainMappingService
126
+
127
+ let entity: DomainMapping
128
+ try {
129
+ // service.register normalizes hostname internally — guards may have
130
+ // returned modifiedPayload but the typed runner doesn't expose it,
131
+ // so we rely on the service for the canonical form.
132
+ entity = await service.register({
133
+ hostname: parsed.data.hostname,
134
+ organizationId: parsed.data.organizationId,
135
+ tenantId: auth.tenantId,
136
+ replacesDomainId: parsed.data.replacesDomainId,
137
+ })
138
+ } catch (err: unknown) {
139
+ if (isUniqueViolation(err)) {
140
+ return NextResponse.json(
141
+ { ok: false, error: 'This domain is not available. Please choose a different hostname.' },
142
+ { status: 409 },
143
+ )
144
+ }
145
+ const message = err instanceof Error ? err.message : 'Failed to register domain'
146
+ return NextResponse.json({ ok: false, error: message }, { status: 500 })
147
+ }
148
+
149
+ if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
150
+ await runCrudMutationGuardAfterSuccess(container, {
151
+ tenantId: auth.tenantId,
152
+ organizationId: parsed.data.organizationId,
153
+ userId: auth.sub,
154
+ resourceKind: 'customer_accounts.domain_mapping',
155
+ resourceId: entity.id,
156
+ operation: 'create',
157
+ requestMethod: req.method,
158
+ requestHeaders: req.headers,
159
+ metadata: guardResult.metadata ?? null,
160
+ })
161
+ }
162
+
163
+ return NextResponse.json({ ok: true, domainMapping: serializeRecord(entity) }, { status: 201 })
164
+ }
165
+
166
+ export async function DELETE(req: Request) {
167
+ const auth = await getAuthFromRequest(req)
168
+ if (!auth || !auth.tenantId) return NextResponse.json({ ok: false, error: 'Unauthorized' }, { status: 401 })
169
+
170
+ const container = await createRequestContainer()
171
+ const rbac = container.resolve('rbacService') as RbacService
172
+ const allowed = await rbac.userHasAllFeatures(auth.sub, [FEATURE], {
173
+ tenantId: auth.tenantId,
174
+ organizationId: auth.orgId,
175
+ })
176
+ if (!allowed) return NextResponse.json({ ok: false, error: 'Forbidden' }, { status: 403 })
177
+
178
+ const url = new URL(req.url)
179
+ const id = url.searchParams.get('id')
180
+ if (!id) return NextResponse.json({ ok: false, error: 'id is required' }, { status: 400 })
181
+
182
+ const service = container.resolve('domainMappingService') as DomainMappingService
183
+ const existing = await service.findById(id, { tenantId: auth.tenantId })
184
+ if (!existing) return NextResponse.json({ ok: false, error: 'Not found' }, { status: 404 })
185
+
186
+ const guardResult = await validateCrudMutationGuard(container, {
187
+ tenantId: auth.tenantId,
188
+ organizationId: existing.organizationId,
189
+ userId: auth.sub,
190
+ resourceKind: 'customer_accounts.domain_mapping',
191
+ resourceId: id,
192
+ operation: 'delete',
193
+ requestMethod: req.method,
194
+ requestHeaders: req.headers,
195
+ })
196
+ if (guardResult && !guardResult.ok) {
197
+ return NextResponse.json(guardResult.body, { status: guardResult.status })
198
+ }
199
+
200
+ await service.remove(id, { tenantId: auth.tenantId })
201
+
202
+ if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
203
+ await runCrudMutationGuardAfterSuccess(container, {
204
+ tenantId: auth.tenantId,
205
+ organizationId: existing.organizationId,
206
+ userId: auth.sub,
207
+ resourceKind: 'customer_accounts.domain_mapping',
208
+ resourceId: id,
209
+ operation: 'delete',
210
+ requestMethod: req.method,
211
+ requestHeaders: req.headers,
212
+ metadata: guardResult.metadata ?? null,
213
+ })
214
+ }
215
+
216
+ return NextResponse.json({ ok: true })
217
+ }
218
+
219
+ const errorSchema = z.object({ ok: z.literal(false), error: z.string() })
220
+ const domainMappingSchema = z.object({
221
+ id: z.string().uuid(),
222
+ hostname: z.string(),
223
+ organizationId: z.string().uuid(),
224
+ tenantId: z.string().uuid(),
225
+ provider: z.literal('traefik'),
226
+ status: z.enum(['pending', 'verified', 'active', 'dns_failed', 'tls_failed']),
227
+ verifiedAt: z.string().nullable(),
228
+ lastDnsCheckAt: z.string().nullable(),
229
+ dnsFailureReason: z.string().nullable(),
230
+ tlsFailureReason: z.string().nullable(),
231
+ tlsRetryCount: z.number().int().nonnegative(),
232
+ cnameTarget: z.string().nullable(),
233
+ aRecordTarget: z.string().nullable(),
234
+ createdAt: z.string(),
235
+ updatedAt: z.string().nullable(),
236
+ })
237
+
238
+ export const openApi: OpenApiRouteDoc = {
239
+ tag: 'CustomerAccounts',
240
+ summary: 'Custom portal domain mappings (admin)',
241
+ methods: {
242
+ GET: {
243
+ summary: 'List domain mappings',
244
+ description: 'Returns all custom-domain mappings for the current tenant, optionally filtered by organization.',
245
+ responses: [
246
+ {
247
+ status: 200,
248
+ description: 'OK',
249
+ schema: z.object({
250
+ ok: z.literal(true),
251
+ domainMappings: z.array(domainMappingSchema),
252
+ config: z.object({
253
+ cnameTarget: z.string().nullable(),
254
+ aRecordTarget: z.string().nullable(),
255
+ }),
256
+ }),
257
+ },
258
+ ],
259
+ errors: [
260
+ { status: 401, description: 'Unauthorized', schema: errorSchema },
261
+ { status: 403, description: 'Forbidden', schema: errorSchema },
262
+ ],
263
+ },
264
+ POST: {
265
+ summary: 'Register a custom domain',
266
+ description: 'Registers a new custom domain mapping for an organization. Verifies via DNS asynchronously.',
267
+ requestBody: {
268
+ contentType: 'application/json',
269
+ schema: z.object({
270
+ hostname: z.string(),
271
+ organizationId: z.string().uuid(),
272
+ replacesDomainId: z.string().uuid().optional(),
273
+ }),
274
+ },
275
+ responses: [
276
+ {
277
+ status: 201,
278
+ description: 'Created',
279
+ schema: z.object({ ok: z.literal(true), domainMapping: domainMappingSchema }),
280
+ },
281
+ ],
282
+ errors: [
283
+ { status: 400, description: 'Validation error', schema: errorSchema },
284
+ { status: 401, description: 'Unauthorized', schema: errorSchema },
285
+ { status: 403, description: 'Forbidden', schema: errorSchema },
286
+ { status: 409, description: 'Conflict', schema: errorSchema },
287
+ ],
288
+ },
289
+ DELETE: {
290
+ summary: 'Remove a custom domain',
291
+ description: 'Removes the domain mapping identified by ?id=. Cache and Traefik routing drain within TTL.',
292
+ responses: [{ status: 200, description: 'OK', schema: z.object({ ok: z.literal(true) }) }],
293
+ errors: [
294
+ { status: 400, description: 'Bad request', schema: errorSchema },
295
+ { status: 401, description: 'Unauthorized', schema: errorSchema },
296
+ { status: 403, description: 'Forbidden', schema: errorSchema },
297
+ ],
298
+ },
299
+ },
300
+ }
301
+
302
+ // Re-export resolve type so tests can import without indirect lookup
303
+ export type { ResolveResult }
@@ -0,0 +1,83 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { z } from 'zod'
3
+ import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
4
+ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
5
+ import { tryNormalizeHostname } from '@open-mercato/core/modules/customer_accounts/lib/hostname'
6
+ import { secretEqual } from '@open-mercato/core/modules/customer_accounts/lib/secretCompare'
7
+ import { DomainMappingService } from '@open-mercato/core/modules/customer_accounts/services/domainMappingService'
8
+
9
+ const ORIGIN_HEADER_NAME = process.env.CUSTOMER_DOMAIN_ORIGIN_HEADER ?? 'X-Open-Mercato-Origin'
10
+
11
+ export const metadata = {
12
+ GET: { requireAuth: false },
13
+ }
14
+
15
+ function withOriginHeader(response: NextResponse): NextResponse {
16
+ response.headers.set(ORIGIN_HEADER_NAME, '1')
17
+ return response
18
+ }
19
+
20
+ function unauthorized(message: string): NextResponse {
21
+ return withOriginHeader(NextResponse.json({ ok: false, error: message }, { status: 403 }))
22
+ }
23
+
24
+ function notFound(): NextResponse {
25
+ return withOriginHeader(NextResponse.json({ ok: false, error: 'Not found' }, { status: 404 }))
26
+ }
27
+
28
+ function ok(): NextResponse {
29
+ return withOriginHeader(NextResponse.json({ ok: true }))
30
+ }
31
+
32
+ export async function GET(req: Request) {
33
+ const expected = process.env.DOMAIN_CHECK_SECRET
34
+ if (!expected) {
35
+ // Fail closed when the gating secret is not configured on the server.
36
+ return withOriginHeader(
37
+ NextResponse.json(
38
+ { ok: false, error: 'DOMAIN_CHECK_SECRET is not configured on the server' },
39
+ { status: 503 },
40
+ ),
41
+ )
42
+ }
43
+ const supplied = req.headers.get('x-domain-check-secret')
44
+ if (!secretEqual(supplied, expected)) return unauthorized('Forbidden')
45
+
46
+ const url = new URL(req.url)
47
+ // Traefik's ForwardAuth middleware cannot template the auth URL with the
48
+ // original Host, so it forwards the original request via X-Forwarded-Host.
49
+ // Manual / direct callers can still pass `?host=` explicitly.
50
+ const rawHost =
51
+ url.searchParams.get('host') ?? req.headers.get('x-forwarded-host')
52
+ if (!rawHost) return notFound()
53
+ const hostname = tryNormalizeHostname(rawHost)
54
+ if (!hostname) return notFound()
55
+
56
+ const container = await createRequestContainer()
57
+ const service = container.resolve('domainMappingService') as DomainMappingService
58
+ const result = await service.isAllowedForTls(hostname)
59
+ if (!result) return notFound()
60
+ return ok()
61
+ }
62
+
63
+ const errorSchema = z.object({ ok: z.literal(false), error: z.string() })
64
+
65
+ export const openApi: OpenApiRouteDoc = {
66
+ tag: 'CustomerAccounts',
67
+ summary: 'Traefik domain-check gate',
68
+ methods: {
69
+ GET: {
70
+ summary: 'Verify a hostname is allowed for TLS provisioning',
71
+ description:
72
+ 'Called by Traefik before issuing a Let\'s Encrypt certificate. Requires the X-Domain-Check-Secret header to match DOMAIN_CHECK_SECRET.',
73
+ responses: [
74
+ { status: 200, description: 'Hostname allowed', schema: z.object({ ok: z.literal(true) }) },
75
+ ],
76
+ errors: [
77
+ { status: 403, description: 'Forbidden', schema: errorSchema },
78
+ { status: 404, description: 'Hostname not registered or not yet verified', schema: errorSchema },
79
+ { status: 503, description: 'Server misconfiguration', schema: errorSchema },
80
+ ],
81
+ },
82
+ },
83
+ }
@@ -0,0 +1,139 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { z } from 'zod'
3
+ import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
4
+ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
5
+ import { secretEqual } from '@open-mercato/core/modules/customer_accounts/lib/secretCompare'
6
+ import { DomainMappingService } from '@open-mercato/core/modules/customer_accounts/services/domainMappingService'
7
+ import { rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'
8
+ import {
9
+ checkAuthRateLimit,
10
+ domainResolveAllIpRateLimitConfig,
11
+ } from '@open-mercato/core/modules/customer_accounts/lib/rateLimiter'
12
+
13
+ const ORIGIN_HEADER_NAME = process.env.CUSTOMER_DOMAIN_ORIGIN_HEADER ?? 'X-Open-Mercato-Origin'
14
+ const LOG_PREFIX = '[domain-resolve/all]'
15
+ const ENDPOINT_PATH = '/api/customer_accounts/domain-resolve/all'
16
+
17
+ export const metadata = {
18
+ GET: { requireAuth: false },
19
+ }
20
+
21
+ function withOriginHeader(response: NextResponse): NextResponse {
22
+ response.headers.set(ORIGIN_HEADER_NAME, '1')
23
+ return response
24
+ }
25
+
26
+ function readCallerIp(req: Request): string {
27
+ const forwardedFor = req.headers.get('x-forwarded-for')
28
+ if (forwardedFor) {
29
+ const firstHop = forwardedFor.split(',')[0]?.trim()
30
+ if (firstHop) return firstHop
31
+ }
32
+ const realIp = req.headers.get('x-real-ip')
33
+ if (realIp && realIp.trim().length > 0) return realIp.trim()
34
+ return 'unknown'
35
+ }
36
+
37
+ export async function GET(req: Request) {
38
+ const callerIp = readCallerIp(req)
39
+ const expected = process.env.DOMAIN_RESOLVE_SECRET
40
+ if (!expected) {
41
+ // Fail closed when the gating secret is not configured on the server.
42
+ console.warn(`${LOG_PREFIX} rejected request — secret not configured`, {
43
+ endpoint: ENDPOINT_PATH,
44
+ ip: callerIp,
45
+ outcome: 'misconfigured',
46
+ })
47
+ return withOriginHeader(
48
+ NextResponse.json(
49
+ { ok: false, error: 'DOMAIN_RESOLVE_SECRET is not configured on the server' },
50
+ { status: 503 },
51
+ ),
52
+ )
53
+ }
54
+
55
+ // Rate-limit by caller IP before the secret comparison to make leak-driven
56
+ // enumeration noisy and bounded. Fails closed (returns the helper's 429
57
+ // response) when the limit is exceeded.
58
+ const { error: rateLimitError } = await checkAuthRateLimit({
59
+ req,
60
+ ipConfig: domainResolveAllIpRateLimitConfig,
61
+ })
62
+ if (rateLimitError) {
63
+ console.warn(`${LOG_PREFIX} rate-limited`, {
64
+ endpoint: ENDPOINT_PATH,
65
+ ip: callerIp,
66
+ outcome: 'rate_limited',
67
+ })
68
+ return withOriginHeader(rateLimitError)
69
+ }
70
+
71
+ const supplied = req.headers.get('x-domain-resolve-secret')
72
+ if (!secretEqual(supplied, expected)) {
73
+ console.warn(`${LOG_PREFIX} rejected request — invalid secret`, {
74
+ endpoint: ENDPOINT_PATH,
75
+ ip: callerIp,
76
+ outcome: 'forbidden',
77
+ })
78
+ return withOriginHeader(NextResponse.json({ ok: false, error: 'Forbidden' }, { status: 403 }))
79
+ }
80
+
81
+ const container = await createRequestContainer()
82
+ const service = container.resolve('domainMappingService') as DomainMappingService
83
+ const records = await service.resolveAll()
84
+ console.info(`${LOG_PREFIX} authorized`, {
85
+ endpoint: ENDPOINT_PATH,
86
+ ip: callerIp,
87
+ outcome: 'ok',
88
+ domains: records.length,
89
+ })
90
+ return withOriginHeader(
91
+ NextResponse.json({
92
+ ok: true,
93
+ domains: records.map((r) => ({
94
+ hostname: r.hostname,
95
+ tenantId: r.tenantId,
96
+ organizationId: r.organizationId,
97
+ orgSlug: r.orgSlug,
98
+ status: r.status,
99
+ })),
100
+ }),
101
+ )
102
+ }
103
+
104
+ const errorSchema = z.object({ ok: z.literal(false), error: z.string() })
105
+
106
+ export const openApi: OpenApiRouteDoc = {
107
+ tag: 'CustomerAccounts',
108
+ summary: 'Batch resolve all active custom domains',
109
+ methods: {
110
+ GET: {
111
+ summary: 'Batch warm-up for the Node middleware',
112
+ description:
113
+ 'Returns every active domain mapping in a single payload so the middleware can populate its cache on process start. Requires X-Domain-Resolve-Secret header.',
114
+ responses: [
115
+ {
116
+ status: 200,
117
+ description: 'OK',
118
+ schema: z.object({
119
+ ok: z.literal(true),
120
+ domains: z.array(
121
+ z.object({
122
+ hostname: z.string(),
123
+ tenantId: z.string().uuid(),
124
+ organizationId: z.string().uuid(),
125
+ orgSlug: z.string().nullable(),
126
+ status: z.literal('active'),
127
+ }),
128
+ ),
129
+ }),
130
+ },
131
+ ],
132
+ errors: [
133
+ { status: 403, description: 'Forbidden', schema: errorSchema },
134
+ { status: 429, description: 'Too many requests', schema: rateLimitErrorSchema },
135
+ { status: 503, description: 'Server misconfiguration', schema: errorSchema },
136
+ ],
137
+ },
138
+ },
139
+ }
@@ -0,0 +1,91 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { z } from 'zod'
3
+ import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
4
+ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
5
+ import { tryNormalizeHostname } from '@open-mercato/core/modules/customer_accounts/lib/hostname'
6
+ import { secretEqual } from '@open-mercato/core/modules/customer_accounts/lib/secretCompare'
7
+ import { DomainMappingService } from '@open-mercato/core/modules/customer_accounts/services/domainMappingService'
8
+
9
+ const ORIGIN_HEADER_NAME = process.env.CUSTOMER_DOMAIN_ORIGIN_HEADER ?? 'X-Open-Mercato-Origin'
10
+
11
+ export const metadata = {
12
+ GET: { requireAuth: false },
13
+ }
14
+
15
+ function withOriginHeader(response: NextResponse): NextResponse {
16
+ response.headers.set(ORIGIN_HEADER_NAME, '1')
17
+ return response
18
+ }
19
+
20
+ export async function GET(req: Request) {
21
+ const expected = process.env.DOMAIN_RESOLVE_SECRET
22
+ if (!expected) {
23
+ // Fail closed when the gating secret is not configured on the server.
24
+ return withOriginHeader(
25
+ NextResponse.json(
26
+ { ok: false, error: 'DOMAIN_RESOLVE_SECRET is not configured on the server' },
27
+ { status: 503 },
28
+ ),
29
+ )
30
+ }
31
+ const supplied = req.headers.get('x-domain-resolve-secret')
32
+ if (!secretEqual(supplied, expected)) {
33
+ return withOriginHeader(NextResponse.json({ ok: false, error: 'Forbidden' }, { status: 403 }))
34
+ }
35
+
36
+ const url = new URL(req.url)
37
+ const rawHost = url.searchParams.get('host')
38
+ if (!rawHost) return withOriginHeader(NextResponse.json({ ok: false, error: 'host param required' }, { status: 400 }))
39
+ const hostname = tryNormalizeHostname(rawHost)
40
+ if (!hostname) return withOriginHeader(NextResponse.json({ ok: false, error: 'invalid host' }, { status: 400 }))
41
+
42
+ const container = await createRequestContainer()
43
+ const service = container.resolve('domainMappingService') as DomainMappingService
44
+ const result = await service.resolveByHostname(hostname)
45
+ if (!result || result.status !== 'active') {
46
+ return withOriginHeader(NextResponse.json({ ok: false, error: 'Not found' }, { status: 404 }))
47
+ }
48
+
49
+ return withOriginHeader(
50
+ NextResponse.json({
51
+ ok: true,
52
+ tenantId: result.tenantId,
53
+ organizationId: result.organizationId,
54
+ orgSlug: result.orgSlug,
55
+ status: result.status,
56
+ }),
57
+ )
58
+ }
59
+
60
+ const errorSchema = z.object({ ok: z.literal(false), error: z.string() })
61
+
62
+ export const openApi: OpenApiRouteDoc = {
63
+ tag: 'CustomerAccounts',
64
+ summary: 'Resolve a custom domain to its tenant and organization',
65
+ methods: {
66
+ GET: {
67
+ summary: 'Single-host resolve for the Node middleware',
68
+ description:
69
+ 'Internal endpoint used by the portal middleware to populate its in-memory cache. Requires X-Domain-Resolve-Secret header.',
70
+ responses: [
71
+ {
72
+ status: 200,
73
+ description: 'OK',
74
+ schema: z.object({
75
+ ok: z.literal(true),
76
+ tenantId: z.string().uuid(),
77
+ organizationId: z.string().uuid(),
78
+ orgSlug: z.string().nullable(),
79
+ status: z.literal('active'),
80
+ }),
81
+ },
82
+ ],
83
+ errors: [
84
+ { status: 400, description: 'Bad request', schema: errorSchema },
85
+ { status: 403, description: 'Forbidden', schema: errorSchema },
86
+ { status: 404, description: 'Not found', schema: errorSchema },
87
+ { status: 503, description: 'Server misconfiguration', schema: errorSchema },
88
+ ],
89
+ },
90
+ },
91
+ }
@@ -15,6 +15,10 @@ import {
15
15
  customerLoginRateLimitConfig,
16
16
  customerLoginIpRateLimitConfig,
17
17
  } from '@open-mercato/core/modules/customer_accounts/lib/rateLimiter'
18
+ import {
19
+ resolveTenantContext,
20
+ TenantResolutionError,
21
+ } from '@open-mercato/core/modules/customer_accounts/lib/resolveTenantContext'
18
22
 
19
23
  export const metadata: { path?: string; requireAuth?: boolean } = { requireAuth: false }
20
24
 
@@ -31,9 +35,16 @@ export async function POST(req: Request) {
31
35
  return NextResponse.json({ ok: false, error: 'Invalid credentials' }, { status: 400 })
32
36
  }
33
37
 
34
- const { email, password, tenantId } = parsed.data
35
- if (!tenantId) {
36
- return NextResponse.json({ ok: false, error: 'tenantId is required' }, { status: 400 })
38
+ const { email, password } = parsed.data
39
+ let tenantId: string
40
+ try {
41
+ const context = await resolveTenantContext(req, parsed.data.tenantId)
42
+ tenantId = context.tenantId
43
+ } catch (err) {
44
+ if (err instanceof TenantResolutionError) {
45
+ return NextResponse.json({ ok: false, error: err.message }, { status: err.status })
46
+ }
47
+ throw err
37
48
  }
38
49
 
39
50
  const { error: rateLimitError, compoundKey } = await checkAuthRateLimit({
@@ -2,6 +2,10 @@ import { NextResponse } from 'next/server'
2
2
  import { z } from 'zod'
3
3
  import type { OpenApiRouteDoc, OpenApiMethodDoc } from '@open-mercato/shared/lib/openapi'
4
4
  import { magicLinkRequestSchema } from '@open-mercato/core/modules/customer_accounts/data/validators'
5
+ import {
6
+ resolveTenantContext,
7
+ TenantResolutionError,
8
+ } from '@open-mercato/core/modules/customer_accounts/lib/resolveTenantContext'
5
9
  import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
6
10
  import { CustomerUserService } from '@open-mercato/core/modules/customer_accounts/services/customerUserService'
7
11
  import { CustomerTokenService } from '@open-mercato/core/modules/customer_accounts/services/customerTokenService'
@@ -37,9 +41,16 @@ export async function POST(req: Request) {
37
41
  return NextResponse.json({ ok: true })
38
42
  }
39
43
 
40
- const { email, tenantId } = parsed.data
41
- if (!tenantId) {
42
- return NextResponse.json({ ok: true })
44
+ const { email } = parsed.data
45
+ let tenantId: string
46
+ try {
47
+ const context = await resolveTenantContext(req, parsed.data.tenantId)
48
+ tenantId = context.tenantId
49
+ } catch (err) {
50
+ if (err instanceof TenantResolutionError) {
51
+ return NextResponse.json({ ok: true })
52
+ }
53
+ throw err
43
54
  }
44
55
 
45
56
  const container = await createRequestContainer()