@open-mercato/core 0.6.8-develop.7043.1.b0199a62c6 → 0.6.8-develop.7047.1.32ab5fcb07

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.
@@ -91,7 +91,9 @@ async function postForm(request, path, data, options) {
91
91
  });
92
92
  }
93
93
  async function withCredentialIsolatedRequest(use) {
94
- const context = await playwrightRequest.newContext(BASE_URL ? { baseURL: BASE_URL } : {});
94
+ const context = await playwrightRequest.newContext({
95
+ baseURL: BASE_URL ?? "http://localhost:3000"
96
+ });
95
97
  try {
96
98
  return await use(context);
97
99
  } finally {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/helpers/integration/api.ts"],
4
- "sourcesContent": ["import { request as playwrightRequest, type APIRequestContext } from '@playwright/test';\nimport { DEFAULT_CREDENTIALS, type Role } from './auth';\n\nconst BASE_URL = process.env.BASE_URL?.trim() || null;\n\nfunction resolveUrl(path: string): string {\n return BASE_URL ? `${BASE_URL}${path}` : path;\n}\n\n// Cached tokens per credential to dodge the login rate limit\n// (5 attempts/60s per email). Tokens are reused across tests in the same\n// Playwright worker; each worker still mints its own.\nconst tokenCache = new Map<string, { token: string; mintedAt: number }>();\nconst TOKEN_TTL_MS = 45 * 60 * 1000; // 45 min; well under the default 2h session TTL.\n\n/**\n * Drops cached tokens so the next getAuthToken() mints a fresh session.\n *\n * A cached token points at a `sessions` row. Anything that deletes those rows \u2014\n * deactivating the user, a password change, an explicit session purge \u2014 leaves the\n * cached token resolving to 401 for the rest of the TTL, and `apiRequest` neither\n * retries nor re-mints. A spec that revokes sessions for an account other specs also\n * use MUST call this afterwards.\n */\nexport function clearAuthTokenCache(): void {\n tokenCache.clear();\n}\n\nexport async function getAuthToken(\n request: APIRequestContext,\n roleOrEmail: Role | string = 'admin',\n password?: string,\n): Promise<string> {\n const role = roleOrEmail in DEFAULT_CREDENTIALS ? (roleOrEmail as Role) : null;\n const credentialAttempts: Array<{ email: string; password: string }> = [];\n\n if (role) {\n const configured = DEFAULT_CREDENTIALS[role];\n credentialAttempts.push({ email: configured.email, password: password ?? configured.password });\n if (!password) {\n credentialAttempts.push({ email: `${role}@acme.com`, password: 'secret' });\n }\n } else {\n credentialAttempts.push({ email: roleOrEmail, password: password ?? 'secret' });\n }\n\n const cacheKey = credentialAttempts\n .map((entry) => `${entry.email}:${entry.password}`)\n .join('|');\n const cached = tokenCache.get(cacheKey);\n if (cached && Date.now() - cached.mintedAt < TOKEN_TTL_MS) {\n return cached.token;\n }\n\n let lastStatus = 0;\n\n for (const attempt of credentialAttempts) {\n const form = new URLSearchParams();\n form.set('email', attempt.email);\n form.set('password', attempt.password);\n\n // Retry on 429 (auth rate limit kicks in after ~25-30 rapid attempts from\n // the same test run). Capped exponential backoff: 1s, 2s, 4s; 3 retries.\n for (let retry = 0; retry < 4; retry += 1) {\n const response = await request.post(resolveUrl('/api/auth/login'), {\n headers: {\n 'content-type': 'application/x-www-form-urlencoded',\n },\n data: form.toString(),\n });\n\n const raw = await response.text();\n let body: Record<string, unknown> | null = null;\n try {\n body = raw ? (JSON.parse(raw) as Record<string, unknown>) : null;\n } catch {\n body = null;\n }\n\n lastStatus = response.status();\n if (response.ok() && body && typeof body.token === 'string' && body.token) {\n tokenCache.set(cacheKey, { token: body.token, mintedAt: Date.now() });\n return body.token;\n }\n if (response.status() !== 429) break;\n const backoffMs = 1000 * 2 ** retry;\n await new Promise((resolve) => setTimeout(resolve, backoffMs));\n }\n }\n\n throw new Error(`Failed to obtain auth token (status ${lastStatus})`);\n}\n\nexport async function apiRequest(\n request: APIRequestContext,\n method: string,\n path: string,\n options: {\n token: string;\n data?: unknown;\n timeout?: number;\n retryTransport?: boolean;\n headers?: Record<string, string>;\n },\n) {\n const headers = {\n Authorization: `Bearer ${options.token}`,\n 'Content-Type': 'application/json',\n ...(options.headers ?? {}),\n };\n const timeout = options.timeout ?? 30_000;\n let lastError: unknown = null;\n const maxAttempts = options.retryTransport === false ? 1 : 2;\n for (let attempt = 0; attempt < maxAttempts; attempt += 1) {\n try {\n return await request.fetch(resolveUrl(path), { method, headers, data: options.data, timeout });\n } catch (error) {\n lastError = error;\n const message = error instanceof Error ? error.message : '';\n const retryable = /timeout|idle-session|socket|ECONNRESET|Target page, context or browser has been closed/i.test(message);\n if (!retryable || attempt === maxAttempts - 1) throw error;\n await new Promise((resolve) => setTimeout(resolve, 250));\n }\n }\n throw lastError;\n}\n\nexport async function postForm(\n request: APIRequestContext,\n path: string,\n data: Record<string, string>,\n options?: { headers?: Record<string, string> },\n) {\n const form = new URLSearchParams();\n for (const [key, value] of Object.entries(data)) form.set(key, value);\n return request.post(resolveUrl(path), {\n headers: {\n 'content-type': 'application/x-www-form-urlencoded',\n ...(options?.headers ?? {}),\n },\n data: form.toString(),\n });\n}\n\n/**\n * Runs `use` against a request context that shares no cookies with the caller's.\n *\n * The `request` fixture keeps a cookie jar, and `/api/auth/login` sets `auth_token`\n * on it. Every later call through that fixture therefore carries the LAST logged-in\n * user's session, even one that deliberately sends no Authorization header or an\n * `ApiKey` one. Whether that cookie is stored at all depends on the deployment:\n * the login route marks it `secure` only when `NODE_ENV === 'production'`, so a lane\n * that serves the app with any other NODE_ENV over http keeps it while a production\n * lane silently drops it.\n *\n * A spec asserting \"no credentials are rejected\" or \"this API key alone decides\n * access\" must not depend on which lane it happens to run in \u2014 it must issue the\n * request from a jar that never saw a login. TC-DOCUMENTS-009 and TC-DOCUMENTS-018\n * both failed in the standalone lane for exactly that reason while passing in the\n * ephemeral one.\n */\nexport async function withCredentialIsolatedRequest<T>(\n use: (request: APIRequestContext) => Promise<T>,\n): Promise<T> {\n const context = await playwrightRequest.newContext(BASE_URL ? { baseURL: BASE_URL } : {});\n try {\n return await use(context);\n } finally {\n await context.dispose();\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { request as playwrightRequest, type APIRequestContext } from '@playwright/test';\nimport { DEFAULT_CREDENTIALS, type Role } from './auth';\n\nconst BASE_URL = process.env.BASE_URL?.trim() || null;\n\nfunction resolveUrl(path: string): string {\n return BASE_URL ? `${BASE_URL}${path}` : path;\n}\n\n// Cached tokens per credential to dodge the login rate limit\n// (5 attempts/60s per email). Tokens are reused across tests in the same\n// Playwright worker; each worker still mints its own.\nconst tokenCache = new Map<string, { token: string; mintedAt: number }>();\nconst TOKEN_TTL_MS = 45 * 60 * 1000; // 45 min; well under the default 2h session TTL.\n\n/**\n * Drops cached tokens so the next getAuthToken() mints a fresh session.\n *\n * A cached token points at a `sessions` row. Anything that deletes those rows \u2014\n * deactivating the user, a password change, an explicit session purge \u2014 leaves the\n * cached token resolving to 401 for the rest of the TTL, and `apiRequest` neither\n * retries nor re-mints. A spec that revokes sessions for an account other specs also\n * use MUST call this afterwards.\n */\nexport function clearAuthTokenCache(): void {\n tokenCache.clear();\n}\n\nexport async function getAuthToken(\n request: APIRequestContext,\n roleOrEmail: Role | string = 'admin',\n password?: string,\n): Promise<string> {\n const role = roleOrEmail in DEFAULT_CREDENTIALS ? (roleOrEmail as Role) : null;\n const credentialAttempts: Array<{ email: string; password: string }> = [];\n\n if (role) {\n const configured = DEFAULT_CREDENTIALS[role];\n credentialAttempts.push({ email: configured.email, password: password ?? configured.password });\n if (!password) {\n credentialAttempts.push({ email: `${role}@acme.com`, password: 'secret' });\n }\n } else {\n credentialAttempts.push({ email: roleOrEmail, password: password ?? 'secret' });\n }\n\n const cacheKey = credentialAttempts\n .map((entry) => `${entry.email}:${entry.password}`)\n .join('|');\n const cached = tokenCache.get(cacheKey);\n if (cached && Date.now() - cached.mintedAt < TOKEN_TTL_MS) {\n return cached.token;\n }\n\n let lastStatus = 0;\n\n for (const attempt of credentialAttempts) {\n const form = new URLSearchParams();\n form.set('email', attempt.email);\n form.set('password', attempt.password);\n\n // Retry on 429 (auth rate limit kicks in after ~25-30 rapid attempts from\n // the same test run). Capped exponential backoff: 1s, 2s, 4s; 3 retries.\n for (let retry = 0; retry < 4; retry += 1) {\n const response = await request.post(resolveUrl('/api/auth/login'), {\n headers: {\n 'content-type': 'application/x-www-form-urlencoded',\n },\n data: form.toString(),\n });\n\n const raw = await response.text();\n let body: Record<string, unknown> | null = null;\n try {\n body = raw ? (JSON.parse(raw) as Record<string, unknown>) : null;\n } catch {\n body = null;\n }\n\n lastStatus = response.status();\n if (response.ok() && body && typeof body.token === 'string' && body.token) {\n tokenCache.set(cacheKey, { token: body.token, mintedAt: Date.now() });\n return body.token;\n }\n if (response.status() !== 429) break;\n const backoffMs = 1000 * 2 ** retry;\n await new Promise((resolve) => setTimeout(resolve, backoffMs));\n }\n }\n\n throw new Error(`Failed to obtain auth token (status ${lastStatus})`);\n}\n\nexport async function apiRequest(\n request: APIRequestContext,\n method: string,\n path: string,\n options: {\n token: string;\n data?: unknown;\n timeout?: number;\n retryTransport?: boolean;\n headers?: Record<string, string>;\n },\n) {\n const headers = {\n Authorization: `Bearer ${options.token}`,\n 'Content-Type': 'application/json',\n ...(options.headers ?? {}),\n };\n const timeout = options.timeout ?? 30_000;\n let lastError: unknown = null;\n const maxAttempts = options.retryTransport === false ? 1 : 2;\n for (let attempt = 0; attempt < maxAttempts; attempt += 1) {\n try {\n return await request.fetch(resolveUrl(path), { method, headers, data: options.data, timeout });\n } catch (error) {\n lastError = error;\n const message = error instanceof Error ? error.message : '';\n const retryable = /timeout|idle-session|socket|ECONNRESET|Target page, context or browser has been closed/i.test(message);\n if (!retryable || attempt === maxAttempts - 1) throw error;\n await new Promise((resolve) => setTimeout(resolve, 250));\n }\n }\n throw lastError;\n}\n\nexport async function postForm(\n request: APIRequestContext,\n path: string,\n data: Record<string, string>,\n options?: { headers?: Record<string, string> },\n) {\n const form = new URLSearchParams();\n for (const [key, value] of Object.entries(data)) form.set(key, value);\n return request.post(resolveUrl(path), {\n headers: {\n 'content-type': 'application/x-www-form-urlencoded',\n ...(options?.headers ?? {}),\n },\n data: form.toString(),\n });\n}\n\n/**\n * Runs `use` against a request context that shares no cookies with the caller's.\n *\n * The `request` fixture keeps a cookie jar, and `/api/auth/login` sets `auth_token`\n * on it. Every later call through that fixture therefore carries the LAST logged-in\n * user's session, even one that deliberately sends no Authorization header or an\n * `ApiKey` one.\n *\n * A spec asserting \"no credentials are rejected\" or \"this API key alone decides\n * access\" must therefore issue the request from a jar that never saw a login,\n * rather than assuming the fixture's jar is empty. TC-DOCUMENTS-009 and\n * TC-DOCUMENTS-018 both observably failed in the standalone lane \u2014 with the\n * replayed cookie visible in the trace \u2014 while passing in the ephemeral one, and\n * routing them through this helper fixed both.\n */\nexport async function withCredentialIsolatedRequest<T>(\n use: (request: APIRequestContext) => Promise<T>,\n): Promise<T> {\n // A hand-built context does not inherit the project's `use.baseURL` the way the\n // `request` fixture does, so it has to repeat the config's own resolution \u2014 otherwise\n // a relative path throws here on any run that leaves BASE_URL unset, while the same\n // path works through the fixture.\n const context = await playwrightRequest.newContext({\n baseURL: BASE_URL ?? 'http://localhost:3000',\n });\n try {\n return await use(context);\n } finally {\n await context.dispose();\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -9,6 +9,9 @@ import {
9
9
  runCrudMutationGuardAfterSuccess
10
10
  } from "@open-mercato/shared/lib/crud/mutation-guard";
11
11
  import { registerDomainSchema } from "@open-mercato/core/modules/customer_accounts/data/validators";
12
+ import {
13
+ DomainMappingOrgScopeError
14
+ } from "@open-mercato/core/modules/customer_accounts/services/domainMappingService";
12
15
  import { DomainMapping } from "@open-mercato/core/modules/customer_accounts/data/entities";
13
16
  const FEATURE = "customer_accounts.domain.manage";
14
17
  const metadata = {
@@ -117,6 +120,12 @@ async function POST(req) {
117
120
  { status: 409 }
118
121
  );
119
122
  }
123
+ if (err instanceof DomainMappingOrgScopeError) {
124
+ return NextResponse.json(
125
+ { ok: false, error: "Organization was not found in the current tenant." },
126
+ { status: 400 }
127
+ );
128
+ }
120
129
  const message = err instanceof Error ? err.message : "Failed to register domain";
121
130
  return NextResponse.json({ ok: false, error: message }, { status: 500 });
122
131
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/customer_accounts/api/admin/domain-mappings.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { UniqueConstraintViolationException } from '@mikro-orm/core'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport {\n validateCrudMutationGuard,\n runCrudMutationGuardAfterSuccess,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport { registerDomainSchema } from '@open-mercato/core/modules/customer_accounts/data/validators'\nimport {\n DomainMappingService,\n type ResolveResult,\n} from '@open-mercato/core/modules/customer_accounts/services/domainMappingService'\nimport { DomainMapping } from '@open-mercato/core/modules/customer_accounts/data/entities'\n\nconst FEATURE = 'customer_accounts.domain.manage'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: [FEATURE] },\n POST: { requireAuth: true, requireFeatures: [FEATURE] },\n DELETE: { requireAuth: true, requireFeatures: [FEATURE] },\n}\n\nfunction isUniqueViolation(error: unknown): boolean {\n if (error instanceof UniqueConstraintViolationException) return true\n if (!error || typeof error !== 'object') return false\n const code = (error as { code?: string }).code\n if (code === '23505') return true\n const messageRaw = (error as { message?: string }).message\n const message = typeof messageRaw === 'string' ? messageRaw : ''\n return message.toLowerCase().includes('duplicate key')\n}\n\nfunction serializeRecord(record: DomainMapping) {\n return {\n id: record.id,\n hostname: record.hostname,\n organizationId: record.organizationId,\n tenantId: record.tenantId,\n provider: record.provider,\n status: record.status,\n verifiedAt: record.verifiedAt?.toISOString() ?? null,\n lastDnsCheckAt: record.lastDnsCheckAt?.toISOString() ?? null,\n dnsFailureReason: record.dnsFailureReason ?? null,\n tlsFailureReason: record.tlsFailureReason ?? null,\n tlsRetryCount: record.tlsRetryCount,\n cnameTarget: process.env.CUSTOM_DOMAIN_CNAME_TARGET ?? null,\n aRecordTarget: process.env.CUSTOM_DOMAIN_A_RECORD_TARGET ?? null,\n createdAt: record.createdAt.toISOString(),\n updatedAt: record.updatedAt?.toISOString() ?? null,\n }\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ ok: false, error: 'Unauthorized' }, { status: 401 })\n\n const container = await createRequestContainer()\n const rbac = container.resolve('rbacService') as RbacService\n const allowed = await rbac.userHasAllFeatures(auth.sub, [FEATURE], {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n if (!allowed) return NextResponse.json({ ok: false, error: 'Forbidden' }, { status: 403 })\n\n const url = new URL(req.url)\n const orgFilter = url.searchParams.get('organizationId')\n\n const em = container.resolve('em') as EntityManager\n const where: Record<string, unknown> = { tenantId: auth.tenantId }\n if (orgFilter) where.organizationId = orgFilter\n const records = await em.find(DomainMapping, where as never, { orderBy: { createdAt: 'desc' } })\n\n return NextResponse.json({\n ok: true,\n domainMappings: records.map(serializeRecord),\n config: {\n cnameTarget: process.env.CUSTOM_DOMAIN_CNAME_TARGET ?? null,\n aRecordTarget: process.env.CUSTOM_DOMAIN_A_RECORD_TARGET ?? null,\n },\n })\n}\n\nexport async function POST(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId) return NextResponse.json({ ok: false, error: 'Unauthorized' }, { status: 401 })\n\n const container = await createRequestContainer()\n const rbac = container.resolve('rbacService') as RbacService\n const allowed = await rbac.userHasAllFeatures(auth.sub, [FEATURE], {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n if (!allowed) return NextResponse.json({ ok: false, error: 'Forbidden' }, { status: 403 })\n\n const body = await readJsonSafe(req, {})\n const parsed = registerDomainSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json(\n { ok: false, error: 'Invalid request', issues: parsed.error.flatten() },\n { status: 400 },\n )\n }\n\n const guardResult = await validateCrudMutationGuard(container, {\n tenantId: auth.tenantId,\n organizationId: parsed.data.organizationId,\n userId: auth.sub,\n resourceKind: 'customer_accounts.domain_mapping',\n resourceId: parsed.data.organizationId,\n operation: 'create',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: parsed.data as unknown as Record<string, unknown>,\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n const service = container.resolve('domainMappingService') as DomainMappingService\n\n let entity: DomainMapping\n try {\n // service.register normalizes hostname internally \u2014 guards may have\n // returned modifiedPayload but the typed runner doesn't expose it,\n // so we rely on the service for the canonical form.\n entity = await service.register({\n hostname: parsed.data.hostname,\n organizationId: parsed.data.organizationId,\n tenantId: auth.tenantId,\n replacesDomainId: parsed.data.replacesDomainId,\n })\n } catch (err: unknown) {\n if (isUniqueViolation(err)) {\n return NextResponse.json(\n { ok: false, error: 'This domain is not available. Please choose a different hostname.' },\n { status: 409 },\n )\n }\n const message = err instanceof Error ? err.message : 'Failed to register domain'\n return NextResponse.json({ ok: false, error: message }, { status: 500 })\n }\n\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(container, {\n tenantId: auth.tenantId,\n organizationId: parsed.data.organizationId,\n userId: auth.sub,\n resourceKind: 'customer_accounts.domain_mapping',\n resourceId: entity.id,\n operation: 'create',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n return NextResponse.json({ ok: true, domainMapping: serializeRecord(entity) }, { status: 201 })\n}\n\nexport async function DELETE(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId) return NextResponse.json({ ok: false, error: 'Unauthorized' }, { status: 401 })\n\n const container = await createRequestContainer()\n const rbac = container.resolve('rbacService') as RbacService\n const allowed = await rbac.userHasAllFeatures(auth.sub, [FEATURE], {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n if (!allowed) return NextResponse.json({ ok: false, error: 'Forbidden' }, { status: 403 })\n\n const url = new URL(req.url)\n const id = url.searchParams.get('id')\n if (!id) return NextResponse.json({ ok: false, error: 'id is required' }, { status: 400 })\n\n const service = container.resolve('domainMappingService') as DomainMappingService\n const existing = await service.findById(id, { tenantId: auth.tenantId })\n if (!existing) return NextResponse.json({ ok: false, error: 'Not found' }, { status: 404 })\n\n const guardResult = await validateCrudMutationGuard(container, {\n tenantId: auth.tenantId,\n organizationId: existing.organizationId,\n userId: auth.sub,\n resourceKind: 'customer_accounts.domain_mapping',\n resourceId: id,\n operation: 'delete',\n requestMethod: req.method,\n requestHeaders: req.headers,\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n await service.remove(id, { tenantId: auth.tenantId })\n\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(container, {\n tenantId: auth.tenantId,\n organizationId: existing.organizationId,\n userId: auth.sub,\n resourceKind: 'customer_accounts.domain_mapping',\n resourceId: id,\n operation: 'delete',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n return NextResponse.json({ ok: true })\n}\n\nconst errorSchema = z.object({ ok: z.literal(false), error: z.string() })\nconst domainMappingSchema = z.object({\n id: z.string().uuid(),\n hostname: z.string(),\n organizationId: z.string().uuid(),\n tenantId: z.string().uuid(),\n provider: z.literal('traefik'),\n status: z.enum(['pending', 'verified', 'active', 'dns_failed', 'tls_failed']),\n verifiedAt: z.string().nullable(),\n lastDnsCheckAt: z.string().nullable(),\n dnsFailureReason: z.string().nullable(),\n tlsFailureReason: z.string().nullable(),\n tlsRetryCount: z.number().int().nonnegative(),\n cnameTarget: z.string().nullable(),\n aRecordTarget: z.string().nullable(),\n createdAt: z.string(),\n updatedAt: z.string().nullable(),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'CustomerAccounts',\n summary: 'Custom portal domain mappings (admin)',\n methods: {\n GET: {\n summary: 'List domain mappings',\n description: 'Returns all custom-domain mappings for the current tenant, optionally filtered by organization.',\n responses: [\n {\n status: 200,\n description: 'OK',\n schema: z.object({\n ok: z.literal(true),\n domainMappings: z.array(domainMappingSchema),\n config: z.object({\n cnameTarget: z.string().nullable(),\n aRecordTarget: z.string().nullable(),\n }),\n }),\n },\n ],\n errors: [\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n { status: 403, description: 'Forbidden', schema: errorSchema },\n ],\n },\n POST: {\n summary: 'Register a custom domain',\n description: 'Registers a new custom domain mapping for an organization. Verifies via DNS asynchronously.',\n requestBody: {\n contentType: 'application/json',\n schema: z.object({\n hostname: z.string(),\n organizationId: z.string().uuid(),\n replacesDomainId: z.string().uuid().optional(),\n }),\n },\n responses: [\n {\n status: 201,\n description: 'Created',\n schema: z.object({ ok: z.literal(true), domainMapping: domainMappingSchema }),\n },\n ],\n errors: [\n { status: 400, description: 'Validation error', schema: errorSchema },\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n { status: 403, description: 'Forbidden', schema: errorSchema },\n { status: 409, description: 'Conflict', schema: errorSchema },\n ],\n },\n DELETE: {\n summary: 'Remove a custom domain',\n description: 'Removes the domain mapping identified by ?id=. Cache and Traefik routing drain within TTL.',\n responses: [{ status: 200, description: 'OK', schema: z.object({ ok: z.literal(true) }) }],\n errors: [\n { status: 400, description: 'Bad request', schema: errorSchema },\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n { status: 403, description: 'Forbidden', schema: errorSchema },\n ],\n },\n },\n}\n\n// Re-export resolve type so tests can import without indirect lookup\nexport type { ResolveResult }\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { UniqueConstraintViolationException } from '@mikro-orm/core'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport {\n validateCrudMutationGuard,\n runCrudMutationGuardAfterSuccess,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport { registerDomainSchema } from '@open-mercato/core/modules/customer_accounts/data/validators'\nimport {\n DomainMappingService,\n DomainMappingOrgScopeError,\n type ResolveResult,\n} from '@open-mercato/core/modules/customer_accounts/services/domainMappingService'\nimport { DomainMapping } from '@open-mercato/core/modules/customer_accounts/data/entities'\n\nconst FEATURE = 'customer_accounts.domain.manage'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: [FEATURE] },\n POST: { requireAuth: true, requireFeatures: [FEATURE] },\n DELETE: { requireAuth: true, requireFeatures: [FEATURE] },\n}\n\nfunction isUniqueViolation(error: unknown): boolean {\n if (error instanceof UniqueConstraintViolationException) return true\n if (!error || typeof error !== 'object') return false\n const code = (error as { code?: string }).code\n if (code === '23505') return true\n const messageRaw = (error as { message?: string }).message\n const message = typeof messageRaw === 'string' ? messageRaw : ''\n return message.toLowerCase().includes('duplicate key')\n}\n\nfunction serializeRecord(record: DomainMapping) {\n return {\n id: record.id,\n hostname: record.hostname,\n organizationId: record.organizationId,\n tenantId: record.tenantId,\n provider: record.provider,\n status: record.status,\n verifiedAt: record.verifiedAt?.toISOString() ?? null,\n lastDnsCheckAt: record.lastDnsCheckAt?.toISOString() ?? null,\n dnsFailureReason: record.dnsFailureReason ?? null,\n tlsFailureReason: record.tlsFailureReason ?? null,\n tlsRetryCount: record.tlsRetryCount,\n cnameTarget: process.env.CUSTOM_DOMAIN_CNAME_TARGET ?? null,\n aRecordTarget: process.env.CUSTOM_DOMAIN_A_RECORD_TARGET ?? null,\n createdAt: record.createdAt.toISOString(),\n updatedAt: record.updatedAt?.toISOString() ?? null,\n }\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ ok: false, error: 'Unauthorized' }, { status: 401 })\n\n const container = await createRequestContainer()\n const rbac = container.resolve('rbacService') as RbacService\n const allowed = await rbac.userHasAllFeatures(auth.sub, [FEATURE], {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n if (!allowed) return NextResponse.json({ ok: false, error: 'Forbidden' }, { status: 403 })\n\n const url = new URL(req.url)\n const orgFilter = url.searchParams.get('organizationId')\n\n const em = container.resolve('em') as EntityManager\n const where: Record<string, unknown> = { tenantId: auth.tenantId }\n if (orgFilter) where.organizationId = orgFilter\n const records = await em.find(DomainMapping, where as never, { orderBy: { createdAt: 'desc' } })\n\n return NextResponse.json({\n ok: true,\n domainMappings: records.map(serializeRecord),\n config: {\n cnameTarget: process.env.CUSTOM_DOMAIN_CNAME_TARGET ?? null,\n aRecordTarget: process.env.CUSTOM_DOMAIN_A_RECORD_TARGET ?? null,\n },\n })\n}\n\nexport async function POST(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId) return NextResponse.json({ ok: false, error: 'Unauthorized' }, { status: 401 })\n\n const container = await createRequestContainer()\n const rbac = container.resolve('rbacService') as RbacService\n const allowed = await rbac.userHasAllFeatures(auth.sub, [FEATURE], {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n if (!allowed) return NextResponse.json({ ok: false, error: 'Forbidden' }, { status: 403 })\n\n const body = await readJsonSafe(req, {})\n const parsed = registerDomainSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json(\n { ok: false, error: 'Invalid request', issues: parsed.error.flatten() },\n { status: 400 },\n )\n }\n\n const guardResult = await validateCrudMutationGuard(container, {\n tenantId: auth.tenantId,\n organizationId: parsed.data.organizationId,\n userId: auth.sub,\n resourceKind: 'customer_accounts.domain_mapping',\n resourceId: parsed.data.organizationId,\n operation: 'create',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: parsed.data as unknown as Record<string, unknown>,\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n const service = container.resolve('domainMappingService') as DomainMappingService\n\n let entity: DomainMapping\n try {\n // service.register normalizes hostname internally \u2014 guards may have\n // returned modifiedPayload but the typed runner doesn't expose it,\n // so we rely on the service for the canonical form.\n entity = await service.register({\n hostname: parsed.data.hostname,\n organizationId: parsed.data.organizationId,\n tenantId: auth.tenantId,\n replacesDomainId: parsed.data.replacesDomainId,\n })\n } catch (err: unknown) {\n if (isUniqueViolation(err)) {\n return NextResponse.json(\n { ok: false, error: 'This domain is not available. Please choose a different hostname.' },\n { status: 409 },\n )\n }\n if (err instanceof DomainMappingOrgScopeError) {\n return NextResponse.json(\n { ok: false, error: 'Organization was not found in the current tenant.' },\n { status: 400 },\n )\n }\n const message = err instanceof Error ? err.message : 'Failed to register domain'\n return NextResponse.json({ ok: false, error: message }, { status: 500 })\n }\n\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(container, {\n tenantId: auth.tenantId,\n organizationId: parsed.data.organizationId,\n userId: auth.sub,\n resourceKind: 'customer_accounts.domain_mapping',\n resourceId: entity.id,\n operation: 'create',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n return NextResponse.json({ ok: true, domainMapping: serializeRecord(entity) }, { status: 201 })\n}\n\nexport async function DELETE(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId) return NextResponse.json({ ok: false, error: 'Unauthorized' }, { status: 401 })\n\n const container = await createRequestContainer()\n const rbac = container.resolve('rbacService') as RbacService\n const allowed = await rbac.userHasAllFeatures(auth.sub, [FEATURE], {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n if (!allowed) return NextResponse.json({ ok: false, error: 'Forbidden' }, { status: 403 })\n\n const url = new URL(req.url)\n const id = url.searchParams.get('id')\n if (!id) return NextResponse.json({ ok: false, error: 'id is required' }, { status: 400 })\n\n const service = container.resolve('domainMappingService') as DomainMappingService\n const existing = await service.findById(id, { tenantId: auth.tenantId })\n if (!existing) return NextResponse.json({ ok: false, error: 'Not found' }, { status: 404 })\n\n const guardResult = await validateCrudMutationGuard(container, {\n tenantId: auth.tenantId,\n organizationId: existing.organizationId,\n userId: auth.sub,\n resourceKind: 'customer_accounts.domain_mapping',\n resourceId: id,\n operation: 'delete',\n requestMethod: req.method,\n requestHeaders: req.headers,\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n await service.remove(id, { tenantId: auth.tenantId })\n\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(container, {\n tenantId: auth.tenantId,\n organizationId: existing.organizationId,\n userId: auth.sub,\n resourceKind: 'customer_accounts.domain_mapping',\n resourceId: id,\n operation: 'delete',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n return NextResponse.json({ ok: true })\n}\n\nconst errorSchema = z.object({ ok: z.literal(false), error: z.string() })\nconst domainMappingSchema = z.object({\n id: z.string().uuid(),\n hostname: z.string(),\n organizationId: z.string().uuid(),\n tenantId: z.string().uuid(),\n provider: z.literal('traefik'),\n status: z.enum(['pending', 'verified', 'active', 'dns_failed', 'tls_failed']),\n verifiedAt: z.string().nullable(),\n lastDnsCheckAt: z.string().nullable(),\n dnsFailureReason: z.string().nullable(),\n tlsFailureReason: z.string().nullable(),\n tlsRetryCount: z.number().int().nonnegative(),\n cnameTarget: z.string().nullable(),\n aRecordTarget: z.string().nullable(),\n createdAt: z.string(),\n updatedAt: z.string().nullable(),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'CustomerAccounts',\n summary: 'Custom portal domain mappings (admin)',\n methods: {\n GET: {\n summary: 'List domain mappings',\n description: 'Returns all custom-domain mappings for the current tenant, optionally filtered by organization.',\n responses: [\n {\n status: 200,\n description: 'OK',\n schema: z.object({\n ok: z.literal(true),\n domainMappings: z.array(domainMappingSchema),\n config: z.object({\n cnameTarget: z.string().nullable(),\n aRecordTarget: z.string().nullable(),\n }),\n }),\n },\n ],\n errors: [\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n { status: 403, description: 'Forbidden', schema: errorSchema },\n ],\n },\n POST: {\n summary: 'Register a custom domain',\n description: 'Registers a new custom domain mapping for an organization. Verifies via DNS asynchronously.',\n requestBody: {\n contentType: 'application/json',\n schema: z.object({\n hostname: z.string(),\n organizationId: z.string().uuid(),\n replacesDomainId: z.string().uuid().optional(),\n }),\n },\n responses: [\n {\n status: 201,\n description: 'Created',\n schema: z.object({ ok: z.literal(true), domainMapping: domainMappingSchema }),\n },\n ],\n errors: [\n { status: 400, description: 'Validation error', schema: errorSchema },\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n { status: 403, description: 'Forbidden', schema: errorSchema },\n { status: 409, description: 'Conflict', schema: errorSchema },\n ],\n },\n DELETE: {\n summary: 'Remove a custom domain',\n description: 'Removes the domain mapping identified by ?id=. Cache and Traefik routing drain within TTL.',\n responses: [{ status: 200, description: 'OK', schema: z.object({ ok: z.literal(true) }) }],\n errors: [\n { status: 400, description: 'Bad request', schema: errorSchema },\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n { status: 403, description: 'Forbidden', schema: errorSchema },\n ],\n },\n },\n}\n\n// Re-export resolve type so tests can import without indirect lookup\nexport type { ResolveResult }\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -7,8 +7,15 @@ import {
7
7
  import { Organization } from "@open-mercato/core/modules/directory/data/entities";
8
8
  import { emitCustomerAccountsEvent } from "@open-mercato/core/modules/customer_accounts/events";
9
9
  import { normalizeHostname, tryNormalizeHostname } from "@open-mercato/core/modules/customer_accounts/lib/hostname";
10
+ import { findOrganizationInTenant } from "@open-mercato/core/modules/customer_accounts/lib/organizationLookup";
10
11
  import { platformDomains } from "@open-mercato/core/modules/customer_accounts/lib/platformDomains";
11
12
  import { detectProxy, isInKnownProxyRange } from "@open-mercato/core/modules/customer_accounts/lib/proxyRanges";
13
+ class DomainMappingOrgScopeError extends Error {
14
+ constructor(organizationId) {
15
+ super(`[internal] organizationId ${organizationId} does not belong to the caller's tenant`);
16
+ this.name = "DomainMappingOrgScopeError";
17
+ }
18
+ }
12
19
  const DOMAIN_ROUTING_TAG = "domain_routing";
13
20
  const RESOLVE_KEY_PREFIX = "domain_routing:resolve";
14
21
  const ACTIVE_BY_ORG_KEY_PREFIX = "domain_routing:active-by-org";
@@ -186,6 +193,8 @@ class DomainMappingService {
186
193
  // -------------------------------------------------------------------------
187
194
  async register(input) {
188
195
  const hostname = normalizeHostname(input.hostname);
196
+ const organization = await findOrganizationInTenant(this.em, input.organizationId, input.tenantId);
197
+ if (!organization) throw new DomainMappingOrgScopeError(input.organizationId);
189
198
  let replacesDomain = null;
190
199
  if (input.replacesDomainId) {
191
200
  replacesDomain = await this.em.findOne(DomainMapping, {
@@ -491,6 +500,7 @@ const __testing__ = {
491
500
  Resolver
492
501
  };
493
502
  export {
503
+ DomainMappingOrgScopeError,
494
504
  DomainMappingService,
495
505
  __testing__
496
506
  };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/customer_accounts/services/domainMappingService.ts"],
4
- "sourcesContent": ["import { Resolver, promises as dnsPromises } from 'node:dns'\nimport { request as httpsRequest } from 'node:https'\nimport { setTimeout as delay } from 'node:timers/promises'\nimport { EntityManager } from '@mikro-orm/postgresql'\nimport {\n DomainMapping,\n type DomainStatus,\n} from '@open-mercato/core/modules/customer_accounts/data/entities'\nimport { Organization } from '@open-mercato/core/modules/directory/data/entities'\nimport { emitCustomerAccountsEvent } from '@open-mercato/core/modules/customer_accounts/events'\nimport { normalizeHostname, tryNormalizeHostname } from '@open-mercato/core/modules/customer_accounts/lib/hostname'\nimport { platformDomains } from '@open-mercato/core/modules/customer_accounts/lib/platformDomains'\nimport { detectProxy, isInKnownProxyRange } from '@open-mercato/core/modules/customer_accounts/lib/proxyRanges'\n\nconst DOMAIN_ROUTING_TAG = 'domain_routing'\nconst RESOLVE_KEY_PREFIX = 'domain_routing:resolve'\nconst ACTIVE_BY_ORG_KEY_PREFIX = 'domain_routing:active-by-org'\nconst RESOLVE_TTL_MS = 5 * 60_000\nconst TLS_HEALTH_CHECK_TIMEOUT_MS = 10_000\nconst TLS_HEALTH_CHECK_RETRY_DELAYS_MS = [1_000, 4_000, 16_000]\nconst DEFAULT_DNS_RECHECK_THRESHOLD_MS = 5 * 60_000\nconst DEFAULT_TLS_MAX_RETRIES = 6\n\nexport type ResolveResult = {\n domainMappingId: string\n hostname: string\n tenantId: string\n organizationId: string\n orgSlug: string | null\n status: DomainStatus\n}\n\nexport type DnsDiagnostics = {\n expectedCnameTarget: string\n expectedARecordTarget: string | null\n detectedRecords: Array<{ type: 'CNAME' | 'A'; value: string; proxy?: string }>\n reverseResolve?: { attempted: boolean; originHeaderPresent: boolean }\n suggestion: string\n}\n\nexport type VerifyResult = {\n domainMapping: DomainMapping\n diagnostics?: DnsDiagnostics\n}\n\nexport type RegisterInput = {\n hostname: string\n organizationId: string\n tenantId: string\n replacesDomainId?: string\n}\n\ntype CacheService = {\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\ntype DnsResolverContract = {\n resolveCname(hostname: string): Promise<string[]>\n resolve4(hostname: string): Promise<string[]>\n}\n\ntype HealthCheckContract = (hostname: string, timeoutMs: number) => Promise<{\n ok: boolean\n originHeaderPresent: boolean\n reason?: string\n}>\n\nconst defaultDnsResolver: DnsResolverContract = {\n async resolveCname(hostname) {\n try {\n return await dnsPromises.resolveCname(hostname)\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code\n if (code === 'ENODATA' || code === 'ENOTFOUND') return []\n throw err\n }\n },\n async resolve4(hostname) {\n try {\n return await dnsPromises.resolve4(hostname)\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code\n if (code === 'ENODATA' || code === 'ENOTFOUND') return []\n throw err\n }\n },\n}\n\nconst defaultHealthCheck: HealthCheckContract = (hostname, timeoutMs) =>\n new Promise((resolve) => {\n const headerName = (process.env.CUSTOMER_DOMAIN_ORIGIN_HEADER ?? 'X-Open-Mercato-Origin').toLowerCase()\n const req = httpsRequest(\n {\n host: hostname,\n port: 443,\n path: '/api/customer_accounts/domain-check',\n method: 'GET',\n headers: { 'X-Domain-Check-Secret': process.env.DOMAIN_CHECK_SECRET ?? '' },\n timeout: timeoutMs,\n },\n (res) => {\n const headerValue = res.headers[headerName]\n const originHeader = Array.isArray(headerValue) ? headerValue[0] : headerValue\n const originHeaderPresent = typeof originHeader === 'string' && originHeader === '1'\n const status = res.statusCode ?? 0\n res.resume() // discard body\n resolve({\n ok: status >= 200 && status < 400,\n originHeaderPresent,\n reason: status >= 200 && status < 400 ? undefined : `HTTP ${status}`,\n })\n },\n )\n req.on('timeout', () => {\n req.destroy(new Error('TLS health check timed out'))\n })\n req.on('error', (err) => {\n resolve({ ok: false, originHeaderPresent: false, reason: (err as Error).message })\n })\n req.end()\n })\n\nexport class DomainMappingService {\n private cache: CacheService | null\n private dns: DnsResolverContract\n private healthCheckImpl: HealthCheckContract\n\n constructor(\n private em: EntityManager,\n deps?: {\n cacheService?: CacheService\n dnsResolver?: DnsResolverContract\n healthCheck?: HealthCheckContract\n },\n ) {\n this.cache = deps?.cacheService ?? null\n this.dns = deps?.dnsResolver ?? defaultDnsResolver\n this.healthCheckImpl = deps?.healthCheck ?? defaultHealthCheck\n }\n\n // -------------------------------------------------------------------------\n // Read paths\n // -------------------------------------------------------------------------\n\n async findById(id: string, scope?: { tenantId?: string }): Promise<DomainMapping | null> {\n const where: Record<string, unknown> = { id }\n if (scope?.tenantId) where.tenantId = scope.tenantId\n return this.em.findOne(DomainMapping, where as never)\n }\n\n async findByOrganization(\n organizationId: string,\n scope?: { tenantId?: string },\n ): Promise<DomainMapping[]> {\n const where: Record<string, unknown> = { organizationId }\n if (scope?.tenantId) where.tenantId = scope.tenantId\n return this.em.find(DomainMapping, where as never, { orderBy: { createdAt: 'asc' } })\n }\n\n async resolveByHostname(input: string): Promise<ResolveResult | null> {\n const hostname = tryNormalizeHostname(input)\n if (!hostname) return null\n if (platformDomains().includes(hostname)) return null\n\n const cacheKey = `${RESOLVE_KEY_PREFIX}:${hostname}`\n if (this.cache) {\n const cached = (await this.cache.get(cacheKey)) as ResolveResult | null | undefined\n if (cached !== undefined && cached !== null) return cached\n }\n\n const result = await this.lookupResolveResult(hostname)\n if (this.cache) {\n await this.cache.set(cacheKey, result, {\n ttl: RESOLVE_TTL_MS,\n tags: [DOMAIN_ROUTING_TAG, `${DOMAIN_ROUTING_TAG}:${hostname}`],\n })\n }\n return result\n }\n\n async isAllowedForTls(input: string): Promise<{ organizationId: string; status: DomainStatus } | null> {\n const hostname = tryNormalizeHostname(input)\n if (!hostname) return null\n if (platformDomains().includes(hostname)) return null\n const row = await this.em.findOne(DomainMapping, {\n hostname,\n status: { $in: ['active', 'verified'] },\n } as never)\n if (!row) return null\n return { organizationId: row.organizationId, status: row.status }\n }\n\n async resolveActiveByOrg(organizationId: string): Promise<{ hostname: string; status: DomainStatus } | null> {\n const cacheKey = `${ACTIVE_BY_ORG_KEY_PREFIX}:${organizationId}`\n if (this.cache) {\n const cached = (await this.cache.get(cacheKey)) as { hostname: string; status: DomainStatus } | null | undefined\n if (cached !== undefined && cached !== null) return cached\n }\n\n const row = await this.em.findOne(DomainMapping, {\n organizationId,\n status: 'active',\n } as never)\n const result = row ? { hostname: row.hostname, status: row.status } : null\n\n if (this.cache) {\n await this.cache.set(cacheKey, result, {\n ttl: RESOLVE_TTL_MS,\n tags: [DOMAIN_ROUTING_TAG, `${DOMAIN_ROUTING_TAG}:org:${organizationId}`],\n })\n }\n return result\n }\n\n async resolveAll(): Promise<ResolveResult[]> {\n const rows = await this.em.find(DomainMapping, { status: 'active' } as never)\n if (rows.length === 0) return []\n const orgIds = Array.from(new Set(rows.map((r) => r.organizationId)))\n const orgs = await this.em.find(Organization, { id: { $in: orgIds } } as never)\n const slugByOrg = new Map<string, string | null>(orgs.map((o) => [o.id, o.slug ?? null]))\n\n return rows.map<ResolveResult>((row) => ({\n domainMappingId: row.id,\n hostname: row.hostname,\n tenantId: row.tenantId,\n organizationId: row.organizationId,\n orgSlug: slugByOrg.get(row.organizationId) ?? null,\n status: row.status,\n }))\n }\n\n // -------------------------------------------------------------------------\n // Worker queries\n // -------------------------------------------------------------------------\n\n async findPendingVerification(threshold?: { olderThanMs?: number }): Promise<DomainMapping[]> {\n const olderThan = threshold?.olderThanMs ?? DEFAULT_DNS_RECHECK_THRESHOLD_MS\n const cutoff = new Date(Date.now() - olderThan)\n return this.em.find(\n DomainMapping,\n {\n status: { $in: ['pending', 'dns_failed'] },\n $or: [{ lastDnsCheckAt: null }, { lastDnsCheckAt: { $lt: cutoff } }],\n } as never,\n { orderBy: { lastDnsCheckAt: 'asc' } },\n )\n }\n\n async findPendingTls(options?: { maxRetries?: number; batchSize?: number }): Promise<DomainMapping[]> {\n const maxRetries = options?.maxRetries ?? DEFAULT_TLS_MAX_RETRIES\n const limit = options?.batchSize ?? 50\n return this.em.find(\n DomainMapping,\n {\n $or: [\n { status: 'verified' },\n { status: 'tls_failed', tlsRetryCount: { $lt: maxRetries } },\n ],\n } as never,\n { orderBy: { updatedAt: 'asc' }, limit },\n )\n }\n\n // -------------------------------------------------------------------------\n // Write paths\n // -------------------------------------------------------------------------\n\n async register(input: RegisterInput): Promise<DomainMapping> {\n const hostname = normalizeHostname(input.hostname)\n\n let replacesDomain: DomainMapping | null = null\n if (input.replacesDomainId) {\n replacesDomain = await this.em.findOne(DomainMapping, {\n id: input.replacesDomainId,\n tenantId: input.tenantId,\n } as never)\n }\n\n const entity = this.em.create(DomainMapping, {\n hostname,\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n replacesDomain: replacesDomain ?? null,\n provider: 'traefik',\n status: 'pending',\n tlsRetryCount: 0,\n createdAt: new Date(),\n } as never)\n await this.em.persist(entity).flush()\n\n await emitCustomerAccountsEvent('customer_accounts.domain_mapping.created', {\n id: entity.id,\n hostname: entity.hostname,\n organizationId: entity.organizationId,\n tenantId: entity.tenantId,\n status: entity.status,\n } as never)\n await this.invalidateCacheFor(entity.hostname, entity.organizationId)\n\n return entity\n }\n\n async verify(id: string): Promise<VerifyResult> {\n const entity = await this.em.findOne(DomainMapping, { id } as never)\n if (!entity) throw new Error(`DomainMapping ${id} not found`)\n\n const expectedCname = process.env.CUSTOM_DOMAIN_CNAME_TARGET ?? ''\n const expectedARecord = process.env.CUSTOM_DOMAIN_A_RECORD_TARGET ?? null\n const now = new Date()\n entity.lastDnsCheckAt = now\n\n const verification = await this.runDnsVerification(entity.hostname, {\n expectedCname,\n expectedARecord,\n })\n\n if (verification.ok) {\n entity.status = 'verified'\n entity.verifiedAt = now\n entity.dnsFailureReason = null\n entity.tlsRetryCount = 0\n entity.tlsFailureReason = null\n await this.em.persist(entity).flush()\n await emitCustomerAccountsEvent('customer_accounts.domain_mapping.verified', {\n id: entity.id,\n hostname: entity.hostname,\n organizationId: entity.organizationId,\n tenantId: entity.tenantId,\n } as never)\n await this.invalidateCacheFor(entity.hostname, entity.organizationId)\n return { domainMapping: entity }\n }\n\n entity.status = 'dns_failed'\n entity.dnsFailureReason = verification.reason\n await this.em.persist(entity).flush()\n await emitCustomerAccountsEvent('customer_accounts.domain_mapping.dns_failed', {\n id: entity.id,\n hostname: entity.hostname,\n organizationId: entity.organizationId,\n tenantId: entity.tenantId,\n reason: verification.reason,\n detectedRecords: verification.diagnostics.detectedRecords,\n } as never)\n await this.invalidateCacheFor(entity.hostname, entity.organizationId)\n return { domainMapping: entity, diagnostics: verification.diagnostics }\n }\n\n async activate(id: string): Promise<DomainMapping> {\n const entity = await this.em.findOne(DomainMapping, { id } as never)\n if (!entity) throw new Error(`DomainMapping ${id} not found`)\n if (entity.status === 'active') return entity\n if (entity.status !== 'verified' && entity.status !== 'tls_failed') {\n throw new Error(`DomainMapping ${id} cannot transition to active from ${entity.status}`)\n }\n\n entity.status = 'active'\n entity.tlsRetryCount = 0\n entity.tlsFailureReason = null\n await this.em.persist(entity).flush()\n\n await emitCustomerAccountsEvent('customer_accounts.domain_mapping.activated', {\n id: entity.id,\n hostname: entity.hostname,\n organizationId: entity.organizationId,\n tenantId: entity.tenantId,\n } as never)\n\n if (entity.replacesDomain) {\n const replaced = await this.em.findOne(DomainMapping, {\n id: (entity.replacesDomain as unknown as { id: string }).id,\n } as never)\n if (replaced) {\n const replacedHostname = replaced.hostname\n const replacedOrg = replaced.organizationId\n this.em.remove(replaced)\n await this.em.flush()\n await emitCustomerAccountsEvent('customer_accounts.domain_mapping.replaced', {\n id: entity.id,\n hostname: entity.hostname,\n organizationId: entity.organizationId,\n tenantId: entity.tenantId,\n replacedDomainId: replaced.id,\n replacedHostname,\n } as never)\n await emitCustomerAccountsEvent('customer_accounts.domain_mapping.deleted', {\n id: replaced.id,\n hostname: replacedHostname,\n organizationId: replacedOrg,\n tenantId: replaced.tenantId,\n } as never)\n await this.invalidateCacheFor(replacedHostname, replacedOrg)\n }\n }\n\n await this.invalidateCacheFor(entity.hostname, entity.organizationId)\n return entity\n }\n\n async remove(id: string, scope?: { tenantId?: string }): Promise<void> {\n const where: Record<string, unknown> = { id }\n if (scope?.tenantId) where.tenantId = scope.tenantId\n const entity = await this.em.findOne(DomainMapping, where as never)\n if (!entity) return\n\n const hostname = entity.hostname\n const organizationId = entity.organizationId\n const tenantId = entity.tenantId\n\n this.em.remove(entity)\n await this.em.flush()\n\n await emitCustomerAccountsEvent('customer_accounts.domain_mapping.deleted', {\n id,\n hostname,\n organizationId,\n tenantId,\n } as never)\n await this.invalidateCacheFor(hostname, organizationId)\n }\n\n async healthCheck(id: string): Promise<DomainMapping> {\n const entity = await this.em.findOne(DomainMapping, { id } as never)\n if (!entity) throw new Error(`DomainMapping ${id} not found`)\n if (entity.status === 'active') return entity\n if (entity.status !== 'verified' && entity.status !== 'tls_failed') {\n throw new Error(`DomainMapping ${id} cannot run health check from status ${entity.status}`)\n }\n\n let lastReason: string | null = null\n for (let attempt = 0; attempt < TLS_HEALTH_CHECK_RETRY_DELAYS_MS.length; attempt++) {\n const result = await this.healthCheckImpl(entity.hostname, TLS_HEALTH_CHECK_TIMEOUT_MS)\n if (result.ok) {\n return this.activate(entity.id)\n }\n lastReason = result.reason ?? 'TLS health check failed'\n if (attempt + 1 < TLS_HEALTH_CHECK_RETRY_DELAYS_MS.length) {\n await delay(TLS_HEALTH_CHECK_RETRY_DELAYS_MS[attempt])\n }\n }\n\n entity.status = 'tls_failed'\n entity.tlsFailureReason = lastReason ?? 'TLS health check failed'\n entity.tlsRetryCount = (entity.tlsRetryCount ?? 0) + 1\n await this.em.persist(entity).flush()\n\n await emitCustomerAccountsEvent('customer_accounts.domain_mapping.tls_failed', {\n id: entity.id,\n hostname: entity.hostname,\n organizationId: entity.organizationId,\n tenantId: entity.tenantId,\n reason: entity.tlsFailureReason,\n retryCount: entity.tlsRetryCount,\n } as never)\n await this.invalidateCacheFor(entity.hostname, entity.organizationId)\n return entity\n }\n\n // -------------------------------------------------------------------------\n // Internals\n // -------------------------------------------------------------------------\n\n private async lookupResolveResult(hostname: string): Promise<ResolveResult | null> {\n const row = await this.em.findOne(DomainMapping, { hostname, status: 'active' } as never)\n if (!row) return null\n const org = await this.em.findOne(Organization, { id: row.organizationId } as never)\n return {\n domainMappingId: row.id,\n hostname: row.hostname,\n tenantId: row.tenantId,\n organizationId: row.organizationId,\n orgSlug: org?.slug ?? null,\n status: row.status,\n }\n }\n\n private async invalidateCacheFor(hostname: string, organizationId: string | null): Promise<void> {\n if (!this.cache) return\n const tags = [DOMAIN_ROUTING_TAG, `${DOMAIN_ROUTING_TAG}:${hostname}`]\n if (organizationId) tags.push(`${DOMAIN_ROUTING_TAG}:org:${organizationId}`)\n try {\n await this.cache.deleteByTags(tags)\n } catch {\n // Cache invalidation is best-effort \u2014 TTL backstop ensures eventual consistency.\n }\n }\n\n private async runDnsVerification(\n hostname: string,\n expected: { expectedCname: string; expectedARecord: string | null },\n ): Promise<\n | {\n ok: true\n method: 'cname' | 'a-record' | 'reverse-resolve'\n diagnostics: DnsDiagnostics\n }\n | { ok: false; reason: string; diagnostics: DnsDiagnostics }\n > {\n const detectedRecords: DnsDiagnostics['detectedRecords'] = []\n const baseDiag = (overrides?: Partial<DnsDiagnostics>): DnsDiagnostics => ({\n expectedCnameTarget: expected.expectedCname,\n expectedARecordTarget: expected.expectedARecord,\n detectedRecords,\n suggestion: overrides?.suggestion ?? '',\n reverseResolve: overrides?.reverseResolve,\n })\n\n // Phase 1: CNAME\n let cnameRecords: string[] = []\n try {\n cnameRecords = await this.dns.resolveCname(hostname)\n } catch (err) {\n return {\n ok: false,\n reason: `DNS lookup error (CNAME): ${(err as Error).message}`,\n diagnostics: baseDiag({\n suggestion: 'DNS lookup failed unexpectedly. Try again in a few minutes.',\n }),\n }\n }\n for (const cname of cnameRecords) detectedRecords.push({ type: 'CNAME', value: cname })\n\n if (expected.expectedCname && cnameRecords.length > 0) {\n const expectedCname = tryNormalizeHostname(expected.expectedCname) ?? expected.expectedCname.toLowerCase()\n const match = cnameRecords.some((c) => (tryNormalizeHostname(c) ?? c.toLowerCase()) === expectedCname)\n if (match) {\n return {\n ok: true,\n method: 'cname',\n diagnostics: baseDiag({\n suggestion: 'CNAME record matches the expected target.',\n }),\n }\n }\n return {\n ok: false,\n reason: `CNAME points to ${cnameRecords.join(', ')} instead of ${expected.expectedCname}`,\n diagnostics: baseDiag({\n suggestion: `Update your CNAME record to point to ${expected.expectedCname}.`,\n }),\n }\n }\n\n // Phase 2: A record\n let aRecords: string[] = []\n try {\n aRecords = await this.dns.resolve4(hostname)\n } catch (err) {\n return {\n ok: false,\n reason: `DNS lookup error (A): ${(err as Error).message}`,\n diagnostics: baseDiag({ suggestion: 'DNS lookup failed unexpectedly. Try again in a few minutes.' }),\n }\n }\n for (const a of aRecords) {\n const proxy = detectProxy(a)\n detectedRecords.push({ type: 'A', value: a, ...(proxy ? { proxy } : {}) })\n }\n\n if (aRecords.length === 0) {\n return {\n ok: false,\n reason: `No CNAME or A record found for ${hostname}`,\n diagnostics: baseDiag({\n suggestion: expected.expectedARecord\n ? `For a subdomain, add a CNAME record pointing to ${expected.expectedCname}. For an apex domain, add an A record pointing to ${expected.expectedARecord}. DNS propagation can take up to 48 hours.`\n : `Add a CNAME record pointing to ${expected.expectedCname}. DNS propagation can take up to 48 hours.`,\n }),\n }\n }\n\n if (expected.expectedARecord && aRecords.includes(expected.expectedARecord)) {\n return {\n ok: true,\n method: 'a-record',\n diagnostics: baseDiag({ suggestion: 'A record matches the expected target.' }),\n }\n }\n\n // Phase 3: reverse-resolve through proxy\n const proxiedRecords = aRecords.filter((ip) => isInKnownProxyRange(ip))\n if (proxiedRecords.length > 0) {\n const probe = await this.healthCheckImpl(hostname, TLS_HEALTH_CHECK_TIMEOUT_MS)\n if (probe.ok && probe.originHeaderPresent) {\n return {\n ok: true,\n method: 'reverse-resolve',\n diagnostics: baseDiag({\n reverseResolve: { attempted: true, originHeaderPresent: true },\n suggestion: 'Domain is proxied \u2014 reverse-resolve confirmed traffic reaches our origin.',\n }),\n }\n }\n return {\n ok: false,\n reason: 'A record points to a known proxy IP, but reverse-resolve over HTTPS did not reach our server',\n diagnostics: baseDiag({\n reverseResolve: { attempted: true, originHeaderPresent: false },\n suggestion: expected.expectedCname\n ? `Your DNS uses a proxy. Either disable the proxy and add a CNAME \u2192 ${expected.expectedCname}, or configure your proxy to forward traffic to ${expected.expectedCname}.`\n : 'Disable the DNS proxy or configure it to forward traffic to our platform.',\n }),\n }\n }\n\n return {\n ok: false,\n reason: expected.expectedARecord\n ? `A record points to ${aRecords.join(', ')} instead of ${expected.expectedARecord}`\n : `A record points to ${aRecords.join(', ')} but apex-domain registration is not enabled on this deployment`,\n diagnostics: baseDiag({\n suggestion: expected.expectedARecord\n ? `Update your A record to point to ${expected.expectedARecord}.`\n : `Apex-domain registration is not enabled on this deployment. Use a subdomain (e.g., shop.${hostname}) and add a CNAME pointing to ${expected.expectedCname}.`,\n }),\n }\n }\n}\n\n// Re-exported for tests; allow injection of fakes.\nexport const __testing__ = {\n DEFAULT_DNS_RECHECK_THRESHOLD_MS,\n DEFAULT_TLS_MAX_RETRIES,\n TLS_HEALTH_CHECK_RETRY_DELAYS_MS,\n RESOLVE_TTL_MS,\n DOMAIN_ROUTING_TAG,\n defaultHealthCheck,\n defaultDnsResolver,\n Resolver,\n}\n"],
5
- "mappings": "AAAA,SAAS,UAAU,YAAY,mBAAmB;AAClD,SAAS,WAAW,oBAAoB;AACxC,SAAS,cAAc,aAAa;AAEpC;AAAA,EACE;AAAA,OAEK;AACP,SAAS,oBAAoB;AAC7B,SAAS,iCAAiC;AAC1C,SAAS,mBAAmB,4BAA4B;AACxD,SAAS,uBAAuB;AAChC,SAAS,aAAa,2BAA2B;AAEjD,MAAM,qBAAqB;AAC3B,MAAM,qBAAqB;AAC3B,MAAM,2BAA2B;AACjC,MAAM,iBAAiB,IAAI;AAC3B,MAAM,8BAA8B;AACpC,MAAM,mCAAmC,CAAC,KAAO,KAAO,IAAM;AAC9D,MAAM,mCAAmC,IAAI;AAC7C,MAAM,0BAA0B;AAgDhC,MAAM,qBAA0C;AAAA,EAC9C,MAAM,aAAa,UAAU;AAC3B,QAAI;AACF,aAAO,MAAM,YAAY,aAAa,QAAQ;AAAA,IAChD,SAAS,KAAK;AACZ,YAAM,OAAQ,IAA8B;AAC5C,UAAI,SAAS,aAAa,SAAS,YAAa,QAAO,CAAC;AACxD,YAAM;AAAA,IACR;AAAA,EACF;AAAA,EACA,MAAM,SAAS,UAAU;AACvB,QAAI;AACF,aAAO,MAAM,YAAY,SAAS,QAAQ;AAAA,IAC5C,SAAS,KAAK;AACZ,YAAM,OAAQ,IAA8B;AAC5C,UAAI,SAAS,aAAa,SAAS,YAAa,QAAO,CAAC;AACxD,YAAM;AAAA,IACR;AAAA,EACF;AACF;AAEA,MAAM,qBAA0C,CAAC,UAAU,cACzD,IAAI,QAAQ,CAAC,YAAY;AACvB,QAAM,cAAc,QAAQ,IAAI,iCAAiC,yBAAyB,YAAY;AACtG,QAAM,MAAM;AAAA,IACV;AAAA,MACE,MAAM;AAAA,MACN,MAAM;AAAA,MACN,MAAM;AAAA,MACN,QAAQ;AAAA,MACR,SAAS,EAAE,yBAAyB,QAAQ,IAAI,uBAAuB,GAAG;AAAA,MAC1E,SAAS;AAAA,IACX;AAAA,IACA,CAAC,QAAQ;AACP,YAAM,cAAc,IAAI,QAAQ,UAAU;AAC1C,YAAM,eAAe,MAAM,QAAQ,WAAW,IAAI,YAAY,CAAC,IAAI;AACnE,YAAM,sBAAsB,OAAO,iBAAiB,YAAY,iBAAiB;AACjF,YAAM,SAAS,IAAI,cAAc;AACjC,UAAI,OAAO;AACX,cAAQ;AAAA,QACN,IAAI,UAAU,OAAO,SAAS;AAAA,QAC9B;AAAA,QACA,QAAQ,UAAU,OAAO,SAAS,MAAM,SAAY,QAAQ,MAAM;AAAA,MACpE,CAAC;AAAA,IACH;AAAA,EACF;AACA,MAAI,GAAG,WAAW,MAAM;AACtB,QAAI,QAAQ,IAAI,MAAM,4BAA4B,CAAC;AAAA,EACrD,CAAC;AACD,MAAI,GAAG,SAAS,CAAC,QAAQ;AACvB,YAAQ,EAAE,IAAI,OAAO,qBAAqB,OAAO,QAAS,IAAc,QAAQ,CAAC;AAAA,EACnF,CAAC;AACD,MAAI,IAAI;AACV,CAAC;AAEI,MAAM,qBAAqB;AAAA,EAKhC,YACU,IACR,MAKA;AANQ;AAOR,SAAK,QAAQ,MAAM,gBAAgB;AACnC,SAAK,MAAM,MAAM,eAAe;AAChC,SAAK,kBAAkB,MAAM,eAAe;AAAA,EAC9C;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,SAAS,IAAY,OAA8D;AACvF,UAAM,QAAiC,EAAE,GAAG;AAC5C,QAAI,OAAO,SAAU,OAAM,WAAW,MAAM;AAC5C,WAAO,KAAK,GAAG,QAAQ,eAAe,KAAc;AAAA,EACtD;AAAA,EAEA,MAAM,mBACJ,gBACA,OAC0B;AAC1B,UAAM,QAAiC,EAAE,eAAe;AACxD,QAAI,OAAO,SAAU,OAAM,WAAW,MAAM;AAC5C,WAAO,KAAK,GAAG,KAAK,eAAe,OAAgB,EAAE,SAAS,EAAE,WAAW,MAAM,EAAE,CAAC;AAAA,EACtF;AAAA,EAEA,MAAM,kBAAkB,OAA8C;AACpE,UAAM,WAAW,qBAAqB,KAAK;AAC3C,QAAI,CAAC,SAAU,QAAO;AACtB,QAAI,gBAAgB,EAAE,SAAS,QAAQ,EAAG,QAAO;AAEjD,UAAM,WAAW,GAAG,kBAAkB,IAAI,QAAQ;AAClD,QAAI,KAAK,OAAO;AACd,YAAM,SAAU,MAAM,KAAK,MAAM,IAAI,QAAQ;AAC7C,UAAI,WAAW,UAAa,WAAW,KAAM,QAAO;AAAA,IACtD;AAEA,UAAM,SAAS,MAAM,KAAK,oBAAoB,QAAQ;AACtD,QAAI,KAAK,OAAO;AACd,YAAM,KAAK,MAAM,IAAI,UAAU,QAAQ;AAAA,QACrC,KAAK;AAAA,QACL,MAAM,CAAC,oBAAoB,GAAG,kBAAkB,IAAI,QAAQ,EAAE;AAAA,MAChE,CAAC;AAAA,IACH;AACA,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,gBAAgB,OAAiF;AACrG,UAAM,WAAW,qBAAqB,KAAK;AAC3C,QAAI,CAAC,SAAU,QAAO;AACtB,QAAI,gBAAgB,EAAE,SAAS,QAAQ,EAAG,QAAO;AACjD,UAAM,MAAM,MAAM,KAAK,GAAG,QAAQ,eAAe;AAAA,MAC/C;AAAA,MACA,QAAQ,EAAE,KAAK,CAAC,UAAU,UAAU,EAAE;AAAA,IACxC,CAAU;AACV,QAAI,CAAC,IAAK,QAAO;AACjB,WAAO,EAAE,gBAAgB,IAAI,gBAAgB,QAAQ,IAAI,OAAO;AAAA,EAClE;AAAA,EAEA,MAAM,mBAAmB,gBAAoF;AAC3G,UAAM,WAAW,GAAG,wBAAwB,IAAI,cAAc;AAC9D,QAAI,KAAK,OAAO;AACd,YAAM,SAAU,MAAM,KAAK,MAAM,IAAI,QAAQ;AAC7C,UAAI,WAAW,UAAa,WAAW,KAAM,QAAO;AAAA,IACtD;AAEA,UAAM,MAAM,MAAM,KAAK,GAAG,QAAQ,eAAe;AAAA,MAC/C;AAAA,MACA,QAAQ;AAAA,IACV,CAAU;AACV,UAAM,SAAS,MAAM,EAAE,UAAU,IAAI,UAAU,QAAQ,IAAI,OAAO,IAAI;AAEtE,QAAI,KAAK,OAAO;AACd,YAAM,KAAK,MAAM,IAAI,UAAU,QAAQ;AAAA,QACrC,KAAK;AAAA,QACL,MAAM,CAAC,oBAAoB,GAAG,kBAAkB,QAAQ,cAAc,EAAE;AAAA,MAC1E,CAAC;AAAA,IACH;AACA,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,aAAuC;AAC3C,UAAM,OAAO,MAAM,KAAK,GAAG,KAAK,eAAe,EAAE,QAAQ,SAAS,CAAU;AAC5E,QAAI,KAAK,WAAW,EAAG,QAAO,CAAC;AAC/B,UAAM,SAAS,MAAM,KAAK,IAAI,IAAI,KAAK,IAAI,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;AACpE,UAAM,OAAO,MAAM,KAAK,GAAG,KAAK,cAAc,EAAE,IAAI,EAAE,KAAK,OAAO,EAAE,CAAU;AAC9E,UAAM,YAAY,IAAI,IAA2B,KAAK,IAAI,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,QAAQ,IAAI,CAAC,CAAC;AAExF,WAAO,KAAK,IAAmB,CAAC,SAAS;AAAA,MACvC,iBAAiB,IAAI;AAAA,MACrB,UAAU,IAAI;AAAA,MACd,UAAU,IAAI;AAAA,MACd,gBAAgB,IAAI;AAAA,MACpB,SAAS,UAAU,IAAI,IAAI,cAAc,KAAK;AAAA,MAC9C,QAAQ,IAAI;AAAA,IACd,EAAE;AAAA,EACJ;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,wBAAwB,WAAgE;AAC5F,UAAM,YAAY,WAAW,eAAe;AAC5C,UAAM,SAAS,IAAI,KAAK,KAAK,IAAI,IAAI,SAAS;AAC9C,WAAO,KAAK,GAAG;AAAA,MACb;AAAA,MACA;AAAA,QACE,QAAQ,EAAE,KAAK,CAAC,WAAW,YAAY,EAAE;AAAA,QACzC,KAAK,CAAC,EAAE,gBAAgB,KAAK,GAAG,EAAE,gBAAgB,EAAE,KAAK,OAAO,EAAE,CAAC;AAAA,MACrE;AAAA,MACA,EAAE,SAAS,EAAE,gBAAgB,MAAM,EAAE;AAAA,IACvC;AAAA,EACF;AAAA,EAEA,MAAM,eAAe,SAAiF;AACpG,UAAM,aAAa,SAAS,cAAc;AAC1C,UAAM,QAAQ,SAAS,aAAa;AACpC,WAAO,KAAK,GAAG;AAAA,MACb;AAAA,MACA;AAAA,QACE,KAAK;AAAA,UACH,EAAE,QAAQ,WAAW;AAAA,UACrB,EAAE,QAAQ,cAAc,eAAe,EAAE,KAAK,WAAW,EAAE;AAAA,QAC7D;AAAA,MACF;AAAA,MACA,EAAE,SAAS,EAAE,WAAW,MAAM,GAAG,MAAM;AAAA,IACzC;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,SAAS,OAA8C;AAC3D,UAAM,WAAW,kBAAkB,MAAM,QAAQ;AAEjD,QAAI,iBAAuC;AAC3C,QAAI,MAAM,kBAAkB;AAC1B,uBAAiB,MAAM,KAAK,GAAG,QAAQ,eAAe;AAAA,QACpD,IAAI,MAAM;AAAA,QACV,UAAU,MAAM;AAAA,MAClB,CAAU;AAAA,IACZ;AAEA,UAAM,SAAS,KAAK,GAAG,OAAO,eAAe;AAAA,MAC3C;AAAA,MACA,UAAU,MAAM;AAAA,MAChB,gBAAgB,MAAM;AAAA,MACtB,gBAAgB,kBAAkB;AAAA,MAClC,UAAU;AAAA,MACV,QAAQ;AAAA,MACR,eAAe;AAAA,MACf,WAAW,oBAAI,KAAK;AAAA,IACtB,CAAU;AACV,UAAM,KAAK,GAAG,QAAQ,MAAM,EAAE,MAAM;AAEpC,UAAM,0BAA0B,4CAA4C;AAAA,MAC1E,IAAI,OAAO;AAAA,MACX,UAAU,OAAO;AAAA,MACjB,gBAAgB,OAAO;AAAA,MACvB,UAAU,OAAO;AAAA,MACjB,QAAQ,OAAO;AAAA,IACjB,CAAU;AACV,UAAM,KAAK,mBAAmB,OAAO,UAAU,OAAO,cAAc;AAEpE,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,OAAO,IAAmC;AAC9C,UAAM,SAAS,MAAM,KAAK,GAAG,QAAQ,eAAe,EAAE,GAAG,CAAU;AACnE,QAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,iBAAiB,EAAE,YAAY;AAE5D,UAAM,gBAAgB,QAAQ,IAAI,8BAA8B;AAChE,UAAM,kBAAkB,QAAQ,IAAI,iCAAiC;AACrE,UAAM,MAAM,oBAAI,KAAK;AACrB,WAAO,iBAAiB;AAExB,UAAM,eAAe,MAAM,KAAK,mBAAmB,OAAO,UAAU;AAAA,MAClE;AAAA,MACA;AAAA,IACF,CAAC;AAED,QAAI,aAAa,IAAI;AACnB,aAAO,SAAS;AAChB,aAAO,aAAa;AACpB,aAAO,mBAAmB;AAC1B,aAAO,gBAAgB;AACvB,aAAO,mBAAmB;AAC1B,YAAM,KAAK,GAAG,QAAQ,MAAM,EAAE,MAAM;AACpC,YAAM,0BAA0B,6CAA6C;AAAA,QAC3E,IAAI,OAAO;AAAA,QACX,UAAU,OAAO;AAAA,QACjB,gBAAgB,OAAO;AAAA,QACvB,UAAU,OAAO;AAAA,MACnB,CAAU;AACV,YAAM,KAAK,mBAAmB,OAAO,UAAU,OAAO,cAAc;AACpE,aAAO,EAAE,eAAe,OAAO;AAAA,IACjC;AAEA,WAAO,SAAS;AAChB,WAAO,mBAAmB,aAAa;AACvC,UAAM,KAAK,GAAG,QAAQ,MAAM,EAAE,MAAM;AACpC,UAAM,0BAA0B,+CAA+C;AAAA,MAC7E,IAAI,OAAO;AAAA,MACX,UAAU,OAAO;AAAA,MACjB,gBAAgB,OAAO;AAAA,MACvB,UAAU,OAAO;AAAA,MACjB,QAAQ,aAAa;AAAA,MACrB,iBAAiB,aAAa,YAAY;AAAA,IAC5C,CAAU;AACV,UAAM,KAAK,mBAAmB,OAAO,UAAU,OAAO,cAAc;AACpE,WAAO,EAAE,eAAe,QAAQ,aAAa,aAAa,YAAY;AAAA,EACxE;AAAA,EAEA,MAAM,SAAS,IAAoC;AACjD,UAAM,SAAS,MAAM,KAAK,GAAG,QAAQ,eAAe,EAAE,GAAG,CAAU;AACnE,QAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,iBAAiB,EAAE,YAAY;AAC5D,QAAI,OAAO,WAAW,SAAU,QAAO;AACvC,QAAI,OAAO,WAAW,cAAc,OAAO,WAAW,cAAc;AAClE,YAAM,IAAI,MAAM,iBAAiB,EAAE,qCAAqC,OAAO,MAAM,EAAE;AAAA,IACzF;AAEA,WAAO,SAAS;AAChB,WAAO,gBAAgB;AACvB,WAAO,mBAAmB;AAC1B,UAAM,KAAK,GAAG,QAAQ,MAAM,EAAE,MAAM;AAEpC,UAAM,0BAA0B,8CAA8C;AAAA,MAC5E,IAAI,OAAO;AAAA,MACX,UAAU,OAAO;AAAA,MACjB,gBAAgB,OAAO;AAAA,MACvB,UAAU,OAAO;AAAA,IACnB,CAAU;AAEV,QAAI,OAAO,gBAAgB;AACzB,YAAM,WAAW,MAAM,KAAK,GAAG,QAAQ,eAAe;AAAA,QACpD,IAAK,OAAO,eAA6C;AAAA,MAC3D,CAAU;AACV,UAAI,UAAU;AACZ,cAAM,mBAAmB,SAAS;AAClC,cAAM,cAAc,SAAS;AAC7B,aAAK,GAAG,OAAO,QAAQ;AACvB,cAAM,KAAK,GAAG,MAAM;AACpB,cAAM,0BAA0B,6CAA6C;AAAA,UAC3E,IAAI,OAAO;AAAA,UACX,UAAU,OAAO;AAAA,UACjB,gBAAgB,OAAO;AAAA,UACvB,UAAU,OAAO;AAAA,UACjB,kBAAkB,SAAS;AAAA,UAC3B;AAAA,QACF,CAAU;AACV,cAAM,0BAA0B,4CAA4C;AAAA,UAC1E,IAAI,SAAS;AAAA,UACb,UAAU;AAAA,UACV,gBAAgB;AAAA,UAChB,UAAU,SAAS;AAAA,QACrB,CAAU;AACV,cAAM,KAAK,mBAAmB,kBAAkB,WAAW;AAAA,MAC7D;AAAA,IACF;AAEA,UAAM,KAAK,mBAAmB,OAAO,UAAU,OAAO,cAAc;AACpE,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,OAAO,IAAY,OAA8C;AACrE,UAAM,QAAiC,EAAE,GAAG;AAC5C,QAAI,OAAO,SAAU,OAAM,WAAW,MAAM;AAC5C,UAAM,SAAS,MAAM,KAAK,GAAG,QAAQ,eAAe,KAAc;AAClE,QAAI,CAAC,OAAQ;AAEb,UAAM,WAAW,OAAO;AACxB,UAAM,iBAAiB,OAAO;AAC9B,UAAM,WAAW,OAAO;AAExB,SAAK,GAAG,OAAO,MAAM;AACrB,UAAM,KAAK,GAAG,MAAM;AAEpB,UAAM,0BAA0B,4CAA4C;AAAA,MAC1E;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF,CAAU;AACV,UAAM,KAAK,mBAAmB,UAAU,cAAc;AAAA,EACxD;AAAA,EAEA,MAAM,YAAY,IAAoC;AACpD,UAAM,SAAS,MAAM,KAAK,GAAG,QAAQ,eAAe,EAAE,GAAG,CAAU;AACnE,QAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,iBAAiB,EAAE,YAAY;AAC5D,QAAI,OAAO,WAAW,SAAU,QAAO;AACvC,QAAI,OAAO,WAAW,cAAc,OAAO,WAAW,cAAc;AAClE,YAAM,IAAI,MAAM,iBAAiB,EAAE,wCAAwC,OAAO,MAAM,EAAE;AAAA,IAC5F;AAEA,QAAI,aAA4B;AAChC,aAAS,UAAU,GAAG,UAAU,iCAAiC,QAAQ,WAAW;AAClF,YAAM,SAAS,MAAM,KAAK,gBAAgB,OAAO,UAAU,2BAA2B;AACtF,UAAI,OAAO,IAAI;AACb,eAAO,KAAK,SAAS,OAAO,EAAE;AAAA,MAChC;AACA,mBAAa,OAAO,UAAU;AAC9B,UAAI,UAAU,IAAI,iCAAiC,QAAQ;AACzD,cAAM,MAAM,iCAAiC,OAAO,CAAC;AAAA,MACvD;AAAA,IACF;AAEA,WAAO,SAAS;AAChB,WAAO,mBAAmB,cAAc;AACxC,WAAO,iBAAiB,OAAO,iBAAiB,KAAK;AACrD,UAAM,KAAK,GAAG,QAAQ,MAAM,EAAE,MAAM;AAEpC,UAAM,0BAA0B,+CAA+C;AAAA,MAC7E,IAAI,OAAO;AAAA,MACX,UAAU,OAAO;AAAA,MACjB,gBAAgB,OAAO;AAAA,MACvB,UAAU,OAAO;AAAA,MACjB,QAAQ,OAAO;AAAA,MACf,YAAY,OAAO;AAAA,IACrB,CAAU;AACV,UAAM,KAAK,mBAAmB,OAAO,UAAU,OAAO,cAAc;AACpE,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAMA,MAAc,oBAAoB,UAAiD;AACjF,UAAM,MAAM,MAAM,KAAK,GAAG,QAAQ,eAAe,EAAE,UAAU,QAAQ,SAAS,CAAU;AACxF,QAAI,CAAC,IAAK,QAAO;AACjB,UAAM,MAAM,MAAM,KAAK,GAAG,QAAQ,cAAc,EAAE,IAAI,IAAI,eAAe,CAAU;AACnF,WAAO;AAAA,MACL,iBAAiB,IAAI;AAAA,MACrB,UAAU,IAAI;AAAA,MACd,UAAU,IAAI;AAAA,MACd,gBAAgB,IAAI;AAAA,MACpB,SAAS,KAAK,QAAQ;AAAA,MACtB,QAAQ,IAAI;AAAA,IACd;AAAA,EACF;AAAA,EAEA,MAAc,mBAAmB,UAAkB,gBAA8C;AAC/F,QAAI,CAAC,KAAK,MAAO;AACjB,UAAM,OAAO,CAAC,oBAAoB,GAAG,kBAAkB,IAAI,QAAQ,EAAE;AACrE,QAAI,eAAgB,MAAK,KAAK,GAAG,kBAAkB,QAAQ,cAAc,EAAE;AAC3E,QAAI;AACF,YAAM,KAAK,MAAM,aAAa,IAAI;AAAA,IACpC,QAAQ;AAAA,IAER;AAAA,EACF;AAAA,EAEA,MAAc,mBACZ,UACA,UAQA;AACA,UAAM,kBAAqD,CAAC;AAC5D,UAAM,WAAW,CAAC,eAAyD;AAAA,MACzE,qBAAqB,SAAS;AAAA,MAC9B,uBAAuB,SAAS;AAAA,MAChC;AAAA,MACA,YAAY,WAAW,cAAc;AAAA,MACrC,gBAAgB,WAAW;AAAA,IAC7B;AAGA,QAAI,eAAyB,CAAC;AAC9B,QAAI;AACF,qBAAe,MAAM,KAAK,IAAI,aAAa,QAAQ;AAAA,IACrD,SAAS,KAAK;AACZ,aAAO;AAAA,QACL,IAAI;AAAA,QACJ,QAAQ,6BAA8B,IAAc,OAAO;AAAA,QAC3D,aAAa,SAAS;AAAA,UACpB,YAAY;AAAA,QACd,CAAC;AAAA,MACH;AAAA,IACF;AACA,eAAW,SAAS,aAAc,iBAAgB,KAAK,EAAE,MAAM,SAAS,OAAO,MAAM,CAAC;AAEtF,QAAI,SAAS,iBAAiB,aAAa,SAAS,GAAG;AACrD,YAAM,gBAAgB,qBAAqB,SAAS,aAAa,KAAK,SAAS,cAAc,YAAY;AACzG,YAAM,QAAQ,aAAa,KAAK,CAAC,OAAO,qBAAqB,CAAC,KAAK,EAAE,YAAY,OAAO,aAAa;AACrG,UAAI,OAAO;AACT,eAAO;AAAA,UACL,IAAI;AAAA,UACJ,QAAQ;AAAA,UACR,aAAa,SAAS;AAAA,YACpB,YAAY;AAAA,UACd,CAAC;AAAA,QACH;AAAA,MACF;AACA,aAAO;AAAA,QACL,IAAI;AAAA,QACJ,QAAQ,mBAAmB,aAAa,KAAK,IAAI,CAAC,eAAe,SAAS,aAAa;AAAA,QACvF,aAAa,SAAS;AAAA,UACpB,YAAY,wCAAwC,SAAS,aAAa;AAAA,QAC5E,CAAC;AAAA,MACH;AAAA,IACF;AAGA,QAAI,WAAqB,CAAC;AAC1B,QAAI;AACF,iBAAW,MAAM,KAAK,IAAI,SAAS,QAAQ;AAAA,IAC7C,SAAS,KAAK;AACZ,aAAO;AAAA,QACL,IAAI;AAAA,QACJ,QAAQ,yBAA0B,IAAc,OAAO;AAAA,QACvD,aAAa,SAAS,EAAE,YAAY,8DAA8D,CAAC;AAAA,MACrG;AAAA,IACF;AACA,eAAW,KAAK,UAAU;AACxB,YAAM,QAAQ,YAAY,CAAC;AAC3B,sBAAgB,KAAK,EAAE,MAAM,KAAK,OAAO,GAAG,GAAI,QAAQ,EAAE,MAAM,IAAI,CAAC,EAAG,CAAC;AAAA,IAC3E;AAEA,QAAI,SAAS,WAAW,GAAG;AACzB,aAAO;AAAA,QACL,IAAI;AAAA,QACJ,QAAQ,kCAAkC,QAAQ;AAAA,QAClD,aAAa,SAAS;AAAA,UACpB,YAAY,SAAS,kBACjB,mDAAmD,SAAS,aAAa,qDAAqD,SAAS,eAAe,+CACtJ,kCAAkC,SAAS,aAAa;AAAA,QAC9D,CAAC;AAAA,MACH;AAAA,IACF;AAEA,QAAI,SAAS,mBAAmB,SAAS,SAAS,SAAS,eAAe,GAAG;AAC3E,aAAO;AAAA,QACL,IAAI;AAAA,QACJ,QAAQ;AAAA,QACR,aAAa,SAAS,EAAE,YAAY,wCAAwC,CAAC;AAAA,MAC/E;AAAA,IACF;AAGA,UAAM,iBAAiB,SAAS,OAAO,CAAC,OAAO,oBAAoB,EAAE,CAAC;AACtE,QAAI,eAAe,SAAS,GAAG;AAC7B,YAAM,QAAQ,MAAM,KAAK,gBAAgB,UAAU,2BAA2B;AAC9E,UAAI,MAAM,MAAM,MAAM,qBAAqB;AACzC,eAAO;AAAA,UACL,IAAI;AAAA,UACJ,QAAQ;AAAA,UACR,aAAa,SAAS;AAAA,YACpB,gBAAgB,EAAE,WAAW,MAAM,qBAAqB,KAAK;AAAA,YAC7D,YAAY;AAAA,UACd,CAAC;AAAA,QACH;AAAA,MACF;AACA,aAAO;AAAA,QACL,IAAI;AAAA,QACJ,QAAQ;AAAA,QACR,aAAa,SAAS;AAAA,UACpB,gBAAgB,EAAE,WAAW,MAAM,qBAAqB,MAAM;AAAA,UAC9D,YAAY,SAAS,gBACjB,0EAAqE,SAAS,aAAa,mDAAmD,SAAS,aAAa,MACpK;AAAA,QACN,CAAC;AAAA,MACH;AAAA,IACF;AAEA,WAAO;AAAA,MACL,IAAI;AAAA,MACJ,QAAQ,SAAS,kBACb,sBAAsB,SAAS,KAAK,IAAI,CAAC,eAAe,SAAS,eAAe,KAChF,sBAAsB,SAAS,KAAK,IAAI,CAAC;AAAA,MAC7C,aAAa,SAAS;AAAA,QACpB,YAAY,SAAS,kBACjB,oCAAoC,SAAS,eAAe,MAC5D,2FAA2F,QAAQ,iCAAiC,SAAS,aAAa;AAAA,MAChK,CAAC;AAAA,IACH;AAAA,EACF;AACF;AAGO,MAAM,cAAc;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;",
4
+ "sourcesContent": ["import { Resolver, promises as dnsPromises } from 'node:dns'\nimport { request as httpsRequest } from 'node:https'\nimport { setTimeout as delay } from 'node:timers/promises'\nimport { EntityManager } from '@mikro-orm/postgresql'\nimport {\n DomainMapping,\n type DomainStatus,\n} from '@open-mercato/core/modules/customer_accounts/data/entities'\nimport { Organization } from '@open-mercato/core/modules/directory/data/entities'\nimport { emitCustomerAccountsEvent } from '@open-mercato/core/modules/customer_accounts/events'\nimport { normalizeHostname, tryNormalizeHostname } from '@open-mercato/core/modules/customer_accounts/lib/hostname'\nimport { findOrganizationInTenant } from '@open-mercato/core/modules/customer_accounts/lib/organizationLookup'\nimport { platformDomains } from '@open-mercato/core/modules/customer_accounts/lib/platformDomains'\nimport { detectProxy, isInKnownProxyRange } from '@open-mercato/core/modules/customer_accounts/lib/proxyRanges'\n\nexport class DomainMappingOrgScopeError extends Error {\n constructor(organizationId: string) {\n super(`[internal] organizationId ${organizationId} does not belong to the caller's tenant`)\n this.name = 'DomainMappingOrgScopeError'\n }\n}\n\nconst DOMAIN_ROUTING_TAG = 'domain_routing'\nconst RESOLVE_KEY_PREFIX = 'domain_routing:resolve'\nconst ACTIVE_BY_ORG_KEY_PREFIX = 'domain_routing:active-by-org'\nconst RESOLVE_TTL_MS = 5 * 60_000\nconst TLS_HEALTH_CHECK_TIMEOUT_MS = 10_000\nconst TLS_HEALTH_CHECK_RETRY_DELAYS_MS = [1_000, 4_000, 16_000]\nconst DEFAULT_DNS_RECHECK_THRESHOLD_MS = 5 * 60_000\nconst DEFAULT_TLS_MAX_RETRIES = 6\n\nexport type ResolveResult = {\n domainMappingId: string\n hostname: string\n tenantId: string\n organizationId: string\n orgSlug: string | null\n status: DomainStatus\n}\n\nexport type DnsDiagnostics = {\n expectedCnameTarget: string\n expectedARecordTarget: string | null\n detectedRecords: Array<{ type: 'CNAME' | 'A'; value: string; proxy?: string }>\n reverseResolve?: { attempted: boolean; originHeaderPresent: boolean }\n suggestion: string\n}\n\nexport type VerifyResult = {\n domainMapping: DomainMapping\n diagnostics?: DnsDiagnostics\n}\n\nexport type RegisterInput = {\n hostname: string\n organizationId: string\n tenantId: string\n replacesDomainId?: string\n}\n\ntype CacheService = {\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\ntype DnsResolverContract = {\n resolveCname(hostname: string): Promise<string[]>\n resolve4(hostname: string): Promise<string[]>\n}\n\ntype HealthCheckContract = (hostname: string, timeoutMs: number) => Promise<{\n ok: boolean\n originHeaderPresent: boolean\n reason?: string\n}>\n\nconst defaultDnsResolver: DnsResolverContract = {\n async resolveCname(hostname) {\n try {\n return await dnsPromises.resolveCname(hostname)\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code\n if (code === 'ENODATA' || code === 'ENOTFOUND') return []\n throw err\n }\n },\n async resolve4(hostname) {\n try {\n return await dnsPromises.resolve4(hostname)\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code\n if (code === 'ENODATA' || code === 'ENOTFOUND') return []\n throw err\n }\n },\n}\n\nconst defaultHealthCheck: HealthCheckContract = (hostname, timeoutMs) =>\n new Promise((resolve) => {\n const headerName = (process.env.CUSTOMER_DOMAIN_ORIGIN_HEADER ?? 'X-Open-Mercato-Origin').toLowerCase()\n const req = httpsRequest(\n {\n host: hostname,\n port: 443,\n path: '/api/customer_accounts/domain-check',\n method: 'GET',\n headers: { 'X-Domain-Check-Secret': process.env.DOMAIN_CHECK_SECRET ?? '' },\n timeout: timeoutMs,\n },\n (res) => {\n const headerValue = res.headers[headerName]\n const originHeader = Array.isArray(headerValue) ? headerValue[0] : headerValue\n const originHeaderPresent = typeof originHeader === 'string' && originHeader === '1'\n const status = res.statusCode ?? 0\n res.resume() // discard body\n resolve({\n ok: status >= 200 && status < 400,\n originHeaderPresent,\n reason: status >= 200 && status < 400 ? undefined : `HTTP ${status}`,\n })\n },\n )\n req.on('timeout', () => {\n req.destroy(new Error('TLS health check timed out'))\n })\n req.on('error', (err) => {\n resolve({ ok: false, originHeaderPresent: false, reason: (err as Error).message })\n })\n req.end()\n })\n\nexport class DomainMappingService {\n private cache: CacheService | null\n private dns: DnsResolverContract\n private healthCheckImpl: HealthCheckContract\n\n constructor(\n private em: EntityManager,\n deps?: {\n cacheService?: CacheService\n dnsResolver?: DnsResolverContract\n healthCheck?: HealthCheckContract\n },\n ) {\n this.cache = deps?.cacheService ?? null\n this.dns = deps?.dnsResolver ?? defaultDnsResolver\n this.healthCheckImpl = deps?.healthCheck ?? defaultHealthCheck\n }\n\n // -------------------------------------------------------------------------\n // Read paths\n // -------------------------------------------------------------------------\n\n async findById(id: string, scope?: { tenantId?: string }): Promise<DomainMapping | null> {\n const where: Record<string, unknown> = { id }\n if (scope?.tenantId) where.tenantId = scope.tenantId\n return this.em.findOne(DomainMapping, where as never)\n }\n\n async findByOrganization(\n organizationId: string,\n scope?: { tenantId?: string },\n ): Promise<DomainMapping[]> {\n const where: Record<string, unknown> = { organizationId }\n if (scope?.tenantId) where.tenantId = scope.tenantId\n return this.em.find(DomainMapping, where as never, { orderBy: { createdAt: 'asc' } })\n }\n\n async resolveByHostname(input: string): Promise<ResolveResult | null> {\n const hostname = tryNormalizeHostname(input)\n if (!hostname) return null\n if (platformDomains().includes(hostname)) return null\n\n const cacheKey = `${RESOLVE_KEY_PREFIX}:${hostname}`\n if (this.cache) {\n const cached = (await this.cache.get(cacheKey)) as ResolveResult | null | undefined\n if (cached !== undefined && cached !== null) return cached\n }\n\n const result = await this.lookupResolveResult(hostname)\n if (this.cache) {\n await this.cache.set(cacheKey, result, {\n ttl: RESOLVE_TTL_MS,\n tags: [DOMAIN_ROUTING_TAG, `${DOMAIN_ROUTING_TAG}:${hostname}`],\n })\n }\n return result\n }\n\n async isAllowedForTls(input: string): Promise<{ organizationId: string; status: DomainStatus } | null> {\n const hostname = tryNormalizeHostname(input)\n if (!hostname) return null\n if (platformDomains().includes(hostname)) return null\n const row = await this.em.findOne(DomainMapping, {\n hostname,\n status: { $in: ['active', 'verified'] },\n } as never)\n if (!row) return null\n return { organizationId: row.organizationId, status: row.status }\n }\n\n async resolveActiveByOrg(organizationId: string): Promise<{ hostname: string; status: DomainStatus } | null> {\n const cacheKey = `${ACTIVE_BY_ORG_KEY_PREFIX}:${organizationId}`\n if (this.cache) {\n const cached = (await this.cache.get(cacheKey)) as { hostname: string; status: DomainStatus } | null | undefined\n if (cached !== undefined && cached !== null) return cached\n }\n\n const row = await this.em.findOne(DomainMapping, {\n organizationId,\n status: 'active',\n } as never)\n const result = row ? { hostname: row.hostname, status: row.status } : null\n\n if (this.cache) {\n await this.cache.set(cacheKey, result, {\n ttl: RESOLVE_TTL_MS,\n tags: [DOMAIN_ROUTING_TAG, `${DOMAIN_ROUTING_TAG}:org:${organizationId}`],\n })\n }\n return result\n }\n\n async resolveAll(): Promise<ResolveResult[]> {\n const rows = await this.em.find(DomainMapping, { status: 'active' } as never)\n if (rows.length === 0) return []\n const orgIds = Array.from(new Set(rows.map((r) => r.organizationId)))\n const orgs = await this.em.find(Organization, { id: { $in: orgIds } } as never)\n const slugByOrg = new Map<string, string | null>(orgs.map((o) => [o.id, o.slug ?? null]))\n\n return rows.map<ResolveResult>((row) => ({\n domainMappingId: row.id,\n hostname: row.hostname,\n tenantId: row.tenantId,\n organizationId: row.organizationId,\n orgSlug: slugByOrg.get(row.organizationId) ?? null,\n status: row.status,\n }))\n }\n\n // -------------------------------------------------------------------------\n // Worker queries\n // -------------------------------------------------------------------------\n\n async findPendingVerification(threshold?: { olderThanMs?: number }): Promise<DomainMapping[]> {\n const olderThan = threshold?.olderThanMs ?? DEFAULT_DNS_RECHECK_THRESHOLD_MS\n const cutoff = new Date(Date.now() - olderThan)\n return this.em.find(\n DomainMapping,\n {\n status: { $in: ['pending', 'dns_failed'] },\n $or: [{ lastDnsCheckAt: null }, { lastDnsCheckAt: { $lt: cutoff } }],\n } as never,\n { orderBy: { lastDnsCheckAt: 'asc' } },\n )\n }\n\n async findPendingTls(options?: { maxRetries?: number; batchSize?: number }): Promise<DomainMapping[]> {\n const maxRetries = options?.maxRetries ?? DEFAULT_TLS_MAX_RETRIES\n const limit = options?.batchSize ?? 50\n return this.em.find(\n DomainMapping,\n {\n $or: [\n { status: 'verified' },\n { status: 'tls_failed', tlsRetryCount: { $lt: maxRetries } },\n ],\n } as never,\n { orderBy: { updatedAt: 'asc' }, limit },\n )\n }\n\n // -------------------------------------------------------------------------\n // Write paths\n // -------------------------------------------------------------------------\n\n async register(input: RegisterInput): Promise<DomainMapping> {\n const hostname = normalizeHostname(input.hostname)\n const organization = await findOrganizationInTenant(this.em, input.organizationId, input.tenantId)\n if (!organization) throw new DomainMappingOrgScopeError(input.organizationId)\n\n let replacesDomain: DomainMapping | null = null\n if (input.replacesDomainId) {\n replacesDomain = await this.em.findOne(DomainMapping, {\n id: input.replacesDomainId,\n tenantId: input.tenantId,\n } as never)\n }\n\n const entity = this.em.create(DomainMapping, {\n hostname,\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n replacesDomain: replacesDomain ?? null,\n provider: 'traefik',\n status: 'pending',\n tlsRetryCount: 0,\n createdAt: new Date(),\n } as never)\n await this.em.persist(entity).flush()\n\n await emitCustomerAccountsEvent('customer_accounts.domain_mapping.created', {\n id: entity.id,\n hostname: entity.hostname,\n organizationId: entity.organizationId,\n tenantId: entity.tenantId,\n status: entity.status,\n } as never)\n await this.invalidateCacheFor(entity.hostname, entity.organizationId)\n\n return entity\n }\n\n async verify(id: string): Promise<VerifyResult> {\n const entity = await this.em.findOne(DomainMapping, { id } as never)\n if (!entity) throw new Error(`DomainMapping ${id} not found`)\n\n const expectedCname = process.env.CUSTOM_DOMAIN_CNAME_TARGET ?? ''\n const expectedARecord = process.env.CUSTOM_DOMAIN_A_RECORD_TARGET ?? null\n const now = new Date()\n entity.lastDnsCheckAt = now\n\n const verification = await this.runDnsVerification(entity.hostname, {\n expectedCname,\n expectedARecord,\n })\n\n if (verification.ok) {\n entity.status = 'verified'\n entity.verifiedAt = now\n entity.dnsFailureReason = null\n entity.tlsRetryCount = 0\n entity.tlsFailureReason = null\n await this.em.persist(entity).flush()\n await emitCustomerAccountsEvent('customer_accounts.domain_mapping.verified', {\n id: entity.id,\n hostname: entity.hostname,\n organizationId: entity.organizationId,\n tenantId: entity.tenantId,\n } as never)\n await this.invalidateCacheFor(entity.hostname, entity.organizationId)\n return { domainMapping: entity }\n }\n\n entity.status = 'dns_failed'\n entity.dnsFailureReason = verification.reason\n await this.em.persist(entity).flush()\n await emitCustomerAccountsEvent('customer_accounts.domain_mapping.dns_failed', {\n id: entity.id,\n hostname: entity.hostname,\n organizationId: entity.organizationId,\n tenantId: entity.tenantId,\n reason: verification.reason,\n detectedRecords: verification.diagnostics.detectedRecords,\n } as never)\n await this.invalidateCacheFor(entity.hostname, entity.organizationId)\n return { domainMapping: entity, diagnostics: verification.diagnostics }\n }\n\n async activate(id: string): Promise<DomainMapping> {\n const entity = await this.em.findOne(DomainMapping, { id } as never)\n if (!entity) throw new Error(`DomainMapping ${id} not found`)\n if (entity.status === 'active') return entity\n if (entity.status !== 'verified' && entity.status !== 'tls_failed') {\n throw new Error(`DomainMapping ${id} cannot transition to active from ${entity.status}`)\n }\n\n entity.status = 'active'\n entity.tlsRetryCount = 0\n entity.tlsFailureReason = null\n await this.em.persist(entity).flush()\n\n await emitCustomerAccountsEvent('customer_accounts.domain_mapping.activated', {\n id: entity.id,\n hostname: entity.hostname,\n organizationId: entity.organizationId,\n tenantId: entity.tenantId,\n } as never)\n\n if (entity.replacesDomain) {\n const replaced = await this.em.findOne(DomainMapping, {\n id: (entity.replacesDomain as unknown as { id: string }).id,\n } as never)\n if (replaced) {\n const replacedHostname = replaced.hostname\n const replacedOrg = replaced.organizationId\n this.em.remove(replaced)\n await this.em.flush()\n await emitCustomerAccountsEvent('customer_accounts.domain_mapping.replaced', {\n id: entity.id,\n hostname: entity.hostname,\n organizationId: entity.organizationId,\n tenantId: entity.tenantId,\n replacedDomainId: replaced.id,\n replacedHostname,\n } as never)\n await emitCustomerAccountsEvent('customer_accounts.domain_mapping.deleted', {\n id: replaced.id,\n hostname: replacedHostname,\n organizationId: replacedOrg,\n tenantId: replaced.tenantId,\n } as never)\n await this.invalidateCacheFor(replacedHostname, replacedOrg)\n }\n }\n\n await this.invalidateCacheFor(entity.hostname, entity.organizationId)\n return entity\n }\n\n async remove(id: string, scope?: { tenantId?: string }): Promise<void> {\n const where: Record<string, unknown> = { id }\n if (scope?.tenantId) where.tenantId = scope.tenantId\n const entity = await this.em.findOne(DomainMapping, where as never)\n if (!entity) return\n\n const hostname = entity.hostname\n const organizationId = entity.organizationId\n const tenantId = entity.tenantId\n\n this.em.remove(entity)\n await this.em.flush()\n\n await emitCustomerAccountsEvent('customer_accounts.domain_mapping.deleted', {\n id,\n hostname,\n organizationId,\n tenantId,\n } as never)\n await this.invalidateCacheFor(hostname, organizationId)\n }\n\n async healthCheck(id: string): Promise<DomainMapping> {\n const entity = await this.em.findOne(DomainMapping, { id } as never)\n if (!entity) throw new Error(`DomainMapping ${id} not found`)\n if (entity.status === 'active') return entity\n if (entity.status !== 'verified' && entity.status !== 'tls_failed') {\n throw new Error(`DomainMapping ${id} cannot run health check from status ${entity.status}`)\n }\n\n let lastReason: string | null = null\n for (let attempt = 0; attempt < TLS_HEALTH_CHECK_RETRY_DELAYS_MS.length; attempt++) {\n const result = await this.healthCheckImpl(entity.hostname, TLS_HEALTH_CHECK_TIMEOUT_MS)\n if (result.ok) {\n return this.activate(entity.id)\n }\n lastReason = result.reason ?? 'TLS health check failed'\n if (attempt + 1 < TLS_HEALTH_CHECK_RETRY_DELAYS_MS.length) {\n await delay(TLS_HEALTH_CHECK_RETRY_DELAYS_MS[attempt])\n }\n }\n\n entity.status = 'tls_failed'\n entity.tlsFailureReason = lastReason ?? 'TLS health check failed'\n entity.tlsRetryCount = (entity.tlsRetryCount ?? 0) + 1\n await this.em.persist(entity).flush()\n\n await emitCustomerAccountsEvent('customer_accounts.domain_mapping.tls_failed', {\n id: entity.id,\n hostname: entity.hostname,\n organizationId: entity.organizationId,\n tenantId: entity.tenantId,\n reason: entity.tlsFailureReason,\n retryCount: entity.tlsRetryCount,\n } as never)\n await this.invalidateCacheFor(entity.hostname, entity.organizationId)\n return entity\n }\n\n // -------------------------------------------------------------------------\n // Internals\n // -------------------------------------------------------------------------\n\n private async lookupResolveResult(hostname: string): Promise<ResolveResult | null> {\n const row = await this.em.findOne(DomainMapping, { hostname, status: 'active' } as never)\n if (!row) return null\n const org = await this.em.findOne(Organization, { id: row.organizationId } as never)\n return {\n domainMappingId: row.id,\n hostname: row.hostname,\n tenantId: row.tenantId,\n organizationId: row.organizationId,\n orgSlug: org?.slug ?? null,\n status: row.status,\n }\n }\n\n private async invalidateCacheFor(hostname: string, organizationId: string | null): Promise<void> {\n if (!this.cache) return\n const tags = [DOMAIN_ROUTING_TAG, `${DOMAIN_ROUTING_TAG}:${hostname}`]\n if (organizationId) tags.push(`${DOMAIN_ROUTING_TAG}:org:${organizationId}`)\n try {\n await this.cache.deleteByTags(tags)\n } catch {\n // Cache invalidation is best-effort \u2014 TTL backstop ensures eventual consistency.\n }\n }\n\n private async runDnsVerification(\n hostname: string,\n expected: { expectedCname: string; expectedARecord: string | null },\n ): Promise<\n | {\n ok: true\n method: 'cname' | 'a-record' | 'reverse-resolve'\n diagnostics: DnsDiagnostics\n }\n | { ok: false; reason: string; diagnostics: DnsDiagnostics }\n > {\n const detectedRecords: DnsDiagnostics['detectedRecords'] = []\n const baseDiag = (overrides?: Partial<DnsDiagnostics>): DnsDiagnostics => ({\n expectedCnameTarget: expected.expectedCname,\n expectedARecordTarget: expected.expectedARecord,\n detectedRecords,\n suggestion: overrides?.suggestion ?? '',\n reverseResolve: overrides?.reverseResolve,\n })\n\n // Phase 1: CNAME\n let cnameRecords: string[] = []\n try {\n cnameRecords = await this.dns.resolveCname(hostname)\n } catch (err) {\n return {\n ok: false,\n reason: `DNS lookup error (CNAME): ${(err as Error).message}`,\n diagnostics: baseDiag({\n suggestion: 'DNS lookup failed unexpectedly. Try again in a few minutes.',\n }),\n }\n }\n for (const cname of cnameRecords) detectedRecords.push({ type: 'CNAME', value: cname })\n\n if (expected.expectedCname && cnameRecords.length > 0) {\n const expectedCname = tryNormalizeHostname(expected.expectedCname) ?? expected.expectedCname.toLowerCase()\n const match = cnameRecords.some((c) => (tryNormalizeHostname(c) ?? c.toLowerCase()) === expectedCname)\n if (match) {\n return {\n ok: true,\n method: 'cname',\n diagnostics: baseDiag({\n suggestion: 'CNAME record matches the expected target.',\n }),\n }\n }\n return {\n ok: false,\n reason: `CNAME points to ${cnameRecords.join(', ')} instead of ${expected.expectedCname}`,\n diagnostics: baseDiag({\n suggestion: `Update your CNAME record to point to ${expected.expectedCname}.`,\n }),\n }\n }\n\n // Phase 2: A record\n let aRecords: string[] = []\n try {\n aRecords = await this.dns.resolve4(hostname)\n } catch (err) {\n return {\n ok: false,\n reason: `DNS lookup error (A): ${(err as Error).message}`,\n diagnostics: baseDiag({ suggestion: 'DNS lookup failed unexpectedly. Try again in a few minutes.' }),\n }\n }\n for (const a of aRecords) {\n const proxy = detectProxy(a)\n detectedRecords.push({ type: 'A', value: a, ...(proxy ? { proxy } : {}) })\n }\n\n if (aRecords.length === 0) {\n return {\n ok: false,\n reason: `No CNAME or A record found for ${hostname}`,\n diagnostics: baseDiag({\n suggestion: expected.expectedARecord\n ? `For a subdomain, add a CNAME record pointing to ${expected.expectedCname}. For an apex domain, add an A record pointing to ${expected.expectedARecord}. DNS propagation can take up to 48 hours.`\n : `Add a CNAME record pointing to ${expected.expectedCname}. DNS propagation can take up to 48 hours.`,\n }),\n }\n }\n\n if (expected.expectedARecord && aRecords.includes(expected.expectedARecord)) {\n return {\n ok: true,\n method: 'a-record',\n diagnostics: baseDiag({ suggestion: 'A record matches the expected target.' }),\n }\n }\n\n // Phase 3: reverse-resolve through proxy\n const proxiedRecords = aRecords.filter((ip) => isInKnownProxyRange(ip))\n if (proxiedRecords.length > 0) {\n const probe = await this.healthCheckImpl(hostname, TLS_HEALTH_CHECK_TIMEOUT_MS)\n if (probe.ok && probe.originHeaderPresent) {\n return {\n ok: true,\n method: 'reverse-resolve',\n diagnostics: baseDiag({\n reverseResolve: { attempted: true, originHeaderPresent: true },\n suggestion: 'Domain is proxied \u2014 reverse-resolve confirmed traffic reaches our origin.',\n }),\n }\n }\n return {\n ok: false,\n reason: 'A record points to a known proxy IP, but reverse-resolve over HTTPS did not reach our server',\n diagnostics: baseDiag({\n reverseResolve: { attempted: true, originHeaderPresent: false },\n suggestion: expected.expectedCname\n ? `Your DNS uses a proxy. Either disable the proxy and add a CNAME \u2192 ${expected.expectedCname}, or configure your proxy to forward traffic to ${expected.expectedCname}.`\n : 'Disable the DNS proxy or configure it to forward traffic to our platform.',\n }),\n }\n }\n\n return {\n ok: false,\n reason: expected.expectedARecord\n ? `A record points to ${aRecords.join(', ')} instead of ${expected.expectedARecord}`\n : `A record points to ${aRecords.join(', ')} but apex-domain registration is not enabled on this deployment`,\n diagnostics: baseDiag({\n suggestion: expected.expectedARecord\n ? `Update your A record to point to ${expected.expectedARecord}.`\n : `Apex-domain registration is not enabled on this deployment. Use a subdomain (e.g., shop.${hostname}) and add a CNAME pointing to ${expected.expectedCname}.`,\n }),\n }\n }\n}\n\n// Re-exported for tests; allow injection of fakes.\nexport const __testing__ = {\n DEFAULT_DNS_RECHECK_THRESHOLD_MS,\n DEFAULT_TLS_MAX_RETRIES,\n TLS_HEALTH_CHECK_RETRY_DELAYS_MS,\n RESOLVE_TTL_MS,\n DOMAIN_ROUTING_TAG,\n defaultHealthCheck,\n defaultDnsResolver,\n Resolver,\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -21,7 +21,6 @@ import { documentUpdateSchema } from "../../commands/documents.js";
21
21
  import { buildIlikeTerm } from "@open-mercato/shared/lib/db/buildIlikeTerm";
22
22
  import { parseBooleanToken } from "@open-mercato/shared/lib/boolean";
23
23
  import { parseIdsParam } from "@open-mercato/shared/lib/crud/ids";
24
- import { recalculateOrderTotalsForDisplay } from "../../commands/returns.js";
25
24
  import { parseDecryptedFieldValue } from "@open-mercato/shared/lib/encryption/tenantDataEncryptionService";
26
25
  const rawBodySchema = z.object({}).passthrough();
27
26
  const normalizeJsonRecord = (value) => {
@@ -497,40 +496,6 @@ function buildDocumentCrudOptions(binding) {
497
496
  afterList: async (payload, ctx) => {
498
497
  await attachTags(payload, { ...ctx, bindingKind: binding.kind });
499
498
  await attachChannelNames(payload, ctx);
500
- if (binding.kind === "order" && Array.isArray(payload?.items) && payload.items.length === 1) {
501
- const item = payload.items[0];
502
- const orderId = typeof item?.id === "string" ? item.id : null;
503
- const tenantId = typeof item?.tenantId === "string" ? item.tenantId : ctx?.auth?.tenantId ?? null;
504
- const organizationId = typeof item?.organizationId === "string" ? item.organizationId : ctx?.selectedOrganizationId ?? ctx?.auth?.orgId ?? null;
505
- if (orderId && tenantId && organizationId) {
506
- const requestEm = ctx?.container?.resolve?.("em");
507
- const em = requestEm?.fork();
508
- if (em) {
509
- const totals = await recalculateOrderTotalsForDisplay(
510
- em,
511
- ctx.container,
512
- orderId,
513
- { tenantId, organizationId }
514
- );
515
- if (totals) {
516
- Object.assign(item, {
517
- subtotalNetAmount: totals.subtotalNetAmount,
518
- subtotalGrossAmount: totals.subtotalGrossAmount,
519
- discountTotalAmount: totals.discountTotalAmount,
520
- taxTotalAmount: totals.taxTotalAmount,
521
- shippingNetAmount: totals.shippingNetAmount,
522
- shippingGrossAmount: totals.shippingGrossAmount,
523
- surchargeTotalAmount: totals.surchargeTotalAmount,
524
- grandTotalNetAmount: totals.grandTotalNetAmount,
525
- grandTotalGrossAmount: totals.grandTotalGrossAmount,
526
- paidTotalAmount: totals.paidTotalAmount,
527
- refundedTotalAmount: totals.refundedTotalAmount,
528
- outstandingAmount: totals.outstandingAmount
529
- });
530
- }
531
- }
532
- }
533
- }
534
499
  }
535
500
  }
536
501
  };