@open-mercato/core 0.6.1-develop.3260.1.dc719cac84 → 0.6.1-develop.3278.1.a4d26475e4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +2 -2
- package/dist/generated/entities/domain_mapping/index.js +31 -0
- package/dist/generated/entities/domain_mapping/index.js.map +7 -0
- package/dist/generated/entities.ids.generated.js +2 -1
- package/dist/generated/entities.ids.generated.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +16 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/helpers/integration/salesUi.js +18 -17
- package/dist/helpers/integration/salesUi.js.map +2 -2
- package/dist/modules/customer_accounts/acl.js +2 -1
- package/dist/modules/customer_accounts/acl.js.map +2 -2
- package/dist/modules/customer_accounts/api/admin/domain-mappings/[id]/health-check.js +110 -0
- package/dist/modules/customer_accounts/api/admin/domain-mappings/[id]/health-check.js.map +7 -0
- package/dist/modules/customer_accounts/api/admin/domain-mappings/[id]/verify.js +114 -0
- package/dist/modules/customer_accounts/api/admin/domain-mappings/[id]/verify.js.map +7 -0
- package/dist/modules/customer_accounts/api/admin/domain-mappings.js +271 -0
- package/dist/modules/customer_accounts/api/admin/domain-mappings.js.map +7 -0
- package/dist/modules/customer_accounts/api/domain-check.js +70 -0
- package/dist/modules/customer_accounts/api/domain-check.js.map +7 -0
- package/dist/modules/customer_accounts/api/domain-resolve/all.js +128 -0
- package/dist/modules/customer_accounts/api/domain-resolve/all.js.map +7 -0
- package/dist/modules/customer_accounts/api/domain-resolve.js +84 -0
- package/dist/modules/customer_accounts/api/domain-resolve.js.map +7 -0
- package/dist/modules/customer_accounts/api/login.js +14 -3
- package/dist/modules/customer_accounts/api/login.js.map +2 -2
- package/dist/modules/customer_accounts/api/magic-link/request.js +14 -3
- package/dist/modules/customer_accounts/api/magic-link/request.js.map +2 -2
- package/dist/modules/customer_accounts/api/password/reset-request.js +14 -3
- package/dist/modules/customer_accounts/api/password/reset-request.js.map +2 -2
- package/dist/modules/customer_accounts/api/signup.js +32 -5
- package/dist/modules/customer_accounts/api/signup.js.map +2 -2
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Actions.js +48 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Actions.js.map +7 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Diagnostics.js +43 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Diagnostics.js.map +7 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/DnsConfig.js +53 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/DnsConfig.js.map +7 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/DomainPanel.js +69 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/DomainPanel.js.map +7 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/OrgSwitcher.js +31 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/OrgSwitcher.js.map +7 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/RegisterDialog.js +147 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/RegisterDialog.js.map +7 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/StatusBadgeSection.js +39 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/StatusBadgeSection.js.map +7 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Stepper.js +60 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Stepper.js.map +7 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/domainStatusMap.js +11 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/domainStatusMap.js.map +7 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/types.js +1 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/types.js.map +7 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/page.js +361 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/page.js.map +7 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/page.meta.js +28 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/page.meta.js.map +7 -0
- package/dist/modules/customer_accounts/data/enrichers.js +78 -12
- package/dist/modules/customer_accounts/data/enrichers.js.map +2 -2
- package/dist/modules/customer_accounts/data/entities.js +85 -2
- package/dist/modules/customer_accounts/data/entities.js.map +2 -2
- package/dist/modules/customer_accounts/data/guards.js +130 -0
- package/dist/modules/customer_accounts/data/guards.js.map +7 -0
- package/dist/modules/customer_accounts/data/validators.js +19 -0
- package/dist/modules/customer_accounts/data/validators.js.map +2 -2
- package/dist/modules/customer_accounts/di.js +23 -1
- package/dist/modules/customer_accounts/di.js.map +2 -2
- package/dist/modules/customer_accounts/events.js +9 -2
- package/dist/modules/customer_accounts/events.js.map +2 -2
- package/dist/modules/customer_accounts/lib/customerAuthServer.js +27 -7
- package/dist/modules/customer_accounts/lib/customerAuthServer.js.map +2 -2
- package/dist/modules/customer_accounts/lib/customerUrl.js +48 -0
- package/dist/modules/customer_accounts/lib/customerUrl.js.map +7 -0
- package/dist/modules/customer_accounts/lib/hostname.js +55 -0
- package/dist/modules/customer_accounts/lib/hostname.js.map +7 -0
- package/dist/modules/customer_accounts/lib/platformDomains.js +7 -0
- package/dist/modules/customer_accounts/lib/platformDomains.js.map +7 -0
- package/dist/modules/customer_accounts/lib/proxyRanges.js +71 -0
- package/dist/modules/customer_accounts/lib/proxyRanges.js.map +7 -0
- package/dist/modules/customer_accounts/lib/rateLimiter.js +7 -0
- package/dist/modules/customer_accounts/lib/rateLimiter.js.map +2 -2
- package/dist/modules/customer_accounts/lib/resolveTenantContext.js +63 -0
- package/dist/modules/customer_accounts/lib/resolveTenantContext.js.map +7 -0
- package/dist/modules/customer_accounts/lib/secretCompare.js +13 -0
- package/dist/modules/customer_accounts/lib/secretCompare.js.map +7 -0
- package/dist/modules/customer_accounts/migrations/Migration20260430120000_customer_accounts.js +37 -0
- package/dist/modules/customer_accounts/migrations/Migration20260430120000_customer_accounts.js.map +7 -0
- package/dist/modules/customer_accounts/notifications.client.js +76 -0
- package/dist/modules/customer_accounts/notifications.client.js.map +2 -2
- package/dist/modules/customer_accounts/notifications.js +76 -0
- package/dist/modules/customer_accounts/notifications.js.map +2 -2
- package/dist/modules/customer_accounts/services/domainMappingService.js +497 -0
- package/dist/modules/customer_accounts/services/domainMappingService.js.map +7 -0
- package/dist/modules/customer_accounts/setup.js +48 -1
- package/dist/modules/customer_accounts/setup.js.map +2 -2
- package/dist/modules/customer_accounts/subscribers/invalidateDomainCache.js +29 -0
- package/dist/modules/customer_accounts/subscribers/invalidateDomainCache.js.map +7 -0
- package/dist/modules/customer_accounts/subscribers/notifyDomainActivated.js +29 -0
- package/dist/modules/customer_accounts/subscribers/notifyDomainActivated.js.map +7 -0
- package/dist/modules/customer_accounts/subscribers/notifyDomainDnsFailed.js +30 -0
- package/dist/modules/customer_accounts/subscribers/notifyDomainDnsFailed.js.map +7 -0
- package/dist/modules/customer_accounts/subscribers/notifyDomainTlsFailed.js +31 -0
- package/dist/modules/customer_accounts/subscribers/notifyDomainTlsFailed.js.map +7 -0
- package/dist/modules/customer_accounts/subscribers/notifyDomainVerified.js +29 -0
- package/dist/modules/customer_accounts/subscribers/notifyDomainVerified.js.map +7 -0
- package/dist/modules/customer_accounts/workers/domainTlsRetryWorker.js +67 -0
- package/dist/modules/customer_accounts/workers/domainTlsRetryWorker.js.map +7 -0
- package/dist/modules/customer_accounts/workers/domainVerificationWorker.js +34 -0
- package/dist/modules/customer_accounts/workers/domainVerificationWorker.js.map +7 -0
- package/dist/modules/customers/api/companies/route.js +4 -9
- package/dist/modules/customers/api/companies/route.js.map +2 -2
- package/dist/modules/customers/api/deals/route.js +4 -9
- package/dist/modules/customers/api/deals/route.js.map +2 -2
- package/dist/modules/customers/api/people/route.js +4 -9
- package/dist/modules/customers/api/people/route.js.map +2 -2
- package/dist/modules/customers/api/utils.js +0 -12
- package/dist/modules/customers/api/utils.js.map +2 -2
- package/dist/modules/customers/backend/customers/companies/page.js +361 -284
- package/dist/modules/customers/backend/customers/companies/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/page.js +378 -473
- package/dist/modules/customers/backend/customers/deals/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/people/page.js +368 -286
- package/dist/modules/customers/backend/customers/people/page.js.map +2 -2
- package/dist/modules/customers/components/detail/assignableStaff.js +15 -1
- package/dist/modules/customers/components/detail/assignableStaff.js.map +2 -2
- package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.js +188 -36
- package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.js.map +2 -2
- package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.js +2 -2
- package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.js.map +2 -2
- package/dist/modules/customers/widgets/injection-table.js +6 -0
- package/dist/modules/customers/widgets/injection-table.js.map +2 -2
- package/dist/modules/perspectives/services/perspectiveService.js +16 -2
- package/dist/modules/perspectives/services/perspectiveService.js.map +2 -2
- package/dist/modules/portal/frontend/[orgSlug]/portal/login/page.js +1 -0
- package/dist/modules/portal/frontend/[orgSlug]/portal/login/page.js.map +2 -2
- package/dist/modules/query_index/lib/engine.js +31 -8
- package/dist/modules/query_index/lib/engine.js.map +2 -2
- package/generated/entities/domain_mapping/index.ts +14 -0
- package/generated/entities.ids.generated.ts +2 -1
- package/generated/entity-fields-registry.ts +16 -0
- package/package.json +5 -5
- package/src/helpers/integration/salesUi.ts +27 -23
- package/src/modules/customer_accounts/acl.ts +1 -0
- package/src/modules/customer_accounts/api/admin/domain-mappings/[id]/health-check.ts +119 -0
- package/src/modules/customer_accounts/api/admin/domain-mappings/[id]/verify.ts +123 -0
- package/src/modules/customer_accounts/api/admin/domain-mappings.ts +303 -0
- package/src/modules/customer_accounts/api/domain-check.ts +83 -0
- package/src/modules/customer_accounts/api/domain-resolve/all.ts +139 -0
- package/src/modules/customer_accounts/api/domain-resolve.ts +91 -0
- package/src/modules/customer_accounts/api/login.ts +14 -3
- package/src/modules/customer_accounts/api/magic-link/request.ts +14 -3
- package/src/modules/customer_accounts/api/password/reset-request.ts +15 -3
- package/src/modules/customer_accounts/api/signup.ts +36 -5
- package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Actions.tsx +70 -0
- package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Diagnostics.tsx +66 -0
- package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/DnsConfig.tsx +78 -0
- package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/DomainPanel.tsx +90 -0
- package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/OrgSwitcher.tsx +43 -0
- package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/RegisterDialog.tsx +178 -0
- package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/StatusBadgeSection.tsx +56 -0
- package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Stepper.tsx +69 -0
- package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/domainStatusMap.ts +10 -0
- package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/types.ts +31 -0
- package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/page.meta.ts +26 -0
- package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/page.tsx +411 -0
- package/src/modules/customer_accounts/data/enrichers.ts +95 -12
- package/src/modules/customer_accounts/data/entities.ts +80 -1
- package/src/modules/customer_accounts/data/guards.ts +155 -0
- package/src/modules/customer_accounts/data/validators.ts +26 -0
- package/src/modules/customer_accounts/di.ts +35 -1
- package/src/modules/customer_accounts/events.ts +8 -1
- package/src/modules/customer_accounts/i18n/de.json +78 -0
- package/src/modules/customer_accounts/i18n/en.json +78 -0
- package/src/modules/customer_accounts/i18n/es.json +78 -0
- package/src/modules/customer_accounts/i18n/pl.json +78 -0
- package/src/modules/customer_accounts/lib/customerAuthServer.ts +44 -8
- package/src/modules/customer_accounts/lib/customerUrl.ts +72 -0
- package/src/modules/customer_accounts/lib/hostname.ts +76 -0
- package/src/modules/customer_accounts/lib/platformDomains.ts +14 -0
- package/src/modules/customer_accounts/lib/proxyRanges.ts +85 -0
- package/src/modules/customer_accounts/lib/rateLimiter.ts +10 -0
- package/src/modules/customer_accounts/lib/resolveTenantContext.ts +96 -0
- package/src/modules/customer_accounts/lib/secretCompare.ts +20 -0
- package/src/modules/customer_accounts/migrations/.snapshot-open-mercato.json +341 -0
- package/src/modules/customer_accounts/migrations/Migration20260430120000_customer_accounts.ts +38 -0
- package/src/modules/customer_accounts/notifications.client.ts +76 -0
- package/src/modules/customer_accounts/notifications.ts +76 -0
- package/src/modules/customer_accounts/services/domainMappingService.ts +632 -0
- package/src/modules/customer_accounts/setup.ts +81 -1
- package/src/modules/customer_accounts/subscribers/invalidateDomainCache.ts +35 -0
- package/src/modules/customer_accounts/subscribers/notifyDomainActivated.ts +32 -0
- package/src/modules/customer_accounts/subscribers/notifyDomainDnsFailed.ts +33 -0
- package/src/modules/customer_accounts/subscribers/notifyDomainTlsFailed.ts +34 -0
- package/src/modules/customer_accounts/subscribers/notifyDomainVerified.ts +32 -0
- package/src/modules/customer_accounts/workers/domainTlsRetryWorker.ts +84 -0
- package/src/modules/customer_accounts/workers/domainVerificationWorker.ts +45 -0
- package/src/modules/customers/api/companies/route.ts +4 -9
- package/src/modules/customers/api/deals/route.ts +4 -9
- package/src/modules/customers/api/people/route.ts +4 -9
- package/src/modules/customers/api/utils.ts +0 -14
- package/src/modules/customers/backend/customers/companies/page.tsx +321 -237
- package/src/modules/customers/backend/customers/deals/page.tsx +321 -455
- package/src/modules/customers/backend/customers/people/page.tsx +331 -240
- package/src/modules/customers/components/detail/assignableStaff.ts +21 -0
- package/src/modules/customers/i18n/de.json +31 -0
- package/src/modules/customers/i18n/en.json +31 -0
- package/src/modules/customers/i18n/es.json +31 -0
- package/src/modules/customers/i18n/pl.json +31 -0
- package/src/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.tsx +197 -40
- package/src/modules/customers/widgets/injection/ai-assistant-trigger/widget.ts +13 -7
- package/src/modules/customers/widgets/injection-table.ts +6 -0
- package/src/modules/perspectives/services/perspectiveService.ts +30 -2
- package/src/modules/portal/frontend/[orgSlug]/portal/login/page.tsx +1 -0
- package/src/modules/query_index/lib/engine.ts +46 -8
|
@@ -1,15 +1,37 @@
|
|
|
1
|
-
import { asClass } from "awilix";
|
|
1
|
+
import { asClass, asFunction } from "awilix";
|
|
2
2
|
import { CustomerUserService } from "@open-mercato/core/modules/customer_accounts/services/customerUserService";
|
|
3
3
|
import { CustomerSessionService } from "@open-mercato/core/modules/customer_accounts/services/customerSessionService";
|
|
4
4
|
import { CustomerTokenService } from "@open-mercato/core/modules/customer_accounts/services/customerTokenService";
|
|
5
5
|
import { CustomerRbacService } from "@open-mercato/core/modules/customer_accounts/services/customerRbacService";
|
|
6
6
|
import { CustomerInvitationService } from "@open-mercato/core/modules/customer_accounts/services/customerInvitationService";
|
|
7
|
+
import { DomainMappingService } from "@open-mercato/core/modules/customer_accounts/services/domainMappingService";
|
|
8
|
+
function isDomainCacheService(candidate) {
|
|
9
|
+
if (!candidate || typeof candidate !== "object") return false;
|
|
10
|
+
const c = candidate;
|
|
11
|
+
return typeof c.get === "function" && typeof c.set === "function" && typeof c.deleteByTags === "function";
|
|
12
|
+
}
|
|
7
13
|
function register(container) {
|
|
8
14
|
container.register({ customerUserService: asClass(CustomerUserService).scoped() });
|
|
9
15
|
container.register({ customerSessionService: asClass(CustomerSessionService).scoped() });
|
|
10
16
|
container.register({ customerTokenService: asClass(CustomerTokenService).scoped() });
|
|
11
17
|
container.register({ customerRbacService: asClass(CustomerRbacService).scoped() });
|
|
12
18
|
container.register({ customerInvitationService: asClass(CustomerInvitationService).scoped() });
|
|
19
|
+
container.register({
|
|
20
|
+
domainMappingService: asFunction(
|
|
21
|
+
function domainMappingServiceFactory(em) {
|
|
22
|
+
let cacheService;
|
|
23
|
+
try {
|
|
24
|
+
if (container.hasRegistration("cache")) {
|
|
25
|
+
const candidate = container.resolve("cache");
|
|
26
|
+
if (isDomainCacheService(candidate)) cacheService = candidate;
|
|
27
|
+
}
|
|
28
|
+
} catch {
|
|
29
|
+
cacheService = void 0;
|
|
30
|
+
}
|
|
31
|
+
return new DomainMappingService(em, cacheService ? { cacheService } : void 0);
|
|
32
|
+
}
|
|
33
|
+
).scoped()
|
|
34
|
+
});
|
|
13
35
|
}
|
|
14
36
|
export {
|
|
15
37
|
register
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/modules/customer_accounts/di.ts"],
|
|
4
|
-
"sourcesContent": ["import { asClass } from 'awilix'\nimport type { AppContainer } 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 { CustomerTokenService } from '@open-mercato/core/modules/customer_accounts/services/customerTokenService'\nimport { CustomerRbacService } from '@open-mercato/core/modules/customer_accounts/services/customerRbacService'\nimport { CustomerInvitationService } from '@open-mercato/core/modules/customer_accounts/services/customerInvitationService'\n\nexport function register(container: AppContainer) {\n container.register({ customerUserService: asClass(CustomerUserService).scoped() })\n container.register({ customerSessionService: asClass(CustomerSessionService).scoped() })\n container.register({ customerTokenService: asClass(CustomerTokenService).scoped() })\n container.register({ customerRbacService: asClass(CustomerRbacService).scoped() })\n container.register({ customerInvitationService: asClass(CustomerInvitationService).scoped() })\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,
|
|
4
|
+
"sourcesContent": ["import { asClass, asFunction } from 'awilix'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { AppContainer } 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 { CustomerTokenService } from '@open-mercato/core/modules/customer_accounts/services/customerTokenService'\nimport { CustomerRbacService } from '@open-mercato/core/modules/customer_accounts/services/customerRbacService'\nimport { CustomerInvitationService } from '@open-mercato/core/modules/customer_accounts/services/customerInvitationService'\nimport { DomainMappingService } from '@open-mercato/core/modules/customer_accounts/services/domainMappingService'\n\ntype DomainCacheService = {\n get: (key: string, options?: unknown) => Promise<unknown>\n set: (key: string, value: unknown, options?: { ttl?: number; tags?: string[] }) => Promise<void>\n deleteByTags: (tags: string[]) => Promise<number>\n}\n\nfunction isDomainCacheService(candidate: unknown): candidate is DomainCacheService {\n if (!candidate || typeof candidate !== 'object') return false\n const c = candidate as Record<string, unknown>\n return typeof c.get === 'function' && typeof c.set === 'function' && typeof c.deleteByTags === 'function'\n}\n\nexport function register(container: AppContainer) {\n container.register({ customerUserService: asClass(CustomerUserService).scoped() })\n container.register({ customerSessionService: asClass(CustomerSessionService).scoped() })\n container.register({ customerTokenService: asClass(CustomerTokenService).scoped() })\n container.register({ customerRbacService: asClass(CustomerRbacService).scoped() })\n container.register({ customerInvitationService: asClass(CustomerInvitationService).scoped() })\n container.register({\n domainMappingService: asFunction(\n function domainMappingServiceFactory(em: EntityManager) {\n // Resolve the cache lazily so registrations from `bootstrap(container)` (which runs after\n // module DI registrars) are visible. Awilix CLASSIC mode would fail to resolve a\n // destructured `cache` param if the key is not yet registered; using `hasRegistration`\n // keeps construction safe for CLI/test contexts where `cache` may never be wired.\n let cacheService: DomainCacheService | undefined\n try {\n if (container.hasRegistration('cache')) {\n const candidate = container.resolve('cache')\n if (isDomainCacheService(candidate)) cacheService = candidate\n }\n } catch {\n cacheService = undefined\n }\n return new DomainMappingService(em, cacheService ? { cacheService } : undefined)\n },\n ).scoped(),\n })\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,SAAS,kBAAkB;AAGpC,SAAS,2BAA2B;AACpC,SAAS,8BAA8B;AACvC,SAAS,4BAA4B;AACrC,SAAS,2BAA2B;AACpC,SAAS,iCAAiC;AAC1C,SAAS,4BAA4B;AAQrC,SAAS,qBAAqB,WAAqD;AACjF,MAAI,CAAC,aAAa,OAAO,cAAc,SAAU,QAAO;AACxD,QAAM,IAAI;AACV,SAAO,OAAO,EAAE,QAAQ,cAAc,OAAO,EAAE,QAAQ,cAAc,OAAO,EAAE,iBAAiB;AACjG;AAEO,SAAS,SAAS,WAAyB;AAChD,YAAU,SAAS,EAAE,qBAAqB,QAAQ,mBAAmB,EAAE,OAAO,EAAE,CAAC;AACjF,YAAU,SAAS,EAAE,wBAAwB,QAAQ,sBAAsB,EAAE,OAAO,EAAE,CAAC;AACvF,YAAU,SAAS,EAAE,sBAAsB,QAAQ,oBAAoB,EAAE,OAAO,EAAE,CAAC;AACnF,YAAU,SAAS,EAAE,qBAAqB,QAAQ,mBAAmB,EAAE,OAAO,EAAE,CAAC;AACjF,YAAU,SAAS,EAAE,2BAA2B,QAAQ,yBAAyB,EAAE,OAAO,EAAE,CAAC;AAC7F,YAAU,SAAS;AAAA,IACjB,sBAAsB;AAAA,MACpB,SAAS,4BAA4B,IAAmB;AAKtD,YAAI;AACJ,YAAI;AACF,cAAI,UAAU,gBAAgB,OAAO,GAAG;AACtC,kBAAM,YAAY,UAAU,QAAQ,OAAO;AAC3C,gBAAI,qBAAqB,SAAS,EAAG,gBAAe;AAAA,UACtD;AAAA,QACF,QAAQ;AACN,yBAAe;AAAA,QACjB;AACA,eAAO,IAAI,qBAAqB,IAAI,eAAe,EAAE,aAAa,IAAI,MAAS;AAAA,MACjF;AAAA,IACF,EAAE,OAAO;AAAA,EACX,CAAC;AACH;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -16,8 +16,15 @@ const events = [
|
|
|
16
16
|
{ id: "customer_accounts.role.updated", label: "Customer Role Updated", entity: "role", category: "crud", portalBroadcast: true },
|
|
17
17
|
{ id: "customer_accounts.role.deleted", label: "Customer Role Deleted", entity: "role", category: "crud" },
|
|
18
18
|
{ id: "customer_accounts.invitation.accepted", label: "Customer Invitation Accepted", category: "lifecycle", clientBroadcast: true },
|
|
19
|
-
{ id: "customer_accounts.
|
|
20
|
-
|
|
19
|
+
{ id: "customer_accounts.password_reset.requested", label: "Customer Password Reset Requested", category: "lifecycle" },
|
|
20
|
+
// Custom domain mapping lifecycle (see .ai/specs/2026-04-08-portal-custom-domain-routing.md)
|
|
21
|
+
{ id: "customer_accounts.domain_mapping.created", label: "Custom Domain Registered", entity: "domain_mapping", category: "crud", clientBroadcast: true },
|
|
22
|
+
{ id: "customer_accounts.domain_mapping.verified", label: "Custom Domain DNS Verified", entity: "domain_mapping", category: "lifecycle", clientBroadcast: true },
|
|
23
|
+
{ id: "customer_accounts.domain_mapping.activated", label: "Custom Domain Active", entity: "domain_mapping", category: "lifecycle", clientBroadcast: true },
|
|
24
|
+
{ id: "customer_accounts.domain_mapping.dns_failed", label: "Custom Domain DNS Verification Failed", entity: "domain_mapping", category: "lifecycle", clientBroadcast: true },
|
|
25
|
+
{ id: "customer_accounts.domain_mapping.tls_failed", label: "Custom Domain SSL Provisioning Failed", entity: "domain_mapping", category: "lifecycle", clientBroadcast: true },
|
|
26
|
+
{ id: "customer_accounts.domain_mapping.deleted", label: "Custom Domain Removed", entity: "domain_mapping", category: "crud", clientBroadcast: true },
|
|
27
|
+
{ id: "customer_accounts.domain_mapping.replaced", label: "Custom Domain Replaced", entity: "domain_mapping", category: "lifecycle", clientBroadcast: true }
|
|
21
28
|
];
|
|
22
29
|
const eventsConfig = createModuleEvents({
|
|
23
30
|
moduleId: "customer_accounts",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/modules/customer_accounts/events.ts"],
|
|
4
|
-
"sourcesContent": ["import { createModuleEvents } from '@open-mercato/shared/modules/events'\n\nconst events = [\n { id: 'customer_accounts.user.created', label: 'Customer User Created', entity: 'user', category: 'crud', clientBroadcast: true },\n { id: 'customer_accounts.user.updated', label: 'Customer User Updated', entity: 'user', category: 'crud', portalBroadcast: true },\n { id: 'customer_accounts.user.deleted', label: 'Customer User Deleted', entity: 'user', category: 'crud' },\n { id: 'customer_accounts.user.locked', label: 'Customer User Locked', entity: 'user', category: 'lifecycle', portalBroadcast: true },\n { id: 'customer_accounts.user.unlocked', label: 'Customer User Unlocked', entity: 'user', category: 'lifecycle', portalBroadcast: true },\n { id: 'customer_accounts.login.success', label: 'Customer Login Successful', category: 'lifecycle' },\n { id: 'customer_accounts.login.failed', label: 'Customer Login Failed', category: 'lifecycle' },\n { id: 'customer_accounts.magic_link.requested', label: 'Customer Magic Link Requested', category: 'lifecycle' },\n { id: 'customer_accounts.email.verified', label: 'Customer Email Verified', category: 'lifecycle', portalBroadcast: true },\n { id: 'customer_accounts.password.reset_requested', label: 'Customer Password Reset Requested', category: 'lifecycle' },\n { id: 'customer_accounts.password.reset', label: 'Customer Password Reset', category: 'lifecycle', portalBroadcast: true },\n { id: 'customer_accounts.password.changed', label: 'Customer Password Changed', category: 'lifecycle' },\n { id: 'customer_accounts.role.created', label: 'Customer Role Created', entity: 'role', category: 'crud' },\n { id: 'customer_accounts.role.updated', label: 'Customer Role Updated', entity: 'role', category: 'crud', portalBroadcast: true },\n { id: 'customer_accounts.role.deleted', label: 'Customer Role Deleted', entity: 'role', category: 'crud' },\n { id: 'customer_accounts.invitation.accepted', label: 'Customer Invitation Accepted', category: 'lifecycle', clientBroadcast: true },\n { id: 'customer_accounts.
|
|
5
|
-
"mappings": "AAAA,SAAS,0BAA0B;AAEnC,MAAM,SAAS;AAAA,EACb,EAAE,IAAI,kCAAkC,OAAO,yBAAyB,QAAQ,QAAQ,UAAU,QAAQ,iBAAiB,KAAK;AAAA,EAChI,EAAE,IAAI,kCAAkC,OAAO,yBAAyB,QAAQ,QAAQ,UAAU,QAAQ,iBAAiB,KAAK;AAAA,EAChI,EAAE,IAAI,kCAAkC,OAAO,yBAAyB,QAAQ,QAAQ,UAAU,OAAO;AAAA,EACzG,EAAE,IAAI,iCAAiC,OAAO,wBAAwB,QAAQ,QAAQ,UAAU,aAAa,iBAAiB,KAAK;AAAA,EACnI,EAAE,IAAI,mCAAmC,OAAO,0BAA0B,QAAQ,QAAQ,UAAU,aAAa,iBAAiB,KAAK;AAAA,EACvI,EAAE,IAAI,mCAAmC,OAAO,6BAA6B,UAAU,YAAY;AAAA,EACnG,EAAE,IAAI,kCAAkC,OAAO,yBAAyB,UAAU,YAAY;AAAA,EAC9F,EAAE,IAAI,0CAA0C,OAAO,iCAAiC,UAAU,YAAY;AAAA,EAC9G,EAAE,IAAI,oCAAoC,OAAO,2BAA2B,UAAU,aAAa,iBAAiB,KAAK;AAAA,EACzH,EAAE,IAAI,8CAA8C,OAAO,qCAAqC,UAAU,YAAY;AAAA,EACtH,EAAE,IAAI,oCAAoC,OAAO,2BAA2B,UAAU,aAAa,iBAAiB,KAAK;AAAA,EACzH,EAAE,IAAI,sCAAsC,OAAO,6BAA6B,UAAU,YAAY;AAAA,EACtG,EAAE,IAAI,kCAAkC,OAAO,yBAAyB,QAAQ,QAAQ,UAAU,OAAO;AAAA,EACzG,EAAE,IAAI,kCAAkC,OAAO,yBAAyB,QAAQ,QAAQ,UAAU,QAAQ,iBAAiB,KAAK;AAAA,EAChI,EAAE,IAAI,kCAAkC,OAAO,yBAAyB,QAAQ,QAAQ,UAAU,OAAO;AAAA,EACzG,EAAE,IAAI,yCAAyC,OAAO,gCAAgC,UAAU,aAAa,iBAAiB,KAAK;AAAA,EACnI,EAAE,IAAI,
|
|
4
|
+
"sourcesContent": ["import { createModuleEvents } from '@open-mercato/shared/modules/events'\n\nconst events = [\n { id: 'customer_accounts.user.created', label: 'Customer User Created', entity: 'user', category: 'crud', clientBroadcast: true },\n { id: 'customer_accounts.user.updated', label: 'Customer User Updated', entity: 'user', category: 'crud', portalBroadcast: true },\n { id: 'customer_accounts.user.deleted', label: 'Customer User Deleted', entity: 'user', category: 'crud' },\n { id: 'customer_accounts.user.locked', label: 'Customer User Locked', entity: 'user', category: 'lifecycle', portalBroadcast: true },\n { id: 'customer_accounts.user.unlocked', label: 'Customer User Unlocked', entity: 'user', category: 'lifecycle', portalBroadcast: true },\n { id: 'customer_accounts.login.success', label: 'Customer Login Successful', category: 'lifecycle' },\n { id: 'customer_accounts.login.failed', label: 'Customer Login Failed', category: 'lifecycle' },\n { id: 'customer_accounts.magic_link.requested', label: 'Customer Magic Link Requested', category: 'lifecycle' },\n { id: 'customer_accounts.email.verified', label: 'Customer Email Verified', category: 'lifecycle', portalBroadcast: true },\n { id: 'customer_accounts.password.reset_requested', label: 'Customer Password Reset Requested', category: 'lifecycle' },\n { id: 'customer_accounts.password.reset', label: 'Customer Password Reset', category: 'lifecycle', portalBroadcast: true },\n { id: 'customer_accounts.password.changed', label: 'Customer Password Changed', category: 'lifecycle' },\n { id: 'customer_accounts.role.created', label: 'Customer Role Created', entity: 'role', category: 'crud' },\n { id: 'customer_accounts.role.updated', label: 'Customer Role Updated', entity: 'role', category: 'crud', portalBroadcast: true },\n { id: 'customer_accounts.role.deleted', label: 'Customer Role Deleted', entity: 'role', category: 'crud' },\n { id: 'customer_accounts.invitation.accepted', label: 'Customer Invitation Accepted', category: 'lifecycle', clientBroadcast: true },\n { id: 'customer_accounts.password_reset.requested', label: 'Customer Password Reset Requested', category: 'lifecycle' },\n // Custom domain mapping lifecycle (see .ai/specs/2026-04-08-portal-custom-domain-routing.md)\n { id: 'customer_accounts.domain_mapping.created', label: 'Custom Domain Registered', entity: 'domain_mapping', category: 'crud', clientBroadcast: true },\n { id: 'customer_accounts.domain_mapping.verified', label: 'Custom Domain DNS Verified', entity: 'domain_mapping', category: 'lifecycle', clientBroadcast: true },\n { id: 'customer_accounts.domain_mapping.activated', label: 'Custom Domain Active', entity: 'domain_mapping', category: 'lifecycle', clientBroadcast: true },\n { id: 'customer_accounts.domain_mapping.dns_failed', label: 'Custom Domain DNS Verification Failed', entity: 'domain_mapping', category: 'lifecycle', clientBroadcast: true },\n { id: 'customer_accounts.domain_mapping.tls_failed', label: 'Custom Domain SSL Provisioning Failed', entity: 'domain_mapping', category: 'lifecycle', clientBroadcast: true },\n { id: 'customer_accounts.domain_mapping.deleted', label: 'Custom Domain Removed', entity: 'domain_mapping', category: 'crud', clientBroadcast: true },\n { id: 'customer_accounts.domain_mapping.replaced', label: 'Custom Domain Replaced', entity: 'domain_mapping', category: 'lifecycle', clientBroadcast: true },\n] as const\n\nexport const eventsConfig = createModuleEvents({\n moduleId: 'customer_accounts',\n events,\n})\n\nexport const emitCustomerAccountsEvent = eventsConfig.emit\n\nexport type CustomerAccountsEventId = typeof events[number]['id']\n\nexport default eventsConfig\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,0BAA0B;AAEnC,MAAM,SAAS;AAAA,EACb,EAAE,IAAI,kCAAkC,OAAO,yBAAyB,QAAQ,QAAQ,UAAU,QAAQ,iBAAiB,KAAK;AAAA,EAChI,EAAE,IAAI,kCAAkC,OAAO,yBAAyB,QAAQ,QAAQ,UAAU,QAAQ,iBAAiB,KAAK;AAAA,EAChI,EAAE,IAAI,kCAAkC,OAAO,yBAAyB,QAAQ,QAAQ,UAAU,OAAO;AAAA,EACzG,EAAE,IAAI,iCAAiC,OAAO,wBAAwB,QAAQ,QAAQ,UAAU,aAAa,iBAAiB,KAAK;AAAA,EACnI,EAAE,IAAI,mCAAmC,OAAO,0BAA0B,QAAQ,QAAQ,UAAU,aAAa,iBAAiB,KAAK;AAAA,EACvI,EAAE,IAAI,mCAAmC,OAAO,6BAA6B,UAAU,YAAY;AAAA,EACnG,EAAE,IAAI,kCAAkC,OAAO,yBAAyB,UAAU,YAAY;AAAA,EAC9F,EAAE,IAAI,0CAA0C,OAAO,iCAAiC,UAAU,YAAY;AAAA,EAC9G,EAAE,IAAI,oCAAoC,OAAO,2BAA2B,UAAU,aAAa,iBAAiB,KAAK;AAAA,EACzH,EAAE,IAAI,8CAA8C,OAAO,qCAAqC,UAAU,YAAY;AAAA,EACtH,EAAE,IAAI,oCAAoC,OAAO,2BAA2B,UAAU,aAAa,iBAAiB,KAAK;AAAA,EACzH,EAAE,IAAI,sCAAsC,OAAO,6BAA6B,UAAU,YAAY;AAAA,EACtG,EAAE,IAAI,kCAAkC,OAAO,yBAAyB,QAAQ,QAAQ,UAAU,OAAO;AAAA,EACzG,EAAE,IAAI,kCAAkC,OAAO,yBAAyB,QAAQ,QAAQ,UAAU,QAAQ,iBAAiB,KAAK;AAAA,EAChI,EAAE,IAAI,kCAAkC,OAAO,yBAAyB,QAAQ,QAAQ,UAAU,OAAO;AAAA,EACzG,EAAE,IAAI,yCAAyC,OAAO,gCAAgC,UAAU,aAAa,iBAAiB,KAAK;AAAA,EACnI,EAAE,IAAI,8CAA8C,OAAO,qCAAqC,UAAU,YAAY;AAAA;AAAA,EAEtH,EAAE,IAAI,4CAA4C,OAAO,4BAA4B,QAAQ,kBAAkB,UAAU,QAAQ,iBAAiB,KAAK;AAAA,EACvJ,EAAE,IAAI,6CAA6C,OAAO,8BAA8B,QAAQ,kBAAkB,UAAU,aAAa,iBAAiB,KAAK;AAAA,EAC/J,EAAE,IAAI,8CAA8C,OAAO,wBAAwB,QAAQ,kBAAkB,UAAU,aAAa,iBAAiB,KAAK;AAAA,EAC1J,EAAE,IAAI,+CAA+C,OAAO,yCAAyC,QAAQ,kBAAkB,UAAU,aAAa,iBAAiB,KAAK;AAAA,EAC5K,EAAE,IAAI,+CAA+C,OAAO,yCAAyC,QAAQ,kBAAkB,UAAU,aAAa,iBAAiB,KAAK;AAAA,EAC5K,EAAE,IAAI,4CAA4C,OAAO,yBAAyB,QAAQ,kBAAkB,UAAU,QAAQ,iBAAiB,KAAK;AAAA,EACpJ,EAAE,IAAI,6CAA6C,OAAO,0BAA0B,QAAQ,kBAAkB,UAAU,aAAa,iBAAiB,KAAK;AAC7J;AAEO,MAAM,eAAe,mBAAmB;AAAA,EAC7C,UAAU;AAAA,EACV;AACF,CAAC;AAEM,MAAM,4BAA4B,aAAa;AAItD,IAAO,iBAAQ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { cookies } from "next/headers";
|
|
2
|
+
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
2
3
|
import { verifyAudienceJwt } from "@open-mercato/shared/lib/auth/jwt";
|
|
3
4
|
import { CUSTOMER_JWT_AUDIENCE } from "@open-mercato/core/modules/customer_accounts/services/customerSessionService";
|
|
5
|
+
import { tryNormalizeHostname } from "@open-mercato/core/modules/customer_accounts/lib/hostname";
|
|
6
|
+
import { platformDomains } from "@open-mercato/core/modules/customer_accounts/lib/platformDomains";
|
|
4
7
|
import { validateUserState } from "./customerAuth.js";
|
|
5
8
|
async function assertSessionStillActive(sessionId) {
|
|
6
9
|
try {
|
|
7
|
-
const [{ createRequestContainer }, { CustomerSessionService }] = await Promise.all([
|
|
8
|
-
import("@open-mercato/shared/lib/di/container"),
|
|
9
|
-
import("@open-mercato/core/modules/customer_accounts/services/customerSessionService")
|
|
10
|
-
]);
|
|
11
10
|
const container = await createRequestContainer();
|
|
12
11
|
const service = container.resolve("customerSessionService");
|
|
13
12
|
const session = await service.findActiveSessionById(sessionId);
|
|
@@ -16,7 +15,7 @@ async function assertSessionStillActive(sessionId) {
|
|
|
16
15
|
return false;
|
|
17
16
|
}
|
|
18
17
|
}
|
|
19
|
-
async function getCustomerAuthFromCookies() {
|
|
18
|
+
async function getCustomerAuthFromCookies(options) {
|
|
20
19
|
const cookieStore = await cookies();
|
|
21
20
|
const token = cookieStore.get("customer_auth_token")?.value;
|
|
22
21
|
if (!token) return null;
|
|
@@ -26,11 +25,15 @@ async function getCustomerAuthFromCookies() {
|
|
|
26
25
|
if (payload.type !== "customer") return null;
|
|
27
26
|
const sid = typeof payload.sid === "string" ? payload.sid : "";
|
|
28
27
|
if (!sid) return null;
|
|
28
|
+
const tenantId = String(payload.tenantId);
|
|
29
|
+
if (options?.expectedTenantId && options.expectedTenantId !== tenantId) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
29
32
|
const stillActive = await assertSessionStillActive(sid);
|
|
30
33
|
if (!stillActive) return null;
|
|
31
34
|
const userState = await validateUserState(
|
|
32
35
|
String(payload.sub),
|
|
33
|
-
|
|
36
|
+
tenantId,
|
|
34
37
|
String(payload.orgId),
|
|
35
38
|
payload.iat
|
|
36
39
|
);
|
|
@@ -39,7 +42,7 @@ async function getCustomerAuthFromCookies() {
|
|
|
39
42
|
sub: String(payload.sub),
|
|
40
43
|
sid,
|
|
41
44
|
type: "customer",
|
|
42
|
-
tenantId
|
|
45
|
+
tenantId,
|
|
43
46
|
orgId: String(payload.orgId),
|
|
44
47
|
email: String(payload.email || ""),
|
|
45
48
|
displayName: String(payload.displayName || ""),
|
|
@@ -51,7 +54,24 @@ async function getCustomerAuthFromCookies() {
|
|
|
51
54
|
return null;
|
|
52
55
|
}
|
|
53
56
|
}
|
|
57
|
+
async function getCustomerAuthForHost(host) {
|
|
58
|
+
if (!host) return getCustomerAuthFromCookies();
|
|
59
|
+
try {
|
|
60
|
+
const hostname = tryNormalizeHostname(host);
|
|
61
|
+
if (!hostname) return getCustomerAuthFromCookies();
|
|
62
|
+
if (platformDomains().includes(hostname)) return getCustomerAuthFromCookies();
|
|
63
|
+
const container = await createRequestContainer();
|
|
64
|
+
const service = container.resolve("domainMappingService");
|
|
65
|
+
const resolved = await service.resolveByHostname(hostname);
|
|
66
|
+
if (!resolved || resolved.status !== "active") return null;
|
|
67
|
+
return getCustomerAuthFromCookies({ expectedTenantId: resolved.tenantId });
|
|
68
|
+
} catch (err) {
|
|
69
|
+
console.warn("[customer_accounts] getCustomerAuthForHost: domain resolve failed; falling back to platform cookie", err);
|
|
70
|
+
return getCustomerAuthFromCookies();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
54
73
|
export {
|
|
74
|
+
getCustomerAuthForHost,
|
|
55
75
|
getCustomerAuthFromCookies
|
|
56
76
|
};
|
|
57
77
|
//# sourceMappingURL=customerAuthServer.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/customer_accounts/lib/customerAuthServer.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Server-side customer auth helpers for Next.js server components.\n *\n * Uses the `cookies()` API from `next/headers` to read the customer auth token\n * from cookies \u2014 analogous to `getAuthFromCookies()` for staff auth.\n */\n\nimport { cookies } from 'next/headers'\nimport { verifyAudienceJwt } from '@open-mercato/shared/lib/auth/jwt'\nimport { CUSTOMER_JWT_AUDIENCE } from '@open-mercato/core/modules/customer_accounts/services/customerSessionService'\nimport { validateUserState } from './customerAuth'\nimport type { CustomerAuthContext } from './customerAuth'\n\nexport type { CustomerAuthContext }\n\nasync function assertSessionStillActive(sessionId: string): Promise<boolean> {\n try {\n const
|
|
5
|
-
"mappings": "AAOA,SAAS,eAAe;AACxB,SAAS,yBAAyB;AAClC,SAAS,
|
|
4
|
+
"sourcesContent": ["/**\n * Server-side customer auth helpers for Next.js server components.\n *\n * Uses the `cookies()` API from `next/headers` to read the customer auth token\n * from cookies \u2014 analogous to `getAuthFromCookies()` for staff auth.\n */\n\nimport { cookies } from 'next/headers'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { verifyAudienceJwt } from '@open-mercato/shared/lib/auth/jwt'\nimport { CUSTOMER_JWT_AUDIENCE, CustomerSessionService } from '@open-mercato/core/modules/customer_accounts/services/customerSessionService'\nimport { DomainMappingService } from '@open-mercato/core/modules/customer_accounts/services/domainMappingService'\nimport { tryNormalizeHostname } from '@open-mercato/core/modules/customer_accounts/lib/hostname'\nimport { platformDomains } from '@open-mercato/core/modules/customer_accounts/lib/platformDomains'\nimport { validateUserState } from './customerAuth'\nimport type { CustomerAuthContext } from './customerAuth'\n\nexport type { CustomerAuthContext }\n\nasync function assertSessionStillActive(sessionId: string): Promise<boolean> {\n try {\n const container = await createRequestContainer()\n const service = container.resolve('customerSessionService') as InstanceType<typeof CustomerSessionService>\n const session = await service.findActiveSessionById(sessionId)\n return session !== null\n } catch {\n return false\n }\n}\n\n/**\n * Read and verify customer auth from cookies in server components.\n *\n * Returns the customer auth context if a valid customer JWT is found,\n * or null if not authenticated.\n *\n * @example\n * ```tsx\n * // In a server component or catch-all route\n * import { getCustomerAuthFromCookies } from '@open-mercato/core/modules/customer_accounts/lib/customerAuthServer'\n *\n * const customerAuth = await getCustomerAuthFromCookies()\n * if (!customerAuth) redirect('/login')\n * ```\n */\nexport async function getCustomerAuthFromCookies(\n options?: { expectedTenantId?: string },\n): Promise<CustomerAuthContext | null> {\n const cookieStore = await cookies()\n const token = cookieStore.get('customer_auth_token')?.value\n if (!token) return null\n\n try {\n const payload = verifyAudienceJwt(CUSTOMER_JWT_AUDIENCE, token) as Record<string, unknown> | null\n if (!payload) return null\n if (payload.type !== 'customer') return null\n const sid = typeof payload.sid === 'string' ? payload.sid : ''\n if (!sid) return null\n const tenantId = String(payload.tenantId)\n if (options?.expectedTenantId && options.expectedTenantId !== tenantId) {\n // Cross-host JWT replay defense. See spec rev 5 Customer Authentication\n // section: the host-resolved tenant is the authoritative scope; mismatched\n // JWTs are rejected as if unauthenticated.\n return null\n }\n const stillActive = await assertSessionStillActive(sid)\n if (!stillActive) return null\n\n const userState = await validateUserState(\n String(payload.sub),\n tenantId,\n String(payload.orgId),\n payload.iat,\n )\n if (!userState.valid) return null\n\n return {\n sub: String(payload.sub),\n sid,\n type: 'customer',\n tenantId,\n orgId: String(payload.orgId),\n email: String(payload.email || ''),\n displayName: String(payload.displayName || ''),\n customerEntityId: payload.customerEntityId ? String(payload.customerEntityId) : null,\n personEntityId: payload.personEntityId ? String(payload.personEntityId) : null,\n resolvedFeatures: userState.resolvedFeatures,\n }\n } catch {\n // Invalid or expired JWT \u2014 treat as unauthenticated\n return null\n }\n}\n\n/**\n * Convenience wrapper for custom-domain pages: resolves the host, calls\n * getCustomerAuthFromCookies with the host-resolved tenant as expectedTenantId.\n *\n * Use from server components rendered under custom domains.\n */\nexport async function getCustomerAuthForHost(\n host: string | null | undefined,\n): Promise<CustomerAuthContext | null> {\n if (!host) return getCustomerAuthFromCookies()\n\n try {\n const hostname = tryNormalizeHostname(host)\n if (!hostname) return getCustomerAuthFromCookies()\n if (platformDomains().includes(hostname)) return getCustomerAuthFromCookies()\n\n const container = await createRequestContainer()\n const service = container.resolve('domainMappingService') as InstanceType<typeof DomainMappingService>\n const resolved = await service.resolveByHostname(hostname)\n if (!resolved || resolved.status !== 'active') return null\n return getCustomerAuthFromCookies({ expectedTenantId: resolved.tenantId })\n } catch (err) {\n console.warn('[customer_accounts] getCustomerAuthForHost: domain resolve failed; falling back to platform cookie', err)\n return getCustomerAuthFromCookies()\n }\n}\n"],
|
|
5
|
+
"mappings": "AAOA,SAAS,eAAe;AACxB,SAAS,8BAA8B;AACvC,SAAS,yBAAyB;AAClC,SAAS,6BAAqD;AAE9D,SAAS,4BAA4B;AACrC,SAAS,uBAAuB;AAChC,SAAS,yBAAyB;AAKlC,eAAe,yBAAyB,WAAqC;AAC3E,MAAI;AACF,UAAM,YAAY,MAAM,uBAAuB;AAC/C,UAAM,UAAU,UAAU,QAAQ,wBAAwB;AAC1D,UAAM,UAAU,MAAM,QAAQ,sBAAsB,SAAS;AAC7D,WAAO,YAAY;AAAA,EACrB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAiBA,eAAsB,2BACpB,SACqC;AACrC,QAAM,cAAc,MAAM,QAAQ;AAClC,QAAM,QAAQ,YAAY,IAAI,qBAAqB,GAAG;AACtD,MAAI,CAAC,MAAO,QAAO;AAEnB,MAAI;AACF,UAAM,UAAU,kBAAkB,uBAAuB,KAAK;AAC9D,QAAI,CAAC,QAAS,QAAO;AACrB,QAAI,QAAQ,SAAS,WAAY,QAAO;AACxC,UAAM,MAAM,OAAO,QAAQ,QAAQ,WAAW,QAAQ,MAAM;AAC5D,QAAI,CAAC,IAAK,QAAO;AACjB,UAAM,WAAW,OAAO,QAAQ,QAAQ;AACxC,QAAI,SAAS,oBAAoB,QAAQ,qBAAqB,UAAU;AAItE,aAAO;AAAA,IACT;AACA,UAAM,cAAc,MAAM,yBAAyB,GAAG;AACtD,QAAI,CAAC,YAAa,QAAO;AAEzB,UAAM,YAAY,MAAM;AAAA,MACtB,OAAO,QAAQ,GAAG;AAAA,MAClB;AAAA,MACA,OAAO,QAAQ,KAAK;AAAA,MACpB,QAAQ;AAAA,IACV;AACA,QAAI,CAAC,UAAU,MAAO,QAAO;AAE7B,WAAO;AAAA,MACL,KAAK,OAAO,QAAQ,GAAG;AAAA,MACvB;AAAA,MACA,MAAM;AAAA,MACN;AAAA,MACA,OAAO,OAAO,QAAQ,KAAK;AAAA,MAC3B,OAAO,OAAO,QAAQ,SAAS,EAAE;AAAA,MACjC,aAAa,OAAO,QAAQ,eAAe,EAAE;AAAA,MAC7C,kBAAkB,QAAQ,mBAAmB,OAAO,QAAQ,gBAAgB,IAAI;AAAA,MAChF,gBAAgB,QAAQ,iBAAiB,OAAO,QAAQ,cAAc,IAAI;AAAA,MAC1E,kBAAkB,UAAU;AAAA,IAC9B;AAAA,EACF,QAAQ;AAEN,WAAO;AAAA,EACT;AACF;AAQA,eAAsB,uBACpB,MACqC;AACrC,MAAI,CAAC,KAAM,QAAO,2BAA2B;AAE7C,MAAI;AACF,UAAM,WAAW,qBAAqB,IAAI;AAC1C,QAAI,CAAC,SAAU,QAAO,2BAA2B;AACjD,QAAI,gBAAgB,EAAE,SAAS,QAAQ,EAAG,QAAO,2BAA2B;AAE5E,UAAM,YAAY,MAAM,uBAAuB;AAC/C,UAAM,UAAU,UAAU,QAAQ,sBAAsB;AACxD,UAAM,WAAW,MAAM,QAAQ,kBAAkB,QAAQ;AACzD,QAAI,CAAC,YAAY,SAAS,WAAW,SAAU,QAAO;AACtD,WAAO,2BAA2B,EAAE,kBAAkB,SAAS,SAAS,CAAC;AAAA,EAC3E,SAAS,KAAK;AACZ,YAAQ,KAAK,sGAAsG,GAAG;AACtH,WAAO,2BAA2B;AAAA,EACpC;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
2
|
+
function platformBaseUrl() {
|
|
3
|
+
const fromEnv = process.env.PLATFORM_PORTAL_BASE_URL?.trim();
|
|
4
|
+
if (fromEnv && fromEnv.length > 0) return fromEnv.replace(/\/+$/, "");
|
|
5
|
+
if (process.env.NODE_ENV === "production") {
|
|
6
|
+
throw new Error(
|
|
7
|
+
"PLATFORM_PORTAL_BASE_URL is required in production. Configure it before sending customer-portal emails."
|
|
8
|
+
);
|
|
9
|
+
}
|
|
10
|
+
return "http://localhost:3000";
|
|
11
|
+
}
|
|
12
|
+
function resolveContainer(container) {
|
|
13
|
+
if (container) return Promise.resolve(container);
|
|
14
|
+
return createRequestContainer();
|
|
15
|
+
}
|
|
16
|
+
async function urlForCustomerOrg(orgId, path, options) {
|
|
17
|
+
const safePath = path.startsWith("/") ? path : `/${path}`;
|
|
18
|
+
const container = await resolveContainer(options?.container);
|
|
19
|
+
let active = null;
|
|
20
|
+
try {
|
|
21
|
+
const service = container.resolve("domainMappingService");
|
|
22
|
+
if (service && typeof service.resolveActiveByOrg === "function") {
|
|
23
|
+
active = await service.resolveActiveByOrg(orgId);
|
|
24
|
+
}
|
|
25
|
+
} catch {
|
|
26
|
+
active = null;
|
|
27
|
+
}
|
|
28
|
+
if (active && active.hostname) {
|
|
29
|
+
return `https://${active.hostname}${safePath}`;
|
|
30
|
+
}
|
|
31
|
+
let orgSlug = null;
|
|
32
|
+
try {
|
|
33
|
+
const orgService = container.resolve("orgService");
|
|
34
|
+
if (orgService && typeof orgService.findById === "function") {
|
|
35
|
+
const org = await orgService.findById(orgId);
|
|
36
|
+
orgSlug = org?.slug ?? null;
|
|
37
|
+
}
|
|
38
|
+
} catch {
|
|
39
|
+
orgSlug = null;
|
|
40
|
+
}
|
|
41
|
+
const base = platformBaseUrl();
|
|
42
|
+
if (orgSlug) return `${base}/${orgSlug}/portal${safePath}`;
|
|
43
|
+
return `${base}${safePath}`;
|
|
44
|
+
}
|
|
45
|
+
export {
|
|
46
|
+
urlForCustomerOrg
|
|
47
|
+
};
|
|
48
|
+
//# sourceMappingURL=customerUrl.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/customer_accounts/lib/customerUrl.ts"],
|
|
4
|
+
"sourcesContent": ["// Build customer-portal URLs that honor the active custom domain when present.\n// All customer-facing email senders (signup welcome, magic link, password reset,\n// notification digests) MUST construct links via this helper instead of\n// hard-coding PLATFORM_PORTAL_BASE_URL \u2014 see customer_accounts/AGENTS.md.\n\nimport type { AppContainer } from '@open-mercato/shared/lib/di/container'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\n\ntype DomainMappingService = {\n resolveActiveByOrg(orgId: string): Promise<{ hostname: string } | null>\n}\n\ntype OrgService = {\n findById(orgId: string): Promise<{ id: string; slug: string | null } | null>\n}\n\nfunction platformBaseUrl(): string {\n const fromEnv = process.env.PLATFORM_PORTAL_BASE_URL?.trim()\n if (fromEnv && fromEnv.length > 0) return fromEnv.replace(/\\/+$/, '')\n // Outbound emails (welcome, magic link, password reset) embed this URL \u2014\n // localhost in production silently sends broken links to real customers.\n if (process.env.NODE_ENV === 'production') {\n throw new Error(\n 'PLATFORM_PORTAL_BASE_URL is required in production. Configure it before sending customer-portal emails.',\n )\n }\n return 'http://localhost:3000'\n}\n\nfunction resolveContainer(container?: AppContainer): Promise<AppContainer> {\n if (container) return Promise.resolve(container)\n return createRequestContainer()\n}\n\nexport async function urlForCustomerOrg(\n orgId: string,\n path: string,\n options?: { container?: AppContainer },\n): Promise<string> {\n const safePath = path.startsWith('/') ? path : `/${path}`\n const container = await resolveContainer(options?.container)\n\n let active: { hostname: string } | null = null\n try {\n const service = container.resolve('domainMappingService') as DomainMappingService\n if (service && typeof service.resolveActiveByOrg === 'function') {\n active = await service.resolveActiveByOrg(orgId)\n }\n } catch {\n // domainMappingService not registered yet (fresh installs, tests) \u2014 fall through to platform URL.\n active = null\n }\n\n if (active && active.hostname) {\n return `https://${active.hostname}${safePath}`\n }\n\n let orgSlug: string | null = null\n try {\n const orgService = container.resolve('orgService') as OrgService | undefined\n if (orgService && typeof orgService.findById === 'function') {\n const org = await orgService.findById(orgId)\n orgSlug = org?.slug ?? null\n }\n } catch {\n orgSlug = null\n }\n\n const base = platformBaseUrl()\n if (orgSlug) return `${base}/${orgSlug}/portal${safePath}`\n return `${base}${safePath}`\n}\n"],
|
|
5
|
+
"mappings": "AAMA,SAAS,8BAA8B;AAUvC,SAAS,kBAA0B;AACjC,QAAM,UAAU,QAAQ,IAAI,0BAA0B,KAAK;AAC3D,MAAI,WAAW,QAAQ,SAAS,EAAG,QAAO,QAAQ,QAAQ,QAAQ,EAAE;AAGpE,MAAI,QAAQ,IAAI,aAAa,cAAc;AACzC,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,iBAAiB,WAAiD;AACzE,MAAI,UAAW,QAAO,QAAQ,QAAQ,SAAS;AAC/C,SAAO,uBAAuB;AAChC;AAEA,eAAsB,kBACpB,OACA,MACA,SACiB;AACjB,QAAM,WAAW,KAAK,WAAW,GAAG,IAAI,OAAO,IAAI,IAAI;AACvD,QAAM,YAAY,MAAM,iBAAiB,SAAS,SAAS;AAE3D,MAAI,SAAsC;AAC1C,MAAI;AACF,UAAM,UAAU,UAAU,QAAQ,sBAAsB;AACxD,QAAI,WAAW,OAAO,QAAQ,uBAAuB,YAAY;AAC/D,eAAS,MAAM,QAAQ,mBAAmB,KAAK;AAAA,IACjD;AAAA,EACF,QAAQ;AAEN,aAAS;AAAA,EACX;AAEA,MAAI,UAAU,OAAO,UAAU;AAC7B,WAAO,WAAW,OAAO,QAAQ,GAAG,QAAQ;AAAA,EAC9C;AAEA,MAAI,UAAyB;AAC7B,MAAI;AACF,UAAM,aAAa,UAAU,QAAQ,YAAY;AACjD,QAAI,cAAc,OAAO,WAAW,aAAa,YAAY;AAC3D,YAAM,MAAM,MAAM,WAAW,SAAS,KAAK;AAC3C,gBAAU,KAAK,QAAQ;AAAA,IACzB;AAAA,EACF,QAAQ;AACN,cAAU;AAAA,EACZ;AAEA,QAAM,OAAO,gBAAgB;AAC7B,MAAI,QAAS,QAAO,GAAG,IAAI,IAAI,OAAO,UAAU,QAAQ;AACxD,SAAO,GAAG,IAAI,GAAG,QAAQ;AAC3B;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const MAX_HOSTNAME_LENGTH = 253;
|
|
2
|
+
const HOSTNAME_LABEL = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
3
|
+
class HostnameNormalizationError extends Error {
|
|
4
|
+
constructor(message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "HostnameNormalizationError";
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
function normalizeHostname(input) {
|
|
10
|
+
if (typeof input !== "string") {
|
|
11
|
+
throw new HostnameNormalizationError("Hostname must be a string");
|
|
12
|
+
}
|
|
13
|
+
let host = input.trim().toLowerCase();
|
|
14
|
+
if (!host) throw new HostnameNormalizationError("Hostname is empty");
|
|
15
|
+
host = host.replace(/^https?:\/\//, "").replace(/\/.*$/, "").replace(/\?.*$/, "").replace(/#.*$/, "").replace(/:\d+$/, "");
|
|
16
|
+
if (host.endsWith(".")) host = host.slice(0, -1);
|
|
17
|
+
if (!host) throw new HostnameNormalizationError("Hostname is empty after stripping");
|
|
18
|
+
try {
|
|
19
|
+
const url = new URL(`https://${host}`);
|
|
20
|
+
host = url.hostname;
|
|
21
|
+
} catch {
|
|
22
|
+
throw new HostnameNormalizationError(`Hostname is not a valid host: ${input}`);
|
|
23
|
+
}
|
|
24
|
+
if (!host || host.length > MAX_HOSTNAME_LENGTH) {
|
|
25
|
+
throw new HostnameNormalizationError(
|
|
26
|
+
`Hostname must be between 1 and ${MAX_HOSTNAME_LENGTH} characters after normalization`
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
const labels = host.split(".");
|
|
30
|
+
if (labels.length < 2) {
|
|
31
|
+
throw new HostnameNormalizationError('Hostname must have at least two labels (e.g. "example.com")');
|
|
32
|
+
}
|
|
33
|
+
for (const label of labels) {
|
|
34
|
+
if (!HOSTNAME_LABEL.test(label)) {
|
|
35
|
+
throw new HostnameNormalizationError(`Invalid DNS label: "${label}"`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (/^\d+$/.test(labels[labels.length - 1])) {
|
|
39
|
+
throw new HostnameNormalizationError(`Hostname looks like an IP address, not a DNS name: ${input}`);
|
|
40
|
+
}
|
|
41
|
+
return host;
|
|
42
|
+
}
|
|
43
|
+
function tryNormalizeHostname(input) {
|
|
44
|
+
try {
|
|
45
|
+
return normalizeHostname(input);
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export {
|
|
51
|
+
HostnameNormalizationError,
|
|
52
|
+
normalizeHostname,
|
|
53
|
+
tryNormalizeHostname
|
|
54
|
+
};
|
|
55
|
+
//# sourceMappingURL=hostname.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/customer_accounts/lib/hostname.ts"],
|
|
4
|
+
"sourcesContent": ["// Hostname normalization for the custom-domain routing feature.\n// Single canonical form (lowercase, no trailing dot, IDN\u2192Punycode) is essential \u2014\n// the UNIQUE constraint on `domain_mappings.hostname` is meaningless without it.\n\nconst MAX_HOSTNAME_LENGTH = 253\nconst HOSTNAME_LABEL = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/\n\nexport class HostnameNormalizationError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'HostnameNormalizationError'\n }\n}\n\nexport function normalizeHostname(input: string): string {\n if (typeof input !== 'string') {\n throw new HostnameNormalizationError('Hostname must be a string')\n }\n\n let host = input.trim().toLowerCase()\n if (!host) throw new HostnameNormalizationError('Hostname is empty')\n\n host = host\n .replace(/^https?:\\/\\//, '')\n .replace(/\\/.*$/, '')\n .replace(/\\?.*$/, '')\n .replace(/#.*$/, '')\n .replace(/:\\d+$/, '')\n\n if (host.endsWith('.')) host = host.slice(0, -1)\n\n if (!host) throw new HostnameNormalizationError('Hostname is empty after stripping')\n\n // Convert IDN (Unicode) to Punycode (ASCII) via the URL constructor \u2014 this\n // matches what browsers and Let's Encrypt use, without depending on the\n // deprecated `node:punycode` module or pulling in `tr46`.\n try {\n const url = new URL(`https://${host}`)\n host = url.hostname\n } catch {\n throw new HostnameNormalizationError(`Hostname is not a valid host: ${input}`)\n }\n\n if (!host || host.length > MAX_HOSTNAME_LENGTH) {\n throw new HostnameNormalizationError(\n `Hostname must be between 1 and ${MAX_HOSTNAME_LENGTH} characters after normalization`,\n )\n }\n\n const labels = host.split('.')\n if (labels.length < 2) {\n throw new HostnameNormalizationError('Hostname must have at least two labels (e.g. \"example.com\")')\n }\n for (const label of labels) {\n if (!HOSTNAME_LABEL.test(label)) {\n throw new HostnameNormalizationError(`Invalid DNS label: \"${label}\"`)\n }\n }\n // Reject IP-address literals (e.g. \"127.0.0.1\"): a custom domain must be a\n // registered DNS name, and a TLD is never all-numeric. This also keeps\n // loopback/test hosts (127.0.0.1) from being mistaken for a tenant's\n // branded domain by the routing layer.\n if (/^\\d+$/.test(labels[labels.length - 1])) {\n throw new HostnameNormalizationError(`Hostname looks like an IP address, not a DNS name: ${input}`)\n }\n\n return host\n}\n\nexport function tryNormalizeHostname(input: string): string | null {\n try {\n return normalizeHostname(input)\n } catch {\n return null\n }\n}\n"],
|
|
5
|
+
"mappings": "AAIA,MAAM,sBAAsB;AAC5B,MAAM,iBAAiB;AAEhB,MAAM,mCAAmC,MAAM;AAAA,EACpD,YAAY,SAAiB;AAC3B,UAAM,OAAO;AACb,SAAK,OAAO;AAAA,EACd;AACF;AAEO,SAAS,kBAAkB,OAAuB;AACvD,MAAI,OAAO,UAAU,UAAU;AAC7B,UAAM,IAAI,2BAA2B,2BAA2B;AAAA,EAClE;AAEA,MAAI,OAAO,MAAM,KAAK,EAAE,YAAY;AACpC,MAAI,CAAC,KAAM,OAAM,IAAI,2BAA2B,mBAAmB;AAEnE,SAAO,KACJ,QAAQ,gBAAgB,EAAE,EAC1B,QAAQ,SAAS,EAAE,EACnB,QAAQ,SAAS,EAAE,EACnB,QAAQ,QAAQ,EAAE,EAClB,QAAQ,SAAS,EAAE;AAEtB,MAAI,KAAK,SAAS,GAAG,EAAG,QAAO,KAAK,MAAM,GAAG,EAAE;AAE/C,MAAI,CAAC,KAAM,OAAM,IAAI,2BAA2B,mCAAmC;AAKnF,MAAI;AACF,UAAM,MAAM,IAAI,IAAI,WAAW,IAAI,EAAE;AACrC,WAAO,IAAI;AAAA,EACb,QAAQ;AACN,UAAM,IAAI,2BAA2B,iCAAiC,KAAK,EAAE;AAAA,EAC/E;AAEA,MAAI,CAAC,QAAQ,KAAK,SAAS,qBAAqB;AAC9C,UAAM,IAAI;AAAA,MACR,kCAAkC,mBAAmB;AAAA,IACvD;AAAA,EACF;AAEA,QAAM,SAAS,KAAK,MAAM,GAAG;AAC7B,MAAI,OAAO,SAAS,GAAG;AACrB,UAAM,IAAI,2BAA2B,6DAA6D;AAAA,EACpG;AACA,aAAW,SAAS,QAAQ;AAC1B,QAAI,CAAC,eAAe,KAAK,KAAK,GAAG;AAC/B,YAAM,IAAI,2BAA2B,uBAAuB,KAAK,GAAG;AAAA,IACtE;AAAA,EACF;AAKA,MAAI,QAAQ,KAAK,OAAO,OAAO,SAAS,CAAC,CAAC,GAAG;AAC3C,UAAM,IAAI,2BAA2B,sDAAsD,KAAK,EAAE;AAAA,EACpG;AAEA,SAAO;AACT;AAEO,SAAS,qBAAqB,OAA8B;AACjE,MAAI;AACF,WAAO,kBAAkB,KAAK;AAAA,EAChC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/customer_accounts/lib/platformDomains.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Parse the `PLATFORM_DOMAINS` env into a normalized list of platform hostnames.\n *\n * Platform hosts are the deployment's own domains (admin app, marketing site,\n * loopback). Custom-domain routing MUST treat platform hosts as non-tenant\n * traffic, so domain registration, resolver lookups, and tenant-context\n * fallbacks all share the same view of which hosts are platform-owned.\n */\nexport function platformDomains(): string[] {\n return (process.env.PLATFORM_DOMAINS ?? 'localhost,openmercato.com')\n .split(',')\n .map((s) => s.trim().toLowerCase())\n .filter(Boolean)\n}\n"],
|
|
5
|
+
"mappings": "AAQO,SAAS,kBAA4B;AAC1C,UAAQ,QAAQ,IAAI,oBAAoB,6BACrC,MAAM,GAAG,EACT,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE,YAAY,CAAC,EACjC,OAAO,OAAO;AACnB;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
const DEFAULT_CLOUDFLARE_RANGES = [
|
|
2
|
+
"173.245.48.0/20",
|
|
3
|
+
"103.21.244.0/22",
|
|
4
|
+
"103.22.200.0/22",
|
|
5
|
+
"103.31.4.0/22",
|
|
6
|
+
"141.101.64.0/18",
|
|
7
|
+
"108.162.192.0/18",
|
|
8
|
+
"190.93.240.0/20",
|
|
9
|
+
"188.114.96.0/20",
|
|
10
|
+
"197.234.240.0/22",
|
|
11
|
+
"198.41.128.0/17",
|
|
12
|
+
"162.158.0.0/15",
|
|
13
|
+
"104.16.0.0/13",
|
|
14
|
+
"104.24.0.0/14",
|
|
15
|
+
"172.64.0.0/13",
|
|
16
|
+
"131.0.72.0/22"
|
|
17
|
+
];
|
|
18
|
+
let cachedRanges = null;
|
|
19
|
+
function parseIpv4(ip) {
|
|
20
|
+
const parts = ip.split(".");
|
|
21
|
+
if (parts.length !== 4) return null;
|
|
22
|
+
let result = 0;
|
|
23
|
+
for (const part of parts) {
|
|
24
|
+
if (!/^\d+$/.test(part)) return null;
|
|
25
|
+
const value = Number(part);
|
|
26
|
+
if (value < 0 || value > 255) return null;
|
|
27
|
+
result = result << 8 | value;
|
|
28
|
+
}
|
|
29
|
+
return result >>> 0;
|
|
30
|
+
}
|
|
31
|
+
function parseCidr(cidr) {
|
|
32
|
+
const [ip, prefixStr] = cidr.split("/");
|
|
33
|
+
if (!ip || !prefixStr) return null;
|
|
34
|
+
const prefix = Number(prefixStr);
|
|
35
|
+
if (!Number.isFinite(prefix) || prefix < 0 || prefix > 32) return null;
|
|
36
|
+
const ipNum = parseIpv4(ip);
|
|
37
|
+
if (ipNum === null) return null;
|
|
38
|
+
const mask = prefix === 0 ? 0 : 4294967295 << 32 - prefix >>> 0;
|
|
39
|
+
const network = (ipNum & mask) >>> 0;
|
|
40
|
+
return { network, mask, raw: cidr };
|
|
41
|
+
}
|
|
42
|
+
function loadRanges() {
|
|
43
|
+
if (cachedRanges) return cachedRanges;
|
|
44
|
+
const fromEnv = (process.env.KNOWN_PROXY_IP_RANGES ?? "").trim();
|
|
45
|
+
const list = fromEnv ? fromEnv.split(",").map((s) => s.trim()).filter(Boolean) : DEFAULT_CLOUDFLARE_RANGES;
|
|
46
|
+
cachedRanges = list.map(parseCidr).filter((v) => v !== null);
|
|
47
|
+
return cachedRanges;
|
|
48
|
+
}
|
|
49
|
+
function isInKnownProxyRange(ip) {
|
|
50
|
+
const ipNum = parseIpv4(ip);
|
|
51
|
+
if (ipNum === null) return false;
|
|
52
|
+
for (const range of loadRanges()) {
|
|
53
|
+
if ((ipNum & range.mask) >>> 0 === range.network) return true;
|
|
54
|
+
}
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
function detectProxy(ip) {
|
|
58
|
+
if (!isInKnownProxyRange(ip)) return null;
|
|
59
|
+
const fromEnv = process.env.KNOWN_PROXY_IP_RANGES;
|
|
60
|
+
if (!fromEnv) return "cloudflare";
|
|
61
|
+
return "unknown";
|
|
62
|
+
}
|
|
63
|
+
function resetProxyRangeCacheForTests() {
|
|
64
|
+
cachedRanges = null;
|
|
65
|
+
}
|
|
66
|
+
export {
|
|
67
|
+
detectProxy,
|
|
68
|
+
isInKnownProxyRange,
|
|
69
|
+
resetProxyRangeCacheForTests
|
|
70
|
+
};
|
|
71
|
+
//# sourceMappingURL=proxyRanges.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/customer_accounts/lib/proxyRanges.ts"],
|
|
4
|
+
"sourcesContent": ["// CIDR matching for known reverse-proxy IP ranges (Cloudflare, Fastly, custom).\n// Used by the DNS verification fallback: if a domain's A record lands inside a\n// known proxy range we fall back to a reverse-resolve over HTTPS, because the\n// proxy hides the real origin from us.\n\nconst DEFAULT_CLOUDFLARE_RANGES = [\n '173.245.48.0/20',\n '103.21.244.0/22',\n '103.22.200.0/22',\n '103.31.4.0/22',\n '141.101.64.0/18',\n '108.162.192.0/18',\n '190.93.240.0/20',\n '188.114.96.0/20',\n '197.234.240.0/22',\n '198.41.128.0/17',\n '162.158.0.0/15',\n '104.16.0.0/13',\n '104.24.0.0/14',\n '172.64.0.0/13',\n '131.0.72.0/22',\n]\n\ntype ParsedCidr = { network: number; mask: number; raw: string }\n\nlet cachedRanges: ParsedCidr[] | null = null\n\nfunction parseIpv4(ip: string): number | null {\n const parts = ip.split('.')\n if (parts.length !== 4) return null\n let result = 0\n for (const part of parts) {\n if (!/^\\d+$/.test(part)) return null\n const value = Number(part)\n if (value < 0 || value > 255) return null\n result = (result << 8) | value\n }\n // Force unsigned 32-bit\n return result >>> 0\n}\n\nfunction parseCidr(cidr: string): ParsedCidr | null {\n const [ip, prefixStr] = cidr.split('/')\n if (!ip || !prefixStr) return null\n const prefix = Number(prefixStr)\n if (!Number.isFinite(prefix) || prefix < 0 || prefix > 32) return null\n const ipNum = parseIpv4(ip)\n if (ipNum === null) return null\n const mask = prefix === 0 ? 0 : (0xffffffff << (32 - prefix)) >>> 0\n const network = (ipNum & mask) >>> 0\n return { network, mask, raw: cidr }\n}\n\nfunction loadRanges(): ParsedCidr[] {\n if (cachedRanges) return cachedRanges\n const fromEnv = (process.env.KNOWN_PROXY_IP_RANGES ?? '').trim()\n const list = fromEnv\n ? fromEnv.split(',').map((s) => s.trim()).filter(Boolean)\n : DEFAULT_CLOUDFLARE_RANGES\n cachedRanges = list.map(parseCidr).filter((v): v is ParsedCidr => v !== null)\n return cachedRanges\n}\n\nexport function isInKnownProxyRange(ip: string): boolean {\n const ipNum = parseIpv4(ip)\n if (ipNum === null) return false\n for (const range of loadRanges()) {\n if (((ipNum & range.mask) >>> 0) === range.network) return true\n }\n return false\n}\n\nexport function detectProxy(ip: string): string | null {\n if (!isInKnownProxyRange(ip)) return null\n // For now we only ship Cloudflare's published ranges by default. Operators\n // using a different proxy can override KNOWN_PROXY_IP_RANGES; we still\n // return 'unknown' for those so callers can phrase the diagnostic correctly.\n const fromEnv = process.env.KNOWN_PROXY_IP_RANGES\n if (!fromEnv) return 'cloudflare'\n return 'unknown'\n}\n\nexport function resetProxyRangeCacheForTests(): void {\n cachedRanges = null\n}\n"],
|
|
5
|
+
"mappings": "AAKA,MAAM,4BAA4B;AAAA,EAChC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAIA,IAAI,eAAoC;AAExC,SAAS,UAAU,IAA2B;AAC5C,QAAM,QAAQ,GAAG,MAAM,GAAG;AAC1B,MAAI,MAAM,WAAW,EAAG,QAAO;AAC/B,MAAI,SAAS;AACb,aAAW,QAAQ,OAAO;AACxB,QAAI,CAAC,QAAQ,KAAK,IAAI,EAAG,QAAO;AAChC,UAAM,QAAQ,OAAO,IAAI;AACzB,QAAI,QAAQ,KAAK,QAAQ,IAAK,QAAO;AACrC,aAAU,UAAU,IAAK;AAAA,EAC3B;AAEA,SAAO,WAAW;AACpB;AAEA,SAAS,UAAU,MAAiC;AAClD,QAAM,CAAC,IAAI,SAAS,IAAI,KAAK,MAAM,GAAG;AACtC,MAAI,CAAC,MAAM,CAAC,UAAW,QAAO;AAC9B,QAAM,SAAS,OAAO,SAAS;AAC/B,MAAI,CAAC,OAAO,SAAS,MAAM,KAAK,SAAS,KAAK,SAAS,GAAI,QAAO;AAClE,QAAM,QAAQ,UAAU,EAAE;AAC1B,MAAI,UAAU,KAAM,QAAO;AAC3B,QAAM,OAAO,WAAW,IAAI,IAAK,cAAe,KAAK,WAAa;AAClE,QAAM,WAAW,QAAQ,UAAU;AACnC,SAAO,EAAE,SAAS,MAAM,KAAK,KAAK;AACpC;AAEA,SAAS,aAA2B;AAClC,MAAI,aAAc,QAAO;AACzB,QAAM,WAAW,QAAQ,IAAI,yBAAyB,IAAI,KAAK;AAC/D,QAAM,OAAO,UACT,QAAQ,MAAM,GAAG,EAAE,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,OAAO,IACtD;AACJ,iBAAe,KAAK,IAAI,SAAS,EAAE,OAAO,CAAC,MAAuB,MAAM,IAAI;AAC5E,SAAO;AACT;AAEO,SAAS,oBAAoB,IAAqB;AACvD,QAAM,QAAQ,UAAU,EAAE;AAC1B,MAAI,UAAU,KAAM,QAAO;AAC3B,aAAW,SAAS,WAAW,GAAG;AAChC,SAAM,QAAQ,MAAM,UAAU,MAAO,MAAM,QAAS,QAAO;AAAA,EAC7D;AACA,SAAO;AACT;AAEO,SAAS,YAAY,IAA2B;AACrD,MAAI,CAAC,oBAAoB,EAAE,EAAG,QAAO;AAIrC,QAAM,UAAU,QAAQ,IAAI;AAC5B,MAAI,CAAC,QAAS,QAAO;AACrB,SAAO;AACT;AAEO,SAAS,+BAAqC;AACnD,iBAAe;AACjB;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -48,6 +48,12 @@ const customerMagicLinkIpRateLimitConfig = readEndpointRateLimitConfig("CUSTOMER
|
|
|
48
48
|
blockDuration: 120,
|
|
49
49
|
keyPrefix: "customer-magic-link-ip"
|
|
50
50
|
});
|
|
51
|
+
const domainResolveAllIpRateLimitConfig = readEndpointRateLimitConfig("DOMAIN_RESOLVE_ALL_IP", {
|
|
52
|
+
points: 30,
|
|
53
|
+
duration: 60,
|
|
54
|
+
blockDuration: 60,
|
|
55
|
+
keyPrefix: "domain-resolve-all"
|
|
56
|
+
});
|
|
51
57
|
export {
|
|
52
58
|
checkAuthRateLimit,
|
|
53
59
|
customerLoginIpRateLimitConfig,
|
|
@@ -58,6 +64,7 @@ export {
|
|
|
58
64
|
customerPasswordResetRateLimitConfig,
|
|
59
65
|
customerSignupIpRateLimitConfig,
|
|
60
66
|
customerSignupRateLimitConfig,
|
|
67
|
+
domainResolveAllIpRateLimitConfig,
|
|
61
68
|
resetAuthRateLimit
|
|
62
69
|
};
|
|
63
70
|
//# sourceMappingURL=rateLimiter.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/customer_accounts/lib/rateLimiter.ts"],
|
|
4
|
-
"sourcesContent": ["import { checkAuthRateLimit, resetAuthRateLimit } from '@open-mercato/core/modules/auth/lib/rateLimitCheck'\nimport { readEndpointRateLimitConfig } from '@open-mercato/shared/lib/ratelimit/config'\n\nexport const customerLoginRateLimitConfig = readEndpointRateLimitConfig('CUSTOMER_LOGIN', {\n points: 5, duration: 60, blockDuration: 60, keyPrefix: 'customer-login',\n})\n\nexport const customerLoginIpRateLimitConfig = readEndpointRateLimitConfig('CUSTOMER_LOGIN_IP', {\n points: 20, duration: 60, blockDuration: 60, keyPrefix: 'customer-login-ip',\n})\n\nexport const customerSignupRateLimitConfig = readEndpointRateLimitConfig('CUSTOMER_SIGNUP', {\n points: 3, duration: 60, blockDuration: 120, keyPrefix: 'customer-signup',\n})\n\nexport const customerSignupIpRateLimitConfig = readEndpointRateLimitConfig('CUSTOMER_SIGNUP_IP', {\n points: 10, duration: 60, blockDuration: 120, keyPrefix: 'customer-signup-ip',\n})\n\nexport const customerPasswordResetRateLimitConfig = readEndpointRateLimitConfig('CUSTOMER_PASSWORD_RESET', {\n points: 3, duration: 60, blockDuration: 120, keyPrefix: 'customer-password-reset',\n})\n\nexport const customerPasswordResetIpRateLimitConfig = readEndpointRateLimitConfig('CUSTOMER_PASSWORD_RESET_IP', {\n points: 10, duration: 60, blockDuration: 120, keyPrefix: 'customer-password-reset-ip',\n})\n\nexport const customerMagicLinkRateLimitConfig = readEndpointRateLimitConfig('CUSTOMER_MAGIC_LINK', {\n points: 3, duration: 60, blockDuration: 120, keyPrefix: 'customer-magic-link',\n})\n\nexport const customerMagicLinkIpRateLimitConfig = readEndpointRateLimitConfig('CUSTOMER_MAGIC_LINK_IP', {\n points: 10, duration: 60, blockDuration: 120, keyPrefix: 'customer-magic-link-ip',\n})\n\nexport { checkAuthRateLimit, resetAuthRateLimit }\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB,0BAA0B;AACvD,SAAS,mCAAmC;AAErC,MAAM,+BAA+B,4BAA4B,kBAAkB;AAAA,EACxF,QAAQ;AAAA,EAAG,UAAU;AAAA,EAAI,eAAe;AAAA,EAAI,WAAW;AACzD,CAAC;AAEM,MAAM,iCAAiC,4BAA4B,qBAAqB;AAAA,EAC7F,QAAQ;AAAA,EAAI,UAAU;AAAA,EAAI,eAAe;AAAA,EAAI,WAAW;AAC1D,CAAC;AAEM,MAAM,gCAAgC,4BAA4B,mBAAmB;AAAA,EAC1F,QAAQ;AAAA,EAAG,UAAU;AAAA,EAAI,eAAe;AAAA,EAAK,WAAW;AAC1D,CAAC;AAEM,MAAM,kCAAkC,4BAA4B,sBAAsB;AAAA,EAC/F,QAAQ;AAAA,EAAI,UAAU;AAAA,EAAI,eAAe;AAAA,EAAK,WAAW;AAC3D,CAAC;AAEM,MAAM,uCAAuC,4BAA4B,2BAA2B;AAAA,EACzG,QAAQ;AAAA,EAAG,UAAU;AAAA,EAAI,eAAe;AAAA,EAAK,WAAW;AAC1D,CAAC;AAEM,MAAM,yCAAyC,4BAA4B,8BAA8B;AAAA,EAC9G,QAAQ;AAAA,EAAI,UAAU;AAAA,EAAI,eAAe;AAAA,EAAK,WAAW;AAC3D,CAAC;AAEM,MAAM,mCAAmC,4BAA4B,uBAAuB;AAAA,EACjG,QAAQ;AAAA,EAAG,UAAU;AAAA,EAAI,eAAe;AAAA,EAAK,WAAW;AAC1D,CAAC;AAEM,MAAM,qCAAqC,4BAA4B,0BAA0B;AAAA,EACtG,QAAQ;AAAA,EAAI,UAAU;AAAA,EAAI,eAAe;AAAA,EAAK,WAAW;AAC3D,CAAC;",
|
|
4
|
+
"sourcesContent": ["import { checkAuthRateLimit, resetAuthRateLimit } from '@open-mercato/core/modules/auth/lib/rateLimitCheck'\nimport { readEndpointRateLimitConfig } from '@open-mercato/shared/lib/ratelimit/config'\n\nexport const customerLoginRateLimitConfig = readEndpointRateLimitConfig('CUSTOMER_LOGIN', {\n points: 5, duration: 60, blockDuration: 60, keyPrefix: 'customer-login',\n})\n\nexport const customerLoginIpRateLimitConfig = readEndpointRateLimitConfig('CUSTOMER_LOGIN_IP', {\n points: 20, duration: 60, blockDuration: 60, keyPrefix: 'customer-login-ip',\n})\n\nexport const customerSignupRateLimitConfig = readEndpointRateLimitConfig('CUSTOMER_SIGNUP', {\n points: 3, duration: 60, blockDuration: 120, keyPrefix: 'customer-signup',\n})\n\nexport const customerSignupIpRateLimitConfig = readEndpointRateLimitConfig('CUSTOMER_SIGNUP_IP', {\n points: 10, duration: 60, blockDuration: 120, keyPrefix: 'customer-signup-ip',\n})\n\nexport const customerPasswordResetRateLimitConfig = readEndpointRateLimitConfig('CUSTOMER_PASSWORD_RESET', {\n points: 3, duration: 60, blockDuration: 120, keyPrefix: 'customer-password-reset',\n})\n\nexport const customerPasswordResetIpRateLimitConfig = readEndpointRateLimitConfig('CUSTOMER_PASSWORD_RESET_IP', {\n points: 10, duration: 60, blockDuration: 120, keyPrefix: 'customer-password-reset-ip',\n})\n\nexport const customerMagicLinkRateLimitConfig = readEndpointRateLimitConfig('CUSTOMER_MAGIC_LINK', {\n points: 3, duration: 60, blockDuration: 120, keyPrefix: 'customer-magic-link',\n})\n\nexport const customerMagicLinkIpRateLimitConfig = readEndpointRateLimitConfig('CUSTOMER_MAGIC_LINK_IP', {\n points: 10, duration: 60, blockDuration: 120, keyPrefix: 'customer-magic-link-ip',\n})\n\n// Bulk custom-domain warm-up endpoint (`/api/customer_accounts/domain-resolve/all`).\n// Single shared `DOMAIN_RESOLVE_SECRET` gates a payload that lists every active\n// custom-domain mapping in the deployment, so we cap requests per IP to make\n// brute-force or post-leak enumeration noisy. 30 req/min/IP comfortably covers\n// the Node middleware warm-up cadence (one call per process boot) while\n// staying well below useful enumeration throughput.\nexport const domainResolveAllIpRateLimitConfig = readEndpointRateLimitConfig('DOMAIN_RESOLVE_ALL_IP', {\n points: 30, duration: 60, blockDuration: 60, keyPrefix: 'domain-resolve-all',\n})\n\nexport { checkAuthRateLimit, resetAuthRateLimit }\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,oBAAoB,0BAA0B;AACvD,SAAS,mCAAmC;AAErC,MAAM,+BAA+B,4BAA4B,kBAAkB;AAAA,EACxF,QAAQ;AAAA,EAAG,UAAU;AAAA,EAAI,eAAe;AAAA,EAAI,WAAW;AACzD,CAAC;AAEM,MAAM,iCAAiC,4BAA4B,qBAAqB;AAAA,EAC7F,QAAQ;AAAA,EAAI,UAAU;AAAA,EAAI,eAAe;AAAA,EAAI,WAAW;AAC1D,CAAC;AAEM,MAAM,gCAAgC,4BAA4B,mBAAmB;AAAA,EAC1F,QAAQ;AAAA,EAAG,UAAU;AAAA,EAAI,eAAe;AAAA,EAAK,WAAW;AAC1D,CAAC;AAEM,MAAM,kCAAkC,4BAA4B,sBAAsB;AAAA,EAC/F,QAAQ;AAAA,EAAI,UAAU;AAAA,EAAI,eAAe;AAAA,EAAK,WAAW;AAC3D,CAAC;AAEM,MAAM,uCAAuC,4BAA4B,2BAA2B;AAAA,EACzG,QAAQ;AAAA,EAAG,UAAU;AAAA,EAAI,eAAe;AAAA,EAAK,WAAW;AAC1D,CAAC;AAEM,MAAM,yCAAyC,4BAA4B,8BAA8B;AAAA,EAC9G,QAAQ;AAAA,EAAI,UAAU;AAAA,EAAI,eAAe;AAAA,EAAK,WAAW;AAC3D,CAAC;AAEM,MAAM,mCAAmC,4BAA4B,uBAAuB;AAAA,EACjG,QAAQ;AAAA,EAAG,UAAU;AAAA,EAAI,eAAe;AAAA,EAAK,WAAW;AAC1D,CAAC;AAEM,MAAM,qCAAqC,4BAA4B,0BAA0B;AAAA,EACtG,QAAQ;AAAA,EAAI,UAAU;AAAA,EAAI,eAAe;AAAA,EAAK,WAAW;AAC3D,CAAC;AAQM,MAAM,oCAAoC,4BAA4B,yBAAyB;AAAA,EACpG,QAAQ;AAAA,EAAI,UAAU;AAAA,EAAI,eAAe;AAAA,EAAI,WAAW;AAC1D,CAAC;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { tryNormalizeHostname } from "@open-mercato/core/modules/customer_accounts/lib/hostname";
|
|
2
|
+
import { platformDomains } from "@open-mercato/core/modules/customer_accounts/lib/platformDomains";
|
|
3
|
+
import { secretEqual } from "@open-mercato/core/modules/customer_accounts/lib/secretCompare";
|
|
4
|
+
class TenantResolutionError extends Error {
|
|
5
|
+
constructor(message, status) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.status = status;
|
|
8
|
+
this.name = "TenantResolutionError";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function readForcedHost(req) {
|
|
12
|
+
if (process.env.NODE_ENV !== "test") return null;
|
|
13
|
+
const expected = process.env.FORCE_HOST_SECRET;
|
|
14
|
+
if (!expected) return null;
|
|
15
|
+
if (!secretEqual(req.headers.get("x-force-host-secret"), expected)) return null;
|
|
16
|
+
const host = req.headers.get("x-force-host");
|
|
17
|
+
return host && host.length > 0 ? host : null;
|
|
18
|
+
}
|
|
19
|
+
async function resolveTenantContext(req, bodyTenantId, options) {
|
|
20
|
+
const rawHost = readForcedHost(req) ?? req.headers.get("host");
|
|
21
|
+
const hostname = rawHost ? tryNormalizeHostname(rawHost) : null;
|
|
22
|
+
const isPlatform = hostname ? platformDomains().includes(hostname) : true;
|
|
23
|
+
if (isPlatform) {
|
|
24
|
+
if (!bodyTenantId) {
|
|
25
|
+
throw new TenantResolutionError("tenantId is required for platform-domain logins", 400);
|
|
26
|
+
}
|
|
27
|
+
return { source: "body", tenantId: bodyTenantId, organizationId: null, hostname };
|
|
28
|
+
}
|
|
29
|
+
let service = null;
|
|
30
|
+
try {
|
|
31
|
+
if (options?.container) {
|
|
32
|
+
service = options.container.resolve("domainMappingService");
|
|
33
|
+
} else {
|
|
34
|
+
const { createRequestContainer } = await import("@open-mercato/shared/lib/di/container");
|
|
35
|
+
const container = await createRequestContainer();
|
|
36
|
+
service = container.resolve("domainMappingService");
|
|
37
|
+
}
|
|
38
|
+
} catch {
|
|
39
|
+
throw new TenantResolutionError("Custom domain routing is not available on this deployment", 503);
|
|
40
|
+
}
|
|
41
|
+
if (!hostname) throw new TenantResolutionError("Unable to determine request hostname", 400);
|
|
42
|
+
const resolved = await service.resolveByHostname(hostname);
|
|
43
|
+
if (!resolved || resolved.status !== "active") {
|
|
44
|
+
throw new TenantResolutionError("This domain is not configured for any active organization", 404);
|
|
45
|
+
}
|
|
46
|
+
if (bodyTenantId && bodyTenantId !== resolved.tenantId) {
|
|
47
|
+
throw new TenantResolutionError(
|
|
48
|
+
"tenantId in request body does not match the resolved custom domain",
|
|
49
|
+
400
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
source: "host",
|
|
54
|
+
tenantId: resolved.tenantId,
|
|
55
|
+
organizationId: resolved.organizationId,
|
|
56
|
+
hostname
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
export {
|
|
60
|
+
TenantResolutionError,
|
|
61
|
+
resolveTenantContext
|
|
62
|
+
};
|
|
63
|
+
//# sourceMappingURL=resolveTenantContext.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/customer_accounts/lib/resolveTenantContext.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Resolve the tenant context for a customer-portal request.\n *\n * - Platform-domain hosts: require `tenantId` in the request body.\n * - Custom-domain hosts: resolve via `domainMappingService.resolveByHostname()`.\n * If the body supplied a different `tenantId`, fail closed (mismatch).\n *\n * This is the single entry point used by all customer-auth routes (login,\n * signup, magic-link, password-reset) so they all behave consistently when\n * the request arrives on a tenant's branded URL.\n *\n * See `.ai/specs/2026-04-08-portal-custom-domain-routing.md` Phase 1.5.\n */\n\nimport { tryNormalizeHostname } from '@open-mercato/core/modules/customer_accounts/lib/hostname'\nimport { platformDomains } from '@open-mercato/core/modules/customer_accounts/lib/platformDomains'\nimport { secretEqual } from '@open-mercato/core/modules/customer_accounts/lib/secretCompare'\nimport type { AppContainer } from '@open-mercato/shared/lib/di/container'\nimport type { DomainMappingService } from '@open-mercato/core/modules/customer_accounts/services/domainMappingService'\n\nexport class TenantResolutionError extends Error {\n constructor(message: string, public readonly status: number) {\n super(message)\n this.name = 'TenantResolutionError'\n }\n}\n\nexport type ResolvedTenantContext = {\n source: 'body' | 'host'\n tenantId: string\n organizationId: string | null\n hostname: string | null\n}\n\nfunction readForcedHost(req: Request): string | null {\n // Test-only override. The middleware honors `X-Force-Host` only when\n // `NODE_ENV === 'test'` AND `X-Force-Host-Secret` matches; we mirror the\n // same check here so request-scoped helpers behave the same way under tests.\n if (process.env.NODE_ENV !== 'test') return null\n const expected = process.env.FORCE_HOST_SECRET\n if (!expected) return null\n if (!secretEqual(req.headers.get('x-force-host-secret'), expected)) return null\n const host = req.headers.get('x-force-host')\n return host && host.length > 0 ? host : null\n}\n\nexport async function resolveTenantContext(\n req: Request,\n bodyTenantId: string | null | undefined,\n options?: { container?: AppContainer },\n): Promise<ResolvedTenantContext> {\n const rawHost = readForcedHost(req) ?? req.headers.get('host')\n const hostname = rawHost ? tryNormalizeHostname(rawHost) : null\n const isPlatform = hostname ? platformDomains().includes(hostname) : true\n\n if (isPlatform) {\n if (!bodyTenantId) {\n throw new TenantResolutionError('tenantId is required for platform-domain logins', 400)\n }\n return { source: 'body', tenantId: bodyTenantId, organizationId: null, hostname }\n }\n\n // Custom-domain host\n let service: DomainMappingService | null = null\n try {\n if (options?.container) {\n service = options.container.resolve('domainMappingService') as DomainMappingService\n } else {\n const { createRequestContainer } = await import('@open-mercato/shared/lib/di/container')\n const container = await createRequestContainer()\n service = container.resolve('domainMappingService') as DomainMappingService\n }\n } catch {\n throw new TenantResolutionError('Custom domain routing is not available on this deployment', 503)\n }\n\n if (!hostname) throw new TenantResolutionError('Unable to determine request hostname', 400)\n const resolved = await service.resolveByHostname(hostname)\n if (!resolved || resolved.status !== 'active') {\n throw new TenantResolutionError('This domain is not configured for any active organization', 404)\n }\n\n if (bodyTenantId && bodyTenantId !== resolved.tenantId) {\n throw new TenantResolutionError(\n 'tenantId in request body does not match the resolved custom domain',\n 400,\n )\n }\n\n return {\n source: 'host',\n tenantId: resolved.tenantId,\n organizationId: resolved.organizationId,\n hostname,\n }\n}\n"],
|
|
5
|
+
"mappings": "AAcA,SAAS,4BAA4B;AACrC,SAAS,uBAAuB;AAChC,SAAS,mBAAmB;AAIrB,MAAM,8BAA8B,MAAM;AAAA,EAC/C,YAAY,SAAiC,QAAgB;AAC3D,UAAM,OAAO;AAD8B;AAE3C,SAAK,OAAO;AAAA,EACd;AACF;AASA,SAAS,eAAe,KAA6B;AAInD,MAAI,QAAQ,IAAI,aAAa,OAAQ,QAAO;AAC5C,QAAM,WAAW,QAAQ,IAAI;AAC7B,MAAI,CAAC,SAAU,QAAO;AACtB,MAAI,CAAC,YAAY,IAAI,QAAQ,IAAI,qBAAqB,GAAG,QAAQ,EAAG,QAAO;AAC3E,QAAM,OAAO,IAAI,QAAQ,IAAI,cAAc;AAC3C,SAAO,QAAQ,KAAK,SAAS,IAAI,OAAO;AAC1C;AAEA,eAAsB,qBACpB,KACA,cACA,SACgC;AAChC,QAAM,UAAU,eAAe,GAAG,KAAK,IAAI,QAAQ,IAAI,MAAM;AAC7D,QAAM,WAAW,UAAU,qBAAqB,OAAO,IAAI;AAC3D,QAAM,aAAa,WAAW,gBAAgB,EAAE,SAAS,QAAQ,IAAI;AAErE,MAAI,YAAY;AACd,QAAI,CAAC,cAAc;AACjB,YAAM,IAAI,sBAAsB,mDAAmD,GAAG;AAAA,IACxF;AACA,WAAO,EAAE,QAAQ,QAAQ,UAAU,cAAc,gBAAgB,MAAM,SAAS;AAAA,EAClF;AAGA,MAAI,UAAuC;AAC3C,MAAI;AACF,QAAI,SAAS,WAAW;AACtB,gBAAU,QAAQ,UAAU,QAAQ,sBAAsB;AAAA,IAC5D,OAAO;AACL,YAAM,EAAE,uBAAuB,IAAI,MAAM,OAAO,uCAAuC;AACvF,YAAM,YAAY,MAAM,uBAAuB;AAC/C,gBAAU,UAAU,QAAQ,sBAAsB;AAAA,IACpD;AAAA,EACF,QAAQ;AACN,UAAM,IAAI,sBAAsB,6DAA6D,GAAG;AAAA,EAClG;AAEA,MAAI,CAAC,SAAU,OAAM,IAAI,sBAAsB,wCAAwC,GAAG;AAC1F,QAAM,WAAW,MAAM,QAAQ,kBAAkB,QAAQ;AACzD,MAAI,CAAC,YAAY,SAAS,WAAW,UAAU;AAC7C,UAAM,IAAI,sBAAsB,6DAA6D,GAAG;AAAA,EAClG;AAEA,MAAI,gBAAgB,iBAAiB,SAAS,UAAU;AACtD,UAAM,IAAI;AAAA,MACR;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AAAA,IACL,QAAQ;AAAA,IACR,UAAU,SAAS;AAAA,IACnB,gBAAgB,SAAS;AAAA,IACzB;AAAA,EACF;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { timingSafeEqual } from "node:crypto";
|
|
2
|
+
function secretEqual(supplied, expected) {
|
|
3
|
+
if (typeof supplied !== "string" || supplied.length === 0) return false;
|
|
4
|
+
if (typeof expected !== "string" || expected.length === 0) return false;
|
|
5
|
+
const a = Buffer.from(supplied, "utf8");
|
|
6
|
+
const b = Buffer.from(expected, "utf8");
|
|
7
|
+
if (a.length !== b.length) return false;
|
|
8
|
+
return timingSafeEqual(a, b);
|
|
9
|
+
}
|
|
10
|
+
export {
|
|
11
|
+
secretEqual
|
|
12
|
+
};
|
|
13
|
+
//# sourceMappingURL=secretCompare.js.map
|