@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,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/customer_accounts/api/domain-check.ts"],
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { tryNormalizeHostname } from '@open-mercato/core/modules/customer_accounts/lib/hostname'\nimport { secretEqual } from '@open-mercato/core/modules/customer_accounts/lib/secretCompare'\nimport { DomainMappingService } from '@open-mercato/core/modules/customer_accounts/services/domainMappingService'\n\nconst ORIGIN_HEADER_NAME = process.env.CUSTOMER_DOMAIN_ORIGIN_HEADER ?? 'X-Open-Mercato-Origin'\n\nexport const metadata = {\n GET: { requireAuth: false },\n}\n\nfunction withOriginHeader(response: NextResponse): NextResponse {\n response.headers.set(ORIGIN_HEADER_NAME, '1')\n return response\n}\n\nfunction unauthorized(message: string): NextResponse {\n return withOriginHeader(NextResponse.json({ ok: false, error: message }, { status: 403 }))\n}\n\nfunction notFound(): NextResponse {\n return withOriginHeader(NextResponse.json({ ok: false, error: 'Not found' }, { status: 404 }))\n}\n\nfunction ok(): NextResponse {\n return withOriginHeader(NextResponse.json({ ok: true }))\n}\n\nexport async function GET(req: Request) {\n const expected = process.env.DOMAIN_CHECK_SECRET\n if (!expected) {\n // Fail closed when the gating secret is not configured on the server.\n return withOriginHeader(\n NextResponse.json(\n { ok: false, error: 'DOMAIN_CHECK_SECRET is not configured on the server' },\n { status: 503 },\n ),\n )\n }\n const supplied = req.headers.get('x-domain-check-secret')\n if (!secretEqual(supplied, expected)) return unauthorized('Forbidden')\n\n const url = new URL(req.url)\n // Traefik's ForwardAuth middleware cannot template the auth URL with the\n // original Host, so it forwards the original request via X-Forwarded-Host.\n // Manual / direct callers can still pass `?host=` explicitly.\n const rawHost =\n url.searchParams.get('host') ?? req.headers.get('x-forwarded-host')\n if (!rawHost) return notFound()\n const hostname = tryNormalizeHostname(rawHost)\n if (!hostname) return notFound()\n\n const container = await createRequestContainer()\n const service = container.resolve('domainMappingService') as DomainMappingService\n const result = await service.isAllowedForTls(hostname)\n if (!result) return notFound()\n return ok()\n}\n\nconst errorSchema = z.object({ ok: z.literal(false), error: z.string() })\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'CustomerAccounts',\n summary: 'Traefik domain-check gate',\n methods: {\n GET: {\n summary: 'Verify a hostname is allowed for TLS provisioning',\n description:\n 'Called by Traefik before issuing a Let\\'s Encrypt certificate. Requires the X-Domain-Check-Secret header to match DOMAIN_CHECK_SECRET.',\n responses: [\n { status: 200, description: 'Hostname allowed', schema: z.object({ ok: z.literal(true) }) },\n ],\n errors: [\n { status: 403, description: 'Forbidden', schema: errorSchema },\n { status: 404, description: 'Hostname not registered or not yet verified', schema: errorSchema },\n { status: 503, description: 'Server misconfiguration', schema: errorSchema },\n ],\n },\n },\n}\n"],
5
+ "mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAElB,SAAS,8BAA8B;AACvC,SAAS,4BAA4B;AACrC,SAAS,mBAAmB;AAG5B,MAAM,qBAAqB,QAAQ,IAAI,iCAAiC;AAEjE,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,MAAM;AAC5B;AAEA,SAAS,iBAAiB,UAAsC;AAC9D,WAAS,QAAQ,IAAI,oBAAoB,GAAG;AAC5C,SAAO;AACT;AAEA,SAAS,aAAa,SAA+B;AACnD,SAAO,iBAAiB,aAAa,KAAK,EAAE,IAAI,OAAO,OAAO,QAAQ,GAAG,EAAE,QAAQ,IAAI,CAAC,CAAC;AAC3F;AAEA,SAAS,WAAyB;AAChC,SAAO,iBAAiB,aAAa,KAAK,EAAE,IAAI,OAAO,OAAO,YAAY,GAAG,EAAE,QAAQ,IAAI,CAAC,CAAC;AAC/F;AAEA,SAAS,KAAmB;AAC1B,SAAO,iBAAiB,aAAa,KAAK,EAAE,IAAI,KAAK,CAAC,CAAC;AACzD;AAEA,eAAsB,IAAI,KAAc;AACtC,QAAM,WAAW,QAAQ,IAAI;AAC7B,MAAI,CAAC,UAAU;AAEb,WAAO;AAAA,MACL,aAAa;AAAA,QACX,EAAE,IAAI,OAAO,OAAO,sDAAsD;AAAA,QAC1E,EAAE,QAAQ,IAAI;AAAA,MAChB;AAAA,IACF;AAAA,EACF;AACA,QAAM,WAAW,IAAI,QAAQ,IAAI,uBAAuB;AACxD,MAAI,CAAC,YAAY,UAAU,QAAQ,EAAG,QAAO,aAAa,WAAW;AAErE,QAAM,MAAM,IAAI,IAAI,IAAI,GAAG;AAI3B,QAAM,UACJ,IAAI,aAAa,IAAI,MAAM,KAAK,IAAI,QAAQ,IAAI,kBAAkB;AACpE,MAAI,CAAC,QAAS,QAAO,SAAS;AAC9B,QAAM,WAAW,qBAAqB,OAAO;AAC7C,MAAI,CAAC,SAAU,QAAO,SAAS;AAE/B,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,UAAU,UAAU,QAAQ,sBAAsB;AACxD,QAAM,SAAS,MAAM,QAAQ,gBAAgB,QAAQ;AACrD,MAAI,CAAC,OAAQ,QAAO,SAAS;AAC7B,SAAO,GAAG;AACZ;AAEA,MAAM,cAAc,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,KAAK,GAAG,OAAO,EAAE,OAAO,EAAE,CAAC;AAEjE,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,KAAK;AAAA,MACH,SAAS;AAAA,MACT,aACE;AAAA,MACF,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,oBAAoB,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,IAAI,EAAE,CAAC,EAAE;AAAA,MAC5F;AAAA,MACA,QAAQ;AAAA,QACN,EAAE,QAAQ,KAAK,aAAa,aAAa,QAAQ,YAAY;AAAA,QAC7D,EAAE,QAAQ,KAAK,aAAa,+CAA+C,QAAQ,YAAY;AAAA,QAC/F,EAAE,QAAQ,KAAK,aAAa,2BAA2B,QAAQ,YAAY;AAAA,MAC7E;AAAA,IACF;AAAA,EACF;AACF;",
6
+ "names": []
7
+ }
@@ -0,0 +1,128 @@
1
+ import { NextResponse } from "next/server";
2
+ import { z } from "zod";
3
+ import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
4
+ import { secretEqual } from "@open-mercato/core/modules/customer_accounts/lib/secretCompare";
5
+ import { rateLimitErrorSchema } from "@open-mercato/shared/lib/ratelimit/helpers";
6
+ import {
7
+ checkAuthRateLimit,
8
+ domainResolveAllIpRateLimitConfig
9
+ } from "@open-mercato/core/modules/customer_accounts/lib/rateLimiter";
10
+ const ORIGIN_HEADER_NAME = process.env.CUSTOMER_DOMAIN_ORIGIN_HEADER ?? "X-Open-Mercato-Origin";
11
+ const LOG_PREFIX = "[domain-resolve/all]";
12
+ const ENDPOINT_PATH = "/api/customer_accounts/domain-resolve/all";
13
+ const metadata = {
14
+ GET: { requireAuth: false }
15
+ };
16
+ function withOriginHeader(response) {
17
+ response.headers.set(ORIGIN_HEADER_NAME, "1");
18
+ return response;
19
+ }
20
+ function readCallerIp(req) {
21
+ const forwardedFor = req.headers.get("x-forwarded-for");
22
+ if (forwardedFor) {
23
+ const firstHop = forwardedFor.split(",")[0]?.trim();
24
+ if (firstHop) return firstHop;
25
+ }
26
+ const realIp = req.headers.get("x-real-ip");
27
+ if (realIp && realIp.trim().length > 0) return realIp.trim();
28
+ return "unknown";
29
+ }
30
+ async function GET(req) {
31
+ const callerIp = readCallerIp(req);
32
+ const expected = process.env.DOMAIN_RESOLVE_SECRET;
33
+ if (!expected) {
34
+ console.warn(`${LOG_PREFIX} rejected request \u2014 secret not configured`, {
35
+ endpoint: ENDPOINT_PATH,
36
+ ip: callerIp,
37
+ outcome: "misconfigured"
38
+ });
39
+ return withOriginHeader(
40
+ NextResponse.json(
41
+ { ok: false, error: "DOMAIN_RESOLVE_SECRET is not configured on the server" },
42
+ { status: 503 }
43
+ )
44
+ );
45
+ }
46
+ const { error: rateLimitError } = await checkAuthRateLimit({
47
+ req,
48
+ ipConfig: domainResolveAllIpRateLimitConfig
49
+ });
50
+ if (rateLimitError) {
51
+ console.warn(`${LOG_PREFIX} rate-limited`, {
52
+ endpoint: ENDPOINT_PATH,
53
+ ip: callerIp,
54
+ outcome: "rate_limited"
55
+ });
56
+ return withOriginHeader(rateLimitError);
57
+ }
58
+ const supplied = req.headers.get("x-domain-resolve-secret");
59
+ if (!secretEqual(supplied, expected)) {
60
+ console.warn(`${LOG_PREFIX} rejected request \u2014 invalid secret`, {
61
+ endpoint: ENDPOINT_PATH,
62
+ ip: callerIp,
63
+ outcome: "forbidden"
64
+ });
65
+ return withOriginHeader(NextResponse.json({ ok: false, error: "Forbidden" }, { status: 403 }));
66
+ }
67
+ const container = await createRequestContainer();
68
+ const service = container.resolve("domainMappingService");
69
+ const records = await service.resolveAll();
70
+ console.info(`${LOG_PREFIX} authorized`, {
71
+ endpoint: ENDPOINT_PATH,
72
+ ip: callerIp,
73
+ outcome: "ok",
74
+ domains: records.length
75
+ });
76
+ return withOriginHeader(
77
+ NextResponse.json({
78
+ ok: true,
79
+ domains: records.map((r) => ({
80
+ hostname: r.hostname,
81
+ tenantId: r.tenantId,
82
+ organizationId: r.organizationId,
83
+ orgSlug: r.orgSlug,
84
+ status: r.status
85
+ }))
86
+ })
87
+ );
88
+ }
89
+ const errorSchema = z.object({ ok: z.literal(false), error: z.string() });
90
+ const openApi = {
91
+ tag: "CustomerAccounts",
92
+ summary: "Batch resolve all active custom domains",
93
+ methods: {
94
+ GET: {
95
+ summary: "Batch warm-up for the Node middleware",
96
+ description: "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.",
97
+ responses: [
98
+ {
99
+ status: 200,
100
+ description: "OK",
101
+ schema: z.object({
102
+ ok: z.literal(true),
103
+ domains: z.array(
104
+ z.object({
105
+ hostname: z.string(),
106
+ tenantId: z.string().uuid(),
107
+ organizationId: z.string().uuid(),
108
+ orgSlug: z.string().nullable(),
109
+ status: z.literal("active")
110
+ })
111
+ )
112
+ })
113
+ }
114
+ ],
115
+ errors: [
116
+ { status: 403, description: "Forbidden", schema: errorSchema },
117
+ { status: 429, description: "Too many requests", schema: rateLimitErrorSchema },
118
+ { status: 503, description: "Server misconfiguration", schema: errorSchema }
119
+ ]
120
+ }
121
+ }
122
+ };
123
+ export {
124
+ GET,
125
+ metadata,
126
+ openApi
127
+ };
128
+ //# sourceMappingURL=all.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../src/modules/customer_accounts/api/domain-resolve/all.ts"],
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { secretEqual } from '@open-mercato/core/modules/customer_accounts/lib/secretCompare'\nimport { DomainMappingService } from '@open-mercato/core/modules/customer_accounts/services/domainMappingService'\nimport { rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport {\n checkAuthRateLimit,\n domainResolveAllIpRateLimitConfig,\n} from '@open-mercato/core/modules/customer_accounts/lib/rateLimiter'\n\nconst ORIGIN_HEADER_NAME = process.env.CUSTOMER_DOMAIN_ORIGIN_HEADER ?? 'X-Open-Mercato-Origin'\nconst LOG_PREFIX = '[domain-resolve/all]'\nconst ENDPOINT_PATH = '/api/customer_accounts/domain-resolve/all'\n\nexport const metadata = {\n GET: { requireAuth: false },\n}\n\nfunction withOriginHeader(response: NextResponse): NextResponse {\n response.headers.set(ORIGIN_HEADER_NAME, '1')\n return response\n}\n\nfunction readCallerIp(req: Request): string {\n const forwardedFor = req.headers.get('x-forwarded-for')\n if (forwardedFor) {\n const firstHop = forwardedFor.split(',')[0]?.trim()\n if (firstHop) return firstHop\n }\n const realIp = req.headers.get('x-real-ip')\n if (realIp && realIp.trim().length > 0) return realIp.trim()\n return 'unknown'\n}\n\nexport async function GET(req: Request) {\n const callerIp = readCallerIp(req)\n const expected = process.env.DOMAIN_RESOLVE_SECRET\n if (!expected) {\n // Fail closed when the gating secret is not configured on the server.\n console.warn(`${LOG_PREFIX} rejected request \u2014 secret not configured`, {\n endpoint: ENDPOINT_PATH,\n ip: callerIp,\n outcome: 'misconfigured',\n })\n return withOriginHeader(\n NextResponse.json(\n { ok: false, error: 'DOMAIN_RESOLVE_SECRET is not configured on the server' },\n { status: 503 },\n ),\n )\n }\n\n // Rate-limit by caller IP before the secret comparison to make leak-driven\n // enumeration noisy and bounded. Fails closed (returns the helper's 429\n // response) when the limit is exceeded.\n const { error: rateLimitError } = await checkAuthRateLimit({\n req,\n ipConfig: domainResolveAllIpRateLimitConfig,\n })\n if (rateLimitError) {\n console.warn(`${LOG_PREFIX} rate-limited`, {\n endpoint: ENDPOINT_PATH,\n ip: callerIp,\n outcome: 'rate_limited',\n })\n return withOriginHeader(rateLimitError)\n }\n\n const supplied = req.headers.get('x-domain-resolve-secret')\n if (!secretEqual(supplied, expected)) {\n console.warn(`${LOG_PREFIX} rejected request \u2014 invalid secret`, {\n endpoint: ENDPOINT_PATH,\n ip: callerIp,\n outcome: 'forbidden',\n })\n return withOriginHeader(NextResponse.json({ ok: false, error: 'Forbidden' }, { status: 403 }))\n }\n\n const container = await createRequestContainer()\n const service = container.resolve('domainMappingService') as DomainMappingService\n const records = await service.resolveAll()\n console.info(`${LOG_PREFIX} authorized`, {\n endpoint: ENDPOINT_PATH,\n ip: callerIp,\n outcome: 'ok',\n domains: records.length,\n })\n return withOriginHeader(\n NextResponse.json({\n ok: true,\n domains: records.map((r) => ({\n hostname: r.hostname,\n tenantId: r.tenantId,\n organizationId: r.organizationId,\n orgSlug: r.orgSlug,\n status: r.status,\n })),\n }),\n )\n}\n\nconst errorSchema = z.object({ ok: z.literal(false), error: z.string() })\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'CustomerAccounts',\n summary: 'Batch resolve all active custom domains',\n methods: {\n GET: {\n summary: 'Batch warm-up for the Node middleware',\n description:\n '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.',\n responses: [\n {\n status: 200,\n description: 'OK',\n schema: z.object({\n ok: z.literal(true),\n domains: z.array(\n z.object({\n hostname: z.string(),\n tenantId: z.string().uuid(),\n organizationId: z.string().uuid(),\n orgSlug: z.string().nullable(),\n status: z.literal('active'),\n }),\n ),\n }),\n },\n ],\n errors: [\n { status: 403, description: 'Forbidden', schema: errorSchema },\n { status: 429, description: 'Too many requests', schema: rateLimitErrorSchema },\n { status: 503, description: 'Server misconfiguration', schema: errorSchema },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,84 @@
1
+ import { NextResponse } from "next/server";
2
+ import { z } from "zod";
3
+ import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
4
+ import { tryNormalizeHostname } from "@open-mercato/core/modules/customer_accounts/lib/hostname";
5
+ import { secretEqual } from "@open-mercato/core/modules/customer_accounts/lib/secretCompare";
6
+ const ORIGIN_HEADER_NAME = process.env.CUSTOMER_DOMAIN_ORIGIN_HEADER ?? "X-Open-Mercato-Origin";
7
+ const metadata = {
8
+ GET: { requireAuth: false }
9
+ };
10
+ function withOriginHeader(response) {
11
+ response.headers.set(ORIGIN_HEADER_NAME, "1");
12
+ return response;
13
+ }
14
+ async function GET(req) {
15
+ const expected = process.env.DOMAIN_RESOLVE_SECRET;
16
+ if (!expected) {
17
+ return withOriginHeader(
18
+ NextResponse.json(
19
+ { ok: false, error: "DOMAIN_RESOLVE_SECRET is not configured on the server" },
20
+ { status: 503 }
21
+ )
22
+ );
23
+ }
24
+ const supplied = req.headers.get("x-domain-resolve-secret");
25
+ if (!secretEqual(supplied, expected)) {
26
+ return withOriginHeader(NextResponse.json({ ok: false, error: "Forbidden" }, { status: 403 }));
27
+ }
28
+ const url = new URL(req.url);
29
+ const rawHost = url.searchParams.get("host");
30
+ if (!rawHost) return withOriginHeader(NextResponse.json({ ok: false, error: "host param required" }, { status: 400 }));
31
+ const hostname = tryNormalizeHostname(rawHost);
32
+ if (!hostname) return withOriginHeader(NextResponse.json({ ok: false, error: "invalid host" }, { status: 400 }));
33
+ const container = await createRequestContainer();
34
+ const service = container.resolve("domainMappingService");
35
+ const result = await service.resolveByHostname(hostname);
36
+ if (!result || result.status !== "active") {
37
+ return withOriginHeader(NextResponse.json({ ok: false, error: "Not found" }, { status: 404 }));
38
+ }
39
+ return withOriginHeader(
40
+ NextResponse.json({
41
+ ok: true,
42
+ tenantId: result.tenantId,
43
+ organizationId: result.organizationId,
44
+ orgSlug: result.orgSlug,
45
+ status: result.status
46
+ })
47
+ );
48
+ }
49
+ const errorSchema = z.object({ ok: z.literal(false), error: z.string() });
50
+ const openApi = {
51
+ tag: "CustomerAccounts",
52
+ summary: "Resolve a custom domain to its tenant and organization",
53
+ methods: {
54
+ GET: {
55
+ summary: "Single-host resolve for the Node middleware",
56
+ description: "Internal endpoint used by the portal middleware to populate its in-memory cache. Requires X-Domain-Resolve-Secret header.",
57
+ responses: [
58
+ {
59
+ status: 200,
60
+ description: "OK",
61
+ schema: z.object({
62
+ ok: z.literal(true),
63
+ tenantId: z.string().uuid(),
64
+ organizationId: z.string().uuid(),
65
+ orgSlug: z.string().nullable(),
66
+ status: z.literal("active")
67
+ })
68
+ }
69
+ ],
70
+ errors: [
71
+ { status: 400, description: "Bad request", schema: errorSchema },
72
+ { status: 403, description: "Forbidden", schema: errorSchema },
73
+ { status: 404, description: "Not found", schema: errorSchema },
74
+ { status: 503, description: "Server misconfiguration", schema: errorSchema }
75
+ ]
76
+ }
77
+ }
78
+ };
79
+ export {
80
+ GET,
81
+ metadata,
82
+ openApi
83
+ };
84
+ //# sourceMappingURL=domain-resolve.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/customer_accounts/api/domain-resolve.ts"],
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { tryNormalizeHostname } from '@open-mercato/core/modules/customer_accounts/lib/hostname'\nimport { secretEqual } from '@open-mercato/core/modules/customer_accounts/lib/secretCompare'\nimport { DomainMappingService } from '@open-mercato/core/modules/customer_accounts/services/domainMappingService'\n\nconst ORIGIN_HEADER_NAME = process.env.CUSTOMER_DOMAIN_ORIGIN_HEADER ?? 'X-Open-Mercato-Origin'\n\nexport const metadata = {\n GET: { requireAuth: false },\n}\n\nfunction withOriginHeader(response: NextResponse): NextResponse {\n response.headers.set(ORIGIN_HEADER_NAME, '1')\n return response\n}\n\nexport async function GET(req: Request) {\n const expected = process.env.DOMAIN_RESOLVE_SECRET\n if (!expected) {\n // Fail closed when the gating secret is not configured on the server.\n return withOriginHeader(\n NextResponse.json(\n { ok: false, error: 'DOMAIN_RESOLVE_SECRET is not configured on the server' },\n { status: 503 },\n ),\n )\n }\n const supplied = req.headers.get('x-domain-resolve-secret')\n if (!secretEqual(supplied, expected)) {\n return withOriginHeader(NextResponse.json({ ok: false, error: 'Forbidden' }, { status: 403 }))\n }\n\n const url = new URL(req.url)\n const rawHost = url.searchParams.get('host')\n if (!rawHost) return withOriginHeader(NextResponse.json({ ok: false, error: 'host param required' }, { status: 400 }))\n const hostname = tryNormalizeHostname(rawHost)\n if (!hostname) return withOriginHeader(NextResponse.json({ ok: false, error: 'invalid host' }, { status: 400 }))\n\n const container = await createRequestContainer()\n const service = container.resolve('domainMappingService') as DomainMappingService\n const result = await service.resolveByHostname(hostname)\n if (!result || result.status !== 'active') {\n return withOriginHeader(NextResponse.json({ ok: false, error: 'Not found' }, { status: 404 }))\n }\n\n return withOriginHeader(\n NextResponse.json({\n ok: true,\n tenantId: result.tenantId,\n organizationId: result.organizationId,\n orgSlug: result.orgSlug,\n status: result.status,\n }),\n )\n}\n\nconst errorSchema = z.object({ ok: z.literal(false), error: z.string() })\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'CustomerAccounts',\n summary: 'Resolve a custom domain to its tenant and organization',\n methods: {\n GET: {\n summary: 'Single-host resolve for the Node middleware',\n description:\n 'Internal endpoint used by the portal middleware to populate its in-memory cache. Requires X-Domain-Resolve-Secret header.',\n responses: [\n {\n status: 200,\n description: 'OK',\n schema: z.object({\n ok: z.literal(true),\n tenantId: z.string().uuid(),\n organizationId: z.string().uuid(),\n orgSlug: z.string().nullable(),\n status: z.literal('active'),\n }),\n },\n ],\n errors: [\n { status: 400, description: 'Bad request', schema: errorSchema },\n { status: 403, description: 'Forbidden', schema: errorSchema },\n { status: 404, description: 'Not found', schema: errorSchema },\n { status: 503, description: 'Server misconfiguration', schema: errorSchema },\n ],\n },\n },\n}\n"],
5
+ "mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAElB,SAAS,8BAA8B;AACvC,SAAS,4BAA4B;AACrC,SAAS,mBAAmB;AAG5B,MAAM,qBAAqB,QAAQ,IAAI,iCAAiC;AAEjE,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,MAAM;AAC5B;AAEA,SAAS,iBAAiB,UAAsC;AAC9D,WAAS,QAAQ,IAAI,oBAAoB,GAAG;AAC5C,SAAO;AACT;AAEA,eAAsB,IAAI,KAAc;AACtC,QAAM,WAAW,QAAQ,IAAI;AAC7B,MAAI,CAAC,UAAU;AAEb,WAAO;AAAA,MACL,aAAa;AAAA,QACX,EAAE,IAAI,OAAO,OAAO,wDAAwD;AAAA,QAC5E,EAAE,QAAQ,IAAI;AAAA,MAChB;AAAA,IACF;AAAA,EACF;AACA,QAAM,WAAW,IAAI,QAAQ,IAAI,yBAAyB;AAC1D,MAAI,CAAC,YAAY,UAAU,QAAQ,GAAG;AACpC,WAAO,iBAAiB,aAAa,KAAK,EAAE,IAAI,OAAO,OAAO,YAAY,GAAG,EAAE,QAAQ,IAAI,CAAC,CAAC;AAAA,EAC/F;AAEA,QAAM,MAAM,IAAI,IAAI,IAAI,GAAG;AAC3B,QAAM,UAAU,IAAI,aAAa,IAAI,MAAM;AAC3C,MAAI,CAAC,QAAS,QAAO,iBAAiB,aAAa,KAAK,EAAE,IAAI,OAAO,OAAO,sBAAsB,GAAG,EAAE,QAAQ,IAAI,CAAC,CAAC;AACrH,QAAM,WAAW,qBAAqB,OAAO;AAC7C,MAAI,CAAC,SAAU,QAAO,iBAAiB,aAAa,KAAK,EAAE,IAAI,OAAO,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC,CAAC;AAE/G,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,UAAU,UAAU,QAAQ,sBAAsB;AACxD,QAAM,SAAS,MAAM,QAAQ,kBAAkB,QAAQ;AACvD,MAAI,CAAC,UAAU,OAAO,WAAW,UAAU;AACzC,WAAO,iBAAiB,aAAa,KAAK,EAAE,IAAI,OAAO,OAAO,YAAY,GAAG,EAAE,QAAQ,IAAI,CAAC,CAAC;AAAA,EAC/F;AAEA,SAAO;AAAA,IACL,aAAa,KAAK;AAAA,MAChB,IAAI;AAAA,MACJ,UAAU,OAAO;AAAA,MACjB,gBAAgB,OAAO;AAAA,MACvB,SAAS,OAAO;AAAA,MAChB,QAAQ,OAAO;AAAA,IACjB,CAAC;AAAA,EACH;AACF;AAEA,MAAM,cAAc,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,KAAK,GAAG,OAAO,EAAE,OAAO,EAAE,CAAC;AAEjE,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,KAAK;AAAA,MACH,SAAS;AAAA,MACT,aACE;AAAA,MACF,WAAW;AAAA,QACT;AAAA,UACE,QAAQ;AAAA,UACR,aAAa;AAAA,UACb,QAAQ,EAAE,OAAO;AAAA,YACf,IAAI,EAAE,QAAQ,IAAI;AAAA,YAClB,UAAU,EAAE,OAAO,EAAE,KAAK;AAAA,YAC1B,gBAAgB,EAAE,OAAO,EAAE,KAAK;AAAA,YAChC,SAAS,EAAE,OAAO,EAAE,SAAS;AAAA,YAC7B,QAAQ,EAAE,QAAQ,QAAQ;AAAA,UAC5B,CAAC;AAAA,QACH;AAAA,MACF;AAAA,MACA,QAAQ;AAAA,QACN,EAAE,QAAQ,KAAK,aAAa,eAAe,QAAQ,YAAY;AAAA,QAC/D,EAAE,QAAQ,KAAK,aAAa,aAAa,QAAQ,YAAY;AAAA,QAC7D,EAAE,QAAQ,KAAK,aAAa,aAAa,QAAQ,YAAY;AAAA,QAC7D,EAAE,QAAQ,KAAK,aAAa,2BAA2B,QAAQ,YAAY;AAAA,MAC7E;AAAA,IACF;AAAA,EACF;AACF;",
6
+ "names": []
7
+ }
@@ -11,6 +11,10 @@ import {
11
11
  customerLoginRateLimitConfig,
12
12
  customerLoginIpRateLimitConfig
13
13
  } from "@open-mercato/core/modules/customer_accounts/lib/rateLimiter";
14
+ import {
15
+ resolveTenantContext,
16
+ TenantResolutionError
17
+ } from "@open-mercato/core/modules/customer_accounts/lib/resolveTenantContext";
14
18
  const metadata = { requireAuth: false };
15
19
  async function POST(req) {
16
20
  let body;
@@ -23,9 +27,16 @@ async function POST(req) {
23
27
  if (!parsed.success) {
24
28
  return NextResponse.json({ ok: false, error: "Invalid credentials" }, { status: 400 });
25
29
  }
26
- const { email, password, tenantId } = parsed.data;
27
- if (!tenantId) {
28
- return NextResponse.json({ ok: false, error: "tenantId is required" }, { status: 400 });
30
+ const { email, password } = parsed.data;
31
+ let tenantId;
32
+ try {
33
+ const context = await resolveTenantContext(req, parsed.data.tenantId);
34
+ tenantId = context.tenantId;
35
+ } catch (err) {
36
+ if (err instanceof TenantResolutionError) {
37
+ return NextResponse.json({ ok: false, error: err.message }, { status: err.status });
38
+ }
39
+ throw err;
29
40
  }
30
41
  const { error: rateLimitError, compoundKey } = await checkAuthRateLimit({
31
42
  req,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/customer_accounts/api/login.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc, OpenApiMethodDoc } from '@open-mercato/shared/lib/openapi'\nimport { loginSchema } from '@open-mercato/core/modules/customer_accounts/data/validators'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { CustomerUserService } from '@open-mercato/core/modules/customer_accounts/services/customerUserService'\nimport { CustomerSessionService } from '@open-mercato/core/modules/customer_accounts/services/customerSessionService'\nimport { CustomerRbacService } from '@open-mercato/core/modules/customer_accounts/services/customerRbacService'\nimport { emitCustomerAccountsEvent } from '@open-mercato/core/modules/customer_accounts/events'\nimport { rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport { getClientIp } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport {\n checkAuthRateLimit,\n resetAuthRateLimit,\n customerLoginRateLimitConfig,\n customerLoginIpRateLimitConfig,\n} from '@open-mercato/core/modules/customer_accounts/lib/rateLimiter'\n\nexport const metadata: { path?: string; requireAuth?: boolean } = { requireAuth: false }\n\nexport async function POST(req: Request) {\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return NextResponse.json({ ok: false, error: 'Invalid request body' }, { status: 400 })\n }\n\n const parsed = loginSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json({ ok: false, error: 'Invalid credentials' }, { status: 400 })\n }\n\n const { email, password, tenantId } = parsed.data\n if (!tenantId) {\n return NextResponse.json({ ok: false, error: 'tenantId is required' }, { status: 400 })\n }\n\n const { error: rateLimitError, compoundKey } = await checkAuthRateLimit({\n req,\n ipConfig: customerLoginIpRateLimitConfig,\n compoundConfig: customerLoginRateLimitConfig,\n compoundIdentifier: email,\n })\n if (rateLimitError) return rateLimitError\n\n const container = await createRequestContainer()\n const customerUserService = container.resolve('customerUserService') as CustomerUserService\n const customerSessionService = container.resolve('customerSessionService') as CustomerSessionService\n const customerRbacService = container.resolve('customerRbacService') as CustomerRbacService\n\n const user = await customerUserService.findByEmail(email, tenantId)\n if (!user || !user.passwordHash) {\n void emitCustomerAccountsEvent('customer_accounts.login.failed', { email, reason: 'invalid_credentials', tenantId }).catch(() => undefined)\n return NextResponse.json({ ok: false, error: 'Invalid email or password' }, { status: 401 })\n }\n\n if (!user.isActive) {\n return NextResponse.json({ ok: false, error: 'Account is deactivated' }, { status: 401 })\n }\n\n if (customerUserService.checkLockout(user)) {\n void emitCustomerAccountsEvent('customer_accounts.login.failed', { email, reason: 'locked', tenantId }).catch(() => undefined)\n return NextResponse.json({ ok: false, error: 'Account is temporarily locked. Please try again later.' }, { status: 423 })\n }\n\n const passwordValid = await customerUserService.verifyPassword(user, password)\n if (!passwordValid) {\n await customerUserService.incrementFailedAttempts(user)\n void emitCustomerAccountsEvent('customer_accounts.login.failed', { email, reason: 'invalid_password', tenantId }).catch(() => undefined)\n return NextResponse.json({ ok: false, error: 'Invalid email or password' }, { status: 401 })\n }\n\n if (!user.emailVerifiedAt) {\n void emitCustomerAccountsEvent('customer_accounts.login.failed', {\n email,\n reason: 'email_not_verified',\n tenantId,\n }).catch(() => undefined)\n return NextResponse.json({ ok: false, error: 'Invalid email or password' }, { status: 401 })\n }\n\n await customerUserService.resetFailedAttempts(user)\n await customerUserService.updateLastLoginAt(user)\n\n if (compoundKey) {\n await resetAuthRateLimit(compoundKey, customerLoginRateLimitConfig)\n }\n\n const acl = await customerRbacService.loadAcl(user.id, { tenantId, organizationId: user.organizationId })\n const resolvedFeatures = acl.features\n\n const ip = getClientIp(req, 0)\n const userAgent = req.headers.get('user-agent') || null\n const { rawToken, jwt, session } = await customerSessionService.createSession(user, resolvedFeatures, ip, userAgent)\n\n void emitCustomerAccountsEvent('customer_accounts.login.success', {\n id: user.id,\n email: user.email,\n tenantId,\n organizationId: user.organizationId,\n }).catch(() => undefined)\n\n const res = NextResponse.json({\n ok: true,\n user: {\n id: user.id,\n email: user.email,\n displayName: user.displayName,\n emailVerified: !!user.emailVerifiedAt,\n },\n resolvedFeatures,\n })\n\n res.cookies.set('customer_auth_token', jwt, {\n httpOnly: true,\n path: '/',\n sameSite: 'lax',\n secure: process.env.NODE_ENV === 'production',\n maxAge: 60 * 60 * 8,\n })\n res.cookies.set('customer_session_token', rawToken, {\n httpOnly: true,\n path: '/',\n sameSite: 'lax',\n secure: process.env.NODE_ENV === 'production',\n maxAge: 60 * 60 * 24 * 30,\n })\n\n return res\n}\n\nconst loginSuccessSchema = z.object({\n ok: z.literal(true),\n user: z.object({\n id: z.string().uuid(),\n email: z.string().email(),\n displayName: z.string(),\n emailVerified: z.boolean(),\n }),\n resolvedFeatures: z.array(z.string()),\n})\n\nconst errorSchema = z.object({\n ok: z.literal(false),\n error: z.string(),\n})\n\nconst methodDoc: OpenApiMethodDoc = {\n summary: 'Authenticate customer credentials',\n description: 'Validates customer credentials and issues JWT + session cookies.',\n tags: ['Customer Authentication'],\n requestBody: {\n schema: loginSchema,\n description: 'Login payload with email and password.',\n },\n responses: [\n { status: 200, description: 'Login successful', schema: loginSuccessSchema },\n ],\n errors: [\n { status: 400, description: 'Validation failed', schema: errorSchema },\n { status: 401, description: 'Invalid credentials', schema: errorSchema },\n { status: 423, description: 'Account locked', schema: errorSchema },\n { status: 429, description: 'Too many login attempts', schema: rateLimitErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Customer login',\n description: 'Handles customer authentication and session issuance.',\n methods: { POST: methodDoc },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc, OpenApiMethodDoc } from '@open-mercato/shared/lib/openapi'\nimport { loginSchema } from '@open-mercato/core/modules/customer_accounts/data/validators'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { CustomerUserService } from '@open-mercato/core/modules/customer_accounts/services/customerUserService'\nimport { CustomerSessionService } from '@open-mercato/core/modules/customer_accounts/services/customerSessionService'\nimport { CustomerRbacService } from '@open-mercato/core/modules/customer_accounts/services/customerRbacService'\nimport { emitCustomerAccountsEvent } from '@open-mercato/core/modules/customer_accounts/events'\nimport { rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport { getClientIp } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport {\n checkAuthRateLimit,\n resetAuthRateLimit,\n customerLoginRateLimitConfig,\n customerLoginIpRateLimitConfig,\n} from '@open-mercato/core/modules/customer_accounts/lib/rateLimiter'\nimport {\n resolveTenantContext,\n TenantResolutionError,\n} from '@open-mercato/core/modules/customer_accounts/lib/resolveTenantContext'\n\nexport const metadata: { path?: string; requireAuth?: boolean } = { requireAuth: false }\n\nexport async function POST(req: Request) {\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return NextResponse.json({ ok: false, error: 'Invalid request body' }, { status: 400 })\n }\n\n const parsed = loginSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json({ ok: false, error: 'Invalid credentials' }, { status: 400 })\n }\n\n const { email, password } = parsed.data\n let tenantId: string\n try {\n const context = await resolveTenantContext(req, parsed.data.tenantId)\n tenantId = context.tenantId\n } catch (err) {\n if (err instanceof TenantResolutionError) {\n return NextResponse.json({ ok: false, error: err.message }, { status: err.status })\n }\n throw err\n }\n\n const { error: rateLimitError, compoundKey } = await checkAuthRateLimit({\n req,\n ipConfig: customerLoginIpRateLimitConfig,\n compoundConfig: customerLoginRateLimitConfig,\n compoundIdentifier: email,\n })\n if (rateLimitError) return rateLimitError\n\n const container = await createRequestContainer()\n const customerUserService = container.resolve('customerUserService') as CustomerUserService\n const customerSessionService = container.resolve('customerSessionService') as CustomerSessionService\n const customerRbacService = container.resolve('customerRbacService') as CustomerRbacService\n\n const user = await customerUserService.findByEmail(email, tenantId)\n if (!user || !user.passwordHash) {\n void emitCustomerAccountsEvent('customer_accounts.login.failed', { email, reason: 'invalid_credentials', tenantId }).catch(() => undefined)\n return NextResponse.json({ ok: false, error: 'Invalid email or password' }, { status: 401 })\n }\n\n if (!user.isActive) {\n return NextResponse.json({ ok: false, error: 'Account is deactivated' }, { status: 401 })\n }\n\n if (customerUserService.checkLockout(user)) {\n void emitCustomerAccountsEvent('customer_accounts.login.failed', { email, reason: 'locked', tenantId }).catch(() => undefined)\n return NextResponse.json({ ok: false, error: 'Account is temporarily locked. Please try again later.' }, { status: 423 })\n }\n\n const passwordValid = await customerUserService.verifyPassword(user, password)\n if (!passwordValid) {\n await customerUserService.incrementFailedAttempts(user)\n void emitCustomerAccountsEvent('customer_accounts.login.failed', { email, reason: 'invalid_password', tenantId }).catch(() => undefined)\n return NextResponse.json({ ok: false, error: 'Invalid email or password' }, { status: 401 })\n }\n\n if (!user.emailVerifiedAt) {\n void emitCustomerAccountsEvent('customer_accounts.login.failed', {\n email,\n reason: 'email_not_verified',\n tenantId,\n }).catch(() => undefined)\n return NextResponse.json({ ok: false, error: 'Invalid email or password' }, { status: 401 })\n }\n\n await customerUserService.resetFailedAttempts(user)\n await customerUserService.updateLastLoginAt(user)\n\n if (compoundKey) {\n await resetAuthRateLimit(compoundKey, customerLoginRateLimitConfig)\n }\n\n const acl = await customerRbacService.loadAcl(user.id, { tenantId, organizationId: user.organizationId })\n const resolvedFeatures = acl.features\n\n const ip = getClientIp(req, 0)\n const userAgent = req.headers.get('user-agent') || null\n const { rawToken, jwt, session } = await customerSessionService.createSession(user, resolvedFeatures, ip, userAgent)\n\n void emitCustomerAccountsEvent('customer_accounts.login.success', {\n id: user.id,\n email: user.email,\n tenantId,\n organizationId: user.organizationId,\n }).catch(() => undefined)\n\n const res = NextResponse.json({\n ok: true,\n user: {\n id: user.id,\n email: user.email,\n displayName: user.displayName,\n emailVerified: !!user.emailVerifiedAt,\n },\n resolvedFeatures,\n })\n\n res.cookies.set('customer_auth_token', jwt, {\n httpOnly: true,\n path: '/',\n sameSite: 'lax',\n secure: process.env.NODE_ENV === 'production',\n maxAge: 60 * 60 * 8,\n })\n res.cookies.set('customer_session_token', rawToken, {\n httpOnly: true,\n path: '/',\n sameSite: 'lax',\n secure: process.env.NODE_ENV === 'production',\n maxAge: 60 * 60 * 24 * 30,\n })\n\n return res\n}\n\nconst loginSuccessSchema = z.object({\n ok: z.literal(true),\n user: z.object({\n id: z.string().uuid(),\n email: z.string().email(),\n displayName: z.string(),\n emailVerified: z.boolean(),\n }),\n resolvedFeatures: z.array(z.string()),\n})\n\nconst errorSchema = z.object({\n ok: z.literal(false),\n error: z.string(),\n})\n\nconst methodDoc: OpenApiMethodDoc = {\n summary: 'Authenticate customer credentials',\n description: 'Validates customer credentials and issues JWT + session cookies.',\n tags: ['Customer Authentication'],\n requestBody: {\n schema: loginSchema,\n description: 'Login payload with email and password.',\n },\n responses: [\n { status: 200, description: 'Login successful', schema: loginSuccessSchema },\n ],\n errors: [\n { status: 400, description: 'Validation failed', schema: errorSchema },\n { status: 401, description: 'Invalid credentials', schema: errorSchema },\n { status: 423, description: 'Account locked', schema: errorSchema },\n { status: 429, description: 'Too many login attempts', schema: rateLimitErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Customer login',\n description: 'Handles customer authentication and session issuance.',\n methods: { POST: methodDoc },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,6 +1,10 @@
1
1
  import { NextResponse } from "next/server";
2
2
  import { z } from "zod";
3
3
  import { magicLinkRequestSchema } from "@open-mercato/core/modules/customer_accounts/data/validators";
4
+ import {
5
+ resolveTenantContext,
6
+ TenantResolutionError
7
+ } from "@open-mercato/core/modules/customer_accounts/lib/resolveTenantContext";
4
8
  import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
5
9
  import { rateLimitErrorSchema } from "@open-mercato/shared/lib/ratelimit/helpers";
6
10
  import {
@@ -29,9 +33,16 @@ async function POST(req) {
29
33
  if (!parsed.success) {
30
34
  return NextResponse.json({ ok: true });
31
35
  }
32
- const { email, tenantId } = parsed.data;
33
- if (!tenantId) {
34
- return NextResponse.json({ ok: true });
36
+ const { email } = parsed.data;
37
+ let tenantId;
38
+ try {
39
+ const context = await resolveTenantContext(req, parsed.data.tenantId);
40
+ tenantId = context.tenantId;
41
+ } catch (err) {
42
+ if (err instanceof TenantResolutionError) {
43
+ return NextResponse.json({ ok: true });
44
+ }
45
+ throw err;
35
46
  }
36
47
  const container = await createRequestContainer();
37
48
  const customerUserService = container.resolve("customerUserService");
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/customer_accounts/api/magic-link/request.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc, OpenApiMethodDoc } from '@open-mercato/shared/lib/openapi'\nimport { magicLinkRequestSchema } from '@open-mercato/core/modules/customer_accounts/data/validators'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { CustomerUserService } from '@open-mercato/core/modules/customer_accounts/services/customerUserService'\nimport { CustomerTokenService } from '@open-mercato/core/modules/customer_accounts/services/customerTokenService'\nimport { rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport {\n checkAuthRateLimit,\n customerMagicLinkRateLimitConfig,\n customerMagicLinkIpRateLimitConfig,\n} from '@open-mercato/core/modules/customer_accounts/lib/rateLimiter'\nimport { readNormalizedEmailFromJsonRequest } from '@open-mercato/core/modules/customer_accounts/lib/rateLimitIdentifier'\n\nexport const metadata: { path?: string; requireAuth?: boolean } = { requireAuth: false }\n\nexport async function POST(req: Request) {\n const rateLimitEmail = await readNormalizedEmailFromJsonRequest(req)\n const { error: rateLimitError } = await checkAuthRateLimit({\n req,\n ipConfig: customerMagicLinkIpRateLimitConfig,\n compoundConfig: customerMagicLinkRateLimitConfig,\n compoundIdentifier: rateLimitEmail,\n })\n if (rateLimitError) return rateLimitError\n\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return NextResponse.json({ ok: true })\n }\n\n const parsed = magicLinkRequestSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json({ ok: true })\n }\n\n const { email, tenantId } = parsed.data\n if (!tenantId) {\n return NextResponse.json({ ok: true })\n }\n\n const container = await createRequestContainer()\n const customerUserService = container.resolve('customerUserService') as CustomerUserService\n const customerTokenService = container.resolve('customerTokenService') as CustomerTokenService\n\n const user = await customerUserService.findByEmail(email, tenantId)\n if (user) {\n await customerTokenService.createMagicLink(user.id, tenantId)\n // Token is stored in DB; email delivery should be handled by a direct service call,\n // NOT via the event bus \u2014 raw tokens must never travel through events.\n void import('@open-mercato/core/modules/customer_accounts/events').then(({ emitCustomerAccountsEvent }) =>\n emitCustomerAccountsEvent('customer_accounts.magic_link.requested', {\n userId: user.id,\n tenantId,\n organizationId: user.organizationId,\n })\n ).catch(() => undefined)\n }\n\n return NextResponse.json({ ok: true })\n}\n\nconst successSchema = z.object({ ok: z.literal(true) })\n\nconst methodDoc: OpenApiMethodDoc = {\n summary: 'Request magic link login',\n description: 'Sends a magic link to the customer email. Always returns 200 to prevent enumeration.',\n tags: ['Customer Authentication'],\n requestBody: {\n schema: magicLinkRequestSchema,\n description: 'Magic link request with email.',\n },\n responses: [\n { status: 200, description: 'Request accepted', schema: successSchema },\n ],\n errors: [\n { status: 429, description: 'Too many requests', schema: rateLimitErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Request customer magic link',\n description: 'Handles magic link login requests for customer accounts.',\n methods: { POST: methodDoc },\n}\n"],
5
- "mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAElB,SAAS,8BAA8B;AACvC,SAAS,8BAA8B;AAGvC,SAAS,4BAA4B;AACrC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,0CAA0C;AAE5C,MAAM,WAAqD,EAAE,aAAa,MAAM;AAEvF,eAAsB,KAAK,KAAc;AACvC,QAAM,iBAAiB,MAAM,mCAAmC,GAAG;AACnE,QAAM,EAAE,OAAO,eAAe,IAAI,MAAM,mBAAmB;AAAA,IACzD;AAAA,IACA,UAAU;AAAA,IACV,gBAAgB;AAAA,IAChB,oBAAoB;AAAA,EACtB,CAAC;AACD,MAAI,eAAgB,QAAO;AAE3B,MAAI;AACJ,MAAI;AACF,WAAO,MAAM,IAAI,KAAK;AAAA,EACxB,QAAQ;AACN,WAAO,aAAa,KAAK,EAAE,IAAI,KAAK,CAAC;AAAA,EACvC;AAEA,QAAM,SAAS,uBAAuB,UAAU,IAAI;AACpD,MAAI,CAAC,OAAO,SAAS;AACnB,WAAO,aAAa,KAAK,EAAE,IAAI,KAAK,CAAC;AAAA,EACvC;AAEA,QAAM,EAAE,OAAO,SAAS,IAAI,OAAO;AACnC,MAAI,CAAC,UAAU;AACb,WAAO,aAAa,KAAK,EAAE,IAAI,KAAK,CAAC;AAAA,EACvC;AAEA,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,sBAAsB,UAAU,QAAQ,qBAAqB;AACnE,QAAM,uBAAuB,UAAU,QAAQ,sBAAsB;AAErE,QAAM,OAAO,MAAM,oBAAoB,YAAY,OAAO,QAAQ;AAClE,MAAI,MAAM;AACR,UAAM,qBAAqB,gBAAgB,KAAK,IAAI,QAAQ;AAG5D,SAAK,OAAO,qDAAqD,EAAE;AAAA,MAAK,CAAC,EAAE,0BAA0B,MACnG,0BAA0B,0CAA0C;AAAA,QAClE,QAAQ,KAAK;AAAA,QACb;AAAA,QACA,gBAAgB,KAAK;AAAA,MACvB,CAAC;AAAA,IACH,EAAE,MAAM,MAAM,MAAS;AAAA,EACzB;AAEA,SAAO,aAAa,KAAK,EAAE,IAAI,KAAK,CAAC;AACvC;AAEA,MAAM,gBAAgB,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,IAAI,EAAE,CAAC;AAEtD,MAAM,YAA8B;AAAA,EAClC,SAAS;AAAA,EACT,aAAa;AAAA,EACb,MAAM,CAAC,yBAAyB;AAAA,EAChC,aAAa;AAAA,IACX,QAAQ;AAAA,IACR,aAAa;AAAA,EACf;AAAA,EACA,WAAW;AAAA,IACT,EAAE,QAAQ,KAAK,aAAa,oBAAoB,QAAQ,cAAc;AAAA,EACxE;AAAA,EACA,QAAQ;AAAA,IACN,EAAE,QAAQ,KAAK,aAAa,qBAAqB,QAAQ,qBAAqB;AAAA,EAChF;AACF;AAEO,MAAM,UAA2B;AAAA,EACtC,SAAS;AAAA,EACT,aAAa;AAAA,EACb,SAAS,EAAE,MAAM,UAAU;AAC7B;",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc, OpenApiMethodDoc } from '@open-mercato/shared/lib/openapi'\nimport { magicLinkRequestSchema } from '@open-mercato/core/modules/customer_accounts/data/validators'\nimport {\n resolveTenantContext,\n TenantResolutionError,\n} from '@open-mercato/core/modules/customer_accounts/lib/resolveTenantContext'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { CustomerUserService } from '@open-mercato/core/modules/customer_accounts/services/customerUserService'\nimport { CustomerTokenService } from '@open-mercato/core/modules/customer_accounts/services/customerTokenService'\nimport { rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport {\n checkAuthRateLimit,\n customerMagicLinkRateLimitConfig,\n customerMagicLinkIpRateLimitConfig,\n} from '@open-mercato/core/modules/customer_accounts/lib/rateLimiter'\nimport { readNormalizedEmailFromJsonRequest } from '@open-mercato/core/modules/customer_accounts/lib/rateLimitIdentifier'\n\nexport const metadata: { path?: string; requireAuth?: boolean } = { requireAuth: false }\n\nexport async function POST(req: Request) {\n const rateLimitEmail = await readNormalizedEmailFromJsonRequest(req)\n const { error: rateLimitError } = await checkAuthRateLimit({\n req,\n ipConfig: customerMagicLinkIpRateLimitConfig,\n compoundConfig: customerMagicLinkRateLimitConfig,\n compoundIdentifier: rateLimitEmail,\n })\n if (rateLimitError) return rateLimitError\n\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return NextResponse.json({ ok: true })\n }\n\n const parsed = magicLinkRequestSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json({ ok: true })\n }\n\n const { email } = parsed.data\n let tenantId: string\n try {\n const context = await resolveTenantContext(req, parsed.data.tenantId)\n tenantId = context.tenantId\n } catch (err) {\n if (err instanceof TenantResolutionError) {\n return NextResponse.json({ ok: true })\n }\n throw err\n }\n\n const container = await createRequestContainer()\n const customerUserService = container.resolve('customerUserService') as CustomerUserService\n const customerTokenService = container.resolve('customerTokenService') as CustomerTokenService\n\n const user = await customerUserService.findByEmail(email, tenantId)\n if (user) {\n await customerTokenService.createMagicLink(user.id, tenantId)\n // Token is stored in DB; email delivery should be handled by a direct service call,\n // NOT via the event bus \u2014 raw tokens must never travel through events.\n void import('@open-mercato/core/modules/customer_accounts/events').then(({ emitCustomerAccountsEvent }) =>\n emitCustomerAccountsEvent('customer_accounts.magic_link.requested', {\n userId: user.id,\n tenantId,\n organizationId: user.organizationId,\n })\n ).catch(() => undefined)\n }\n\n return NextResponse.json({ ok: true })\n}\n\nconst successSchema = z.object({ ok: z.literal(true) })\n\nconst methodDoc: OpenApiMethodDoc = {\n summary: 'Request magic link login',\n description: 'Sends a magic link to the customer email. Always returns 200 to prevent enumeration.',\n tags: ['Customer Authentication'],\n requestBody: {\n schema: magicLinkRequestSchema,\n description: 'Magic link request with email.',\n },\n responses: [\n { status: 200, description: 'Request accepted', schema: successSchema },\n ],\n errors: [\n { status: 429, description: 'Too many requests', schema: rateLimitErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Request customer magic link',\n description: 'Handles magic link login requests for customer accounts.',\n methods: { POST: methodDoc },\n}\n"],
5
+ "mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAElB,SAAS,8BAA8B;AACvC;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,8BAA8B;AAGvC,SAAS,4BAA4B;AACrC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,0CAA0C;AAE5C,MAAM,WAAqD,EAAE,aAAa,MAAM;AAEvF,eAAsB,KAAK,KAAc;AACvC,QAAM,iBAAiB,MAAM,mCAAmC,GAAG;AACnE,QAAM,EAAE,OAAO,eAAe,IAAI,MAAM,mBAAmB;AAAA,IACzD;AAAA,IACA,UAAU;AAAA,IACV,gBAAgB;AAAA,IAChB,oBAAoB;AAAA,EACtB,CAAC;AACD,MAAI,eAAgB,QAAO;AAE3B,MAAI;AACJ,MAAI;AACF,WAAO,MAAM,IAAI,KAAK;AAAA,EACxB,QAAQ;AACN,WAAO,aAAa,KAAK,EAAE,IAAI,KAAK,CAAC;AAAA,EACvC;AAEA,QAAM,SAAS,uBAAuB,UAAU,IAAI;AACpD,MAAI,CAAC,OAAO,SAAS;AACnB,WAAO,aAAa,KAAK,EAAE,IAAI,KAAK,CAAC;AAAA,EACvC;AAEA,QAAM,EAAE,MAAM,IAAI,OAAO;AACzB,MAAI;AACJ,MAAI;AACF,UAAM,UAAU,MAAM,qBAAqB,KAAK,OAAO,KAAK,QAAQ;AACpE,eAAW,QAAQ;AAAA,EACrB,SAAS,KAAK;AACZ,QAAI,eAAe,uBAAuB;AACxC,aAAO,aAAa,KAAK,EAAE,IAAI,KAAK,CAAC;AAAA,IACvC;AACA,UAAM;AAAA,EACR;AAEA,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,sBAAsB,UAAU,QAAQ,qBAAqB;AACnE,QAAM,uBAAuB,UAAU,QAAQ,sBAAsB;AAErE,QAAM,OAAO,MAAM,oBAAoB,YAAY,OAAO,QAAQ;AAClE,MAAI,MAAM;AACR,UAAM,qBAAqB,gBAAgB,KAAK,IAAI,QAAQ;AAG5D,SAAK,OAAO,qDAAqD,EAAE;AAAA,MAAK,CAAC,EAAE,0BAA0B,MACnG,0BAA0B,0CAA0C;AAAA,QAClE,QAAQ,KAAK;AAAA,QACb;AAAA,QACA,gBAAgB,KAAK;AAAA,MACvB,CAAC;AAAA,IACH,EAAE,MAAM,MAAM,MAAS;AAAA,EACzB;AAEA,SAAO,aAAa,KAAK,EAAE,IAAI,KAAK,CAAC;AACvC;AAEA,MAAM,gBAAgB,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,IAAI,EAAE,CAAC;AAEtD,MAAM,YAA8B;AAAA,EAClC,SAAS;AAAA,EACT,aAAa;AAAA,EACb,MAAM,CAAC,yBAAyB;AAAA,EAChC,aAAa;AAAA,IACX,QAAQ;AAAA,IACR,aAAa;AAAA,EACf;AAAA,EACA,WAAW;AAAA,IACT,EAAE,QAAQ,KAAK,aAAa,oBAAoB,QAAQ,cAAc;AAAA,EACxE;AAAA,EACA,QAAQ;AAAA,IACN,EAAE,QAAQ,KAAK,aAAa,qBAAqB,QAAQ,qBAAqB;AAAA,EAChF;AACF;AAEO,MAAM,UAA2B;AAAA,EACtC,SAAS;AAAA,EACT,aAAa;AAAA,EACb,SAAS,EAAE,MAAM,UAAU;AAC7B;",
6
6
  "names": []
7
7
  }
@@ -1,6 +1,10 @@
1
1
  import { NextResponse } from "next/server";
2
2
  import { z } from "zod";
3
3
  import { passwordResetRequestSchema } from "@open-mercato/core/modules/customer_accounts/data/validators";
4
+ import {
5
+ resolveTenantContext,
6
+ TenantResolutionError
7
+ } from "@open-mercato/core/modules/customer_accounts/lib/resolveTenantContext";
4
8
  import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
5
9
  import { rateLimitErrorSchema } from "@open-mercato/shared/lib/ratelimit/helpers";
6
10
  import {
@@ -29,9 +33,16 @@ async function POST(req) {
29
33
  if (!parsed.success) {
30
34
  return NextResponse.json({ ok: true });
31
35
  }
32
- const { email, tenantId } = parsed.data;
33
- if (!tenantId) {
34
- return NextResponse.json({ ok: true });
36
+ const { email } = parsed.data;
37
+ let tenantId;
38
+ try {
39
+ const context = await resolveTenantContext(req, parsed.data.tenantId);
40
+ tenantId = context.tenantId;
41
+ } catch (err) {
42
+ if (err instanceof TenantResolutionError) {
43
+ return NextResponse.json({ ok: true });
44
+ }
45
+ throw err;
35
46
  }
36
47
  const container = await createRequestContainer();
37
48
  const customerUserService = container.resolve("customerUserService");
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/customer_accounts/api/password/reset-request.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc, OpenApiMethodDoc } from '@open-mercato/shared/lib/openapi'\nimport { passwordResetRequestSchema } from '@open-mercato/core/modules/customer_accounts/data/validators'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { CustomerUserService } from '@open-mercato/core/modules/customer_accounts/services/customerUserService'\nimport { CustomerTokenService } from '@open-mercato/core/modules/customer_accounts/services/customerTokenService'\nimport { rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport {\n checkAuthRateLimit,\n customerPasswordResetRateLimitConfig,\n customerPasswordResetIpRateLimitConfig,\n} from '@open-mercato/core/modules/customer_accounts/lib/rateLimiter'\nimport { readNormalizedEmailFromJsonRequest } from '@open-mercato/core/modules/customer_accounts/lib/rateLimitIdentifier'\n\nexport const metadata: { path?: string; requireAuth?: boolean } = { requireAuth: false }\n\nexport async function POST(req: Request) {\n const rateLimitEmail = await readNormalizedEmailFromJsonRequest(req)\n const { error: rateLimitError } = await checkAuthRateLimit({\n req,\n ipConfig: customerPasswordResetIpRateLimitConfig,\n compoundConfig: customerPasswordResetRateLimitConfig,\n compoundIdentifier: rateLimitEmail,\n })\n if (rateLimitError) return rateLimitError\n\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return NextResponse.json({ ok: true }) // Always 200\n }\n\n const parsed = passwordResetRequestSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json({ ok: true }) // Always 200 to prevent enumeration\n }\n\n const { email, tenantId } = parsed.data\n if (!tenantId) {\n return NextResponse.json({ ok: true })\n }\n\n const container = await createRequestContainer()\n const customerUserService = container.resolve('customerUserService') as CustomerUserService\n const customerTokenService = container.resolve('customerTokenService') as CustomerTokenService\n\n const user = await customerUserService.findByEmail(email, tenantId)\n if (user) {\n await customerTokenService.createPasswordReset(user.id, tenantId)\n // Token is stored in DB; email delivery should be handled by a direct service call,\n // NOT via the event bus \u2014 raw tokens must never travel through events.\n void import('@open-mercato/core/modules/customer_accounts/events').then(({ emitCustomerAccountsEvent }) =>\n emitCustomerAccountsEvent('customer_accounts.password_reset.requested', {\n userId: user.id,\n tenantId,\n })\n ).catch(() => undefined)\n }\n\n // Always return 200 to prevent email enumeration\n return NextResponse.json({ ok: true })\n}\n\nconst successSchema = z.object({ ok: z.literal(true) })\n\nconst methodDoc: OpenApiMethodDoc = {\n summary: 'Request customer password reset',\n description: 'Initiates a password reset flow. Always returns 200 to prevent email enumeration.',\n tags: ['Customer Authentication'],\n requestBody: {\n schema: passwordResetRequestSchema,\n description: 'Password reset request with email.',\n },\n responses: [\n { status: 200, description: 'Request accepted', schema: successSchema },\n ],\n errors: [\n { status: 429, description: 'Too many requests', schema: rateLimitErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Customer password reset request',\n description: 'Handles password reset initiation for customer accounts.',\n methods: { POST: methodDoc },\n}\n"],
5
- "mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAElB,SAAS,kCAAkC;AAC3C,SAAS,8BAA8B;AAGvC,SAAS,4BAA4B;AACrC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,0CAA0C;AAE5C,MAAM,WAAqD,EAAE,aAAa,MAAM;AAEvF,eAAsB,KAAK,KAAc;AACvC,QAAM,iBAAiB,MAAM,mCAAmC,GAAG;AACnE,QAAM,EAAE,OAAO,eAAe,IAAI,MAAM,mBAAmB;AAAA,IACzD;AAAA,IACA,UAAU;AAAA,IACV,gBAAgB;AAAA,IAChB,oBAAoB;AAAA,EACtB,CAAC;AACD,MAAI,eAAgB,QAAO;AAE3B,MAAI;AACJ,MAAI;AACF,WAAO,MAAM,IAAI,KAAK;AAAA,EACxB,QAAQ;AACN,WAAO,aAAa,KAAK,EAAE,IAAI,KAAK,CAAC;AAAA,EACvC;AAEA,QAAM,SAAS,2BAA2B,UAAU,IAAI;AACxD,MAAI,CAAC,OAAO,SAAS;AACnB,WAAO,aAAa,KAAK,EAAE,IAAI,KAAK,CAAC;AAAA,EACvC;AAEA,QAAM,EAAE,OAAO,SAAS,IAAI,OAAO;AACnC,MAAI,CAAC,UAAU;AACb,WAAO,aAAa,KAAK,EAAE,IAAI,KAAK,CAAC;AAAA,EACvC;AAEA,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,sBAAsB,UAAU,QAAQ,qBAAqB;AACnE,QAAM,uBAAuB,UAAU,QAAQ,sBAAsB;AAErE,QAAM,OAAO,MAAM,oBAAoB,YAAY,OAAO,QAAQ;AAClE,MAAI,MAAM;AACR,UAAM,qBAAqB,oBAAoB,KAAK,IAAI,QAAQ;AAGhE,SAAK,OAAO,qDAAqD,EAAE;AAAA,MAAK,CAAC,EAAE,0BAA0B,MACnG,0BAA0B,8CAA8C;AAAA,QACtE,QAAQ,KAAK;AAAA,QACb;AAAA,MACF,CAAC;AAAA,IACH,EAAE,MAAM,MAAM,MAAS;AAAA,EACzB;AAGA,SAAO,aAAa,KAAK,EAAE,IAAI,KAAK,CAAC;AACvC;AAEA,MAAM,gBAAgB,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,IAAI,EAAE,CAAC;AAEtD,MAAM,YAA8B;AAAA,EAClC,SAAS;AAAA,EACT,aAAa;AAAA,EACb,MAAM,CAAC,yBAAyB;AAAA,EAChC,aAAa;AAAA,IACX,QAAQ;AAAA,IACR,aAAa;AAAA,EACf;AAAA,EACA,WAAW;AAAA,IACT,EAAE,QAAQ,KAAK,aAAa,oBAAoB,QAAQ,cAAc;AAAA,EACxE;AAAA,EACA,QAAQ;AAAA,IACN,EAAE,QAAQ,KAAK,aAAa,qBAAqB,QAAQ,qBAAqB;AAAA,EAChF;AACF;AAEO,MAAM,UAA2B;AAAA,EACtC,SAAS;AAAA,EACT,aAAa;AAAA,EACb,SAAS,EAAE,MAAM,UAAU;AAC7B;",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc, OpenApiMethodDoc } from '@open-mercato/shared/lib/openapi'\nimport { passwordResetRequestSchema } from '@open-mercato/core/modules/customer_accounts/data/validators'\nimport {\n resolveTenantContext,\n TenantResolutionError,\n} from '@open-mercato/core/modules/customer_accounts/lib/resolveTenantContext'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { CustomerUserService } from '@open-mercato/core/modules/customer_accounts/services/customerUserService'\nimport { CustomerTokenService } from '@open-mercato/core/modules/customer_accounts/services/customerTokenService'\nimport { rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport {\n checkAuthRateLimit,\n customerPasswordResetRateLimitConfig,\n customerPasswordResetIpRateLimitConfig,\n} from '@open-mercato/core/modules/customer_accounts/lib/rateLimiter'\nimport { readNormalizedEmailFromJsonRequest } from '@open-mercato/core/modules/customer_accounts/lib/rateLimitIdentifier'\n\nexport const metadata: { path?: string; requireAuth?: boolean } = { requireAuth: false }\n\nexport async function POST(req: Request) {\n const rateLimitEmail = await readNormalizedEmailFromJsonRequest(req)\n const { error: rateLimitError } = await checkAuthRateLimit({\n req,\n ipConfig: customerPasswordResetIpRateLimitConfig,\n compoundConfig: customerPasswordResetRateLimitConfig,\n compoundIdentifier: rateLimitEmail,\n })\n if (rateLimitError) return rateLimitError\n\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return NextResponse.json({ ok: true }) // Always 200\n }\n\n const parsed = passwordResetRequestSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json({ ok: true }) // Always 200 to prevent enumeration\n }\n\n const { email } = parsed.data\n let tenantId: string\n try {\n const context = await resolveTenantContext(req, parsed.data.tenantId)\n tenantId = context.tenantId\n } catch (err) {\n if (err instanceof TenantResolutionError) {\n // Always return 200 to avoid leaking whether a tenant exists for this host.\n return NextResponse.json({ ok: true })\n }\n throw err\n }\n\n const container = await createRequestContainer()\n const customerUserService = container.resolve('customerUserService') as CustomerUserService\n const customerTokenService = container.resolve('customerTokenService') as CustomerTokenService\n\n const user = await customerUserService.findByEmail(email, tenantId)\n if (user) {\n await customerTokenService.createPasswordReset(user.id, tenantId)\n // Token is stored in DB; email delivery should be handled by a direct service call,\n // NOT via the event bus \u2014 raw tokens must never travel through events.\n void import('@open-mercato/core/modules/customer_accounts/events').then(({ emitCustomerAccountsEvent }) =>\n emitCustomerAccountsEvent('customer_accounts.password_reset.requested', {\n userId: user.id,\n tenantId,\n })\n ).catch(() => undefined)\n }\n\n // Always return 200 to prevent email enumeration\n return NextResponse.json({ ok: true })\n}\n\nconst successSchema = z.object({ ok: z.literal(true) })\n\nconst methodDoc: OpenApiMethodDoc = {\n summary: 'Request customer password reset',\n description: 'Initiates a password reset flow. Always returns 200 to prevent email enumeration.',\n tags: ['Customer Authentication'],\n requestBody: {\n schema: passwordResetRequestSchema,\n description: 'Password reset request with email.',\n },\n responses: [\n { status: 200, description: 'Request accepted', schema: successSchema },\n ],\n errors: [\n { status: 429, description: 'Too many requests', schema: rateLimitErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Customer password reset request',\n description: 'Handles password reset initiation for customer accounts.',\n methods: { POST: methodDoc },\n}\n"],
5
+ "mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAElB,SAAS,kCAAkC;AAC3C;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,8BAA8B;AAGvC,SAAS,4BAA4B;AACrC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,0CAA0C;AAE5C,MAAM,WAAqD,EAAE,aAAa,MAAM;AAEvF,eAAsB,KAAK,KAAc;AACvC,QAAM,iBAAiB,MAAM,mCAAmC,GAAG;AACnE,QAAM,EAAE,OAAO,eAAe,IAAI,MAAM,mBAAmB;AAAA,IACzD;AAAA,IACA,UAAU;AAAA,IACV,gBAAgB;AAAA,IAChB,oBAAoB;AAAA,EACtB,CAAC;AACD,MAAI,eAAgB,QAAO;AAE3B,MAAI;AACJ,MAAI;AACF,WAAO,MAAM,IAAI,KAAK;AAAA,EACxB,QAAQ;AACN,WAAO,aAAa,KAAK,EAAE,IAAI,KAAK,CAAC;AAAA,EACvC;AAEA,QAAM,SAAS,2BAA2B,UAAU,IAAI;AACxD,MAAI,CAAC,OAAO,SAAS;AACnB,WAAO,aAAa,KAAK,EAAE,IAAI,KAAK,CAAC;AAAA,EACvC;AAEA,QAAM,EAAE,MAAM,IAAI,OAAO;AACzB,MAAI;AACJ,MAAI;AACF,UAAM,UAAU,MAAM,qBAAqB,KAAK,OAAO,KAAK,QAAQ;AACpE,eAAW,QAAQ;AAAA,EACrB,SAAS,KAAK;AACZ,QAAI,eAAe,uBAAuB;AAExC,aAAO,aAAa,KAAK,EAAE,IAAI,KAAK,CAAC;AAAA,IACvC;AACA,UAAM;AAAA,EACR;AAEA,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,sBAAsB,UAAU,QAAQ,qBAAqB;AACnE,QAAM,uBAAuB,UAAU,QAAQ,sBAAsB;AAErE,QAAM,OAAO,MAAM,oBAAoB,YAAY,OAAO,QAAQ;AAClE,MAAI,MAAM;AACR,UAAM,qBAAqB,oBAAoB,KAAK,IAAI,QAAQ;AAGhE,SAAK,OAAO,qDAAqD,EAAE;AAAA,MAAK,CAAC,EAAE,0BAA0B,MACnG,0BAA0B,8CAA8C;AAAA,QACtE,QAAQ,KAAK;AAAA,QACb;AAAA,MACF,CAAC;AAAA,IACH,EAAE,MAAM,MAAM,MAAS;AAAA,EACzB;AAGA,SAAO,aAAa,KAAK,EAAE,IAAI,KAAK,CAAC;AACvC;AAEA,MAAM,gBAAgB,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,IAAI,EAAE,CAAC;AAEtD,MAAM,YAA8B;AAAA,EAClC,SAAS;AAAA,EACT,aAAa;AAAA,EACb,MAAM,CAAC,yBAAyB;AAAA,EAChC,aAAa;AAAA,IACX,QAAQ;AAAA,IACR,aAAa;AAAA,EACf;AAAA,EACA,WAAW;AAAA,IACT,EAAE,QAAQ,KAAK,aAAa,oBAAoB,QAAQ,cAAc;AAAA,EACxE;AAAA,EACA,QAAQ;AAAA,IACN,EAAE,QAAQ,KAAK,aAAa,qBAAqB,QAAQ,qBAAqB;AAAA,EAChF;AACF;AAEO,MAAM,UAA2B;AAAA,EACtC,SAAS;AAAA,EACT,aAAa;AAAA,EACb,SAAS,EAAE,MAAM,UAAU;AAC7B;",
6
6
  "names": []
7
7
  }
@@ -18,6 +18,11 @@ import {
18
18
  import { readNormalizedEmailFromJsonRequest } from "@open-mercato/core/modules/customer_accounts/lib/rateLimitIdentifier";
19
19
  import { findOrganizationInTenant } from "@open-mercato/core/modules/customer_accounts/lib/organizationLookup";
20
20
  import { getSecurityEmailBaseUrl, mapSecurityEmailUrlError } from "@open-mercato/shared/lib/url";
21
+ import {
22
+ resolveTenantContext,
23
+ TenantResolutionError
24
+ } from "@open-mercato/core/modules/customer_accounts/lib/resolveTenantContext";
25
+ import { urlForCustomerOrg } from "@open-mercato/core/modules/customer_accounts/lib/customerUrl";
21
26
  const metadata = { requireAuth: false };
22
27
  const TIMING_EQUALIZATION_HASH = "$2b$10$.F2A6UHFzk.d8trNdfqt4OLz05Nf3IOuMmN6VJKflhD4.rz.prR8i";
23
28
  function resolvePortalLoginUrl(baseUrl, organizationSlug) {
@@ -46,9 +51,21 @@ async function POST(req) {
46
51
  if (!parsed.success) {
47
52
  return NextResponse.json({ ok: false, error: "Validation failed", details: parsed.error.flatten().fieldErrors }, { status: 400 });
48
53
  }
49
- const { email, password, displayName, tenantId, organizationId } = parsed.data;
50
- if (!tenantId || !organizationId) {
51
- return NextResponse.json({ ok: false, error: "tenantId and organizationId are required" }, { status: 400 });
54
+ const { email, password, displayName } = parsed.data;
55
+ let tenantId;
56
+ let organizationId;
57
+ try {
58
+ const context = await resolveTenantContext(req, parsed.data.tenantId);
59
+ tenantId = context.tenantId;
60
+ organizationId = context.organizationId ?? parsed.data.organizationId ?? null;
61
+ } catch (err) {
62
+ if (err instanceof TenantResolutionError) {
63
+ return NextResponse.json({ ok: false, error: err.message }, { status: err.status });
64
+ }
65
+ throw err;
66
+ }
67
+ if (!organizationId) {
68
+ return NextResponse.json({ ok: false, error: "organizationId is required" }, { status: 400 });
52
69
  }
53
70
  let baseUrl;
54
71
  try {
@@ -74,7 +91,11 @@ async function POST(req) {
74
91
  if (existing) {
75
92
  await bcryptCompare(password, TIMING_EQUALIZATION_HASH);
76
93
  const existingOrg = await findOrganizationInTenant(em, existing.organizationId, tenantId);
77
- const loginUrl = resolvePortalLoginUrl(baseUrl, existingOrg?.slug ?? null);
94
+ let loginUrl = resolvePortalLoginUrl(baseUrl, existingOrg?.slug ?? null);
95
+ try {
96
+ loginUrl = await urlForCustomerOrg(existing.organizationId, "/login", { container });
97
+ } catch {
98
+ }
78
99
  const subject2 = translate("customer_accounts.signup.existing.subject", "You already have a portal account");
79
100
  const copy2 = {
80
101
  preview: translate("customer_accounts.signup.existing.preview", "A sign-up attempt was made for an email that already has a portal account."),
@@ -114,7 +135,13 @@ async function POST(req) {
114
135
  }
115
136
  await em.persist(user).flush();
116
137
  const verificationToken = await customerTokenService.createEmailVerification(user.id, tenantId);
117
- const verifyUrl = resolvePortalVerifyUrl(baseUrl, verificationToken, orgRow.slug);
138
+ let verifyUrl = resolvePortalVerifyUrl(baseUrl, verificationToken, orgRow.slug);
139
+ try {
140
+ verifyUrl = await urlForCustomerOrg(organizationId, `/verify?token=${encodeURIComponent(verificationToken)}`, {
141
+ container
142
+ });
143
+ } catch {
144
+ }
118
145
  const subject = translate("customer_accounts.signup.created.subject", "Verify your portal account");
119
146
  const copy = {
120
147
  preview: translate("customer_accounts.signup.created.preview", "Verify your portal account to finish sign-up."),