@open-mercato/core 0.10.1-develop.7690.1.9d41ec4115 → 0.10.1-develop.7699.1.5236fabda0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/auth/api/autologin.js +3 -1
  3. package/dist/modules/auth/api/autologin.js.map +2 -2
  4. package/dist/modules/auth/api/login.js +4 -1
  5. package/dist/modules/auth/api/login.js.map +2 -2
  6. package/dist/modules/auth/lib/sessionIntegrity.js +2 -0
  7. package/dist/modules/auth/lib/sessionIntegrity.js.map +2 -2
  8. package/dist/modules/auth/lib/tenantEligibility.js +16 -0
  9. package/dist/modules/auth/lib/tenantEligibility.js.map +7 -0
  10. package/dist/modules/auth/services/authService.js +5 -0
  11. package/dist/modules/auth/services/authService.js.map +2 -2
  12. package/dist/modules/customer_accounts/api/admin/domain-mappings/[id]/health-check.js +5 -5
  13. package/dist/modules/customer_accounts/api/admin/domain-mappings/[id]/health-check.js.map +2 -2
  14. package/dist/modules/customer_accounts/api/admin/domain-mappings/[id]/verify.js +5 -5
  15. package/dist/modules/customer_accounts/api/admin/domain-mappings/[id]/verify.js.map +2 -2
  16. package/dist/modules/customer_accounts/api/admin/domain-mappings.js +36 -14
  17. package/dist/modules/customer_accounts/api/admin/domain-mappings.js.map +2 -2
  18. package/dist/modules/customer_accounts/api/login.js +2 -1
  19. package/dist/modules/customer_accounts/api/login.js.map +2 -2
  20. package/dist/modules/customer_accounts/lib/customerAuth.js +2 -0
  21. package/dist/modules/customer_accounts/lib/customerAuth.js.map +2 -2
  22. package/dist/modules/customer_accounts/lib/tenantEligibility.js +16 -0
  23. package/dist/modules/customer_accounts/lib/tenantEligibility.js.map +7 -0
  24. package/dist/modules/customer_accounts/services/customerInvitationService.js +2 -0
  25. package/dist/modules/customer_accounts/services/customerInvitationService.js.map +2 -2
  26. package/dist/modules/customer_accounts/services/customerSessionService.js +2 -0
  27. package/dist/modules/customer_accounts/services/customerSessionService.js.map +2 -2
  28. package/dist/modules/customer_accounts/services/customerTokenService.js +2 -0
  29. package/dist/modules/customer_accounts/services/customerTokenService.js.map +2 -2
  30. package/dist/modules/customer_accounts/services/domainMappingService.js +5 -2
  31. package/dist/modules/customer_accounts/services/domainMappingService.js.map +2 -2
  32. package/dist/modules/customers/components/linking/LinkEntityDialog.js +10 -2
  33. package/dist/modules/customers/components/linking/LinkEntityDialog.js.map +2 -2
  34. package/dist/modules/sales/backend/sales/documents/[id]/page.js +17 -3
  35. package/dist/modules/sales/backend/sales/documents/[id]/page.js.map +2 -2
  36. package/package.json +7 -7
  37. package/src/modules/auth/api/autologin.ts +3 -1
  38. package/src/modules/auth/api/login.ts +4 -1
  39. package/src/modules/auth/lib/sessionIntegrity.ts +2 -0
  40. package/src/modules/auth/lib/tenantEligibility.ts +17 -0
  41. package/src/modules/auth/services/authService.ts +5 -0
  42. package/src/modules/customer_accounts/api/admin/domain-mappings/[id]/health-check.ts +5 -5
  43. package/src/modules/customer_accounts/api/admin/domain-mappings/[id]/verify.ts +5 -5
  44. package/src/modules/customer_accounts/api/admin/domain-mappings.ts +38 -16
  45. package/src/modules/customer_accounts/api/login.ts +3 -1
  46. package/src/modules/customer_accounts/lib/customerAuth.ts +2 -0
  47. package/src/modules/customer_accounts/lib/tenantEligibility.ts +14 -0
  48. package/src/modules/customer_accounts/services/customerInvitationService.ts +2 -0
  49. package/src/modules/customer_accounts/services/customerSessionService.ts +2 -0
  50. package/src/modules/customer_accounts/services/customerTokenService.ts +2 -0
  51. package/src/modules/customer_accounts/services/domainMappingService.ts +3 -0
  52. package/src/modules/customers/components/linking/LinkEntityDialog.tsx +10 -2
  53. package/src/modules/sales/backend/sales/documents/[id]/page.tsx +15 -2
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/customer_accounts/services/customerSessionService.ts"],
4
- "sourcesContent": ["import { EntityManager } from '@mikro-orm/postgresql'\nimport { CustomerUser, CustomerUserSession } from '@open-mercato/core/modules/customer_accounts/data/entities'\nimport { generateSecureToken, hashToken } from '@open-mercato/core/modules/customer_accounts/lib/tokenGenerator'\nimport { signAudienceJwt } from '@open-mercato/shared/lib/auth/jwt'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\n\nexport const CUSTOMER_JWT_AUDIENCE = 'customer'\nconst CUSTOMER_JWT_TTL_SECONDS = 60 * 60 * 8\n\nconst DEFAULT_SESSION_TTL_DAYS = 30\nconst DEFAULT_MAX_SESSIONS_PER_USER = 5\n\nfunction resolveMaxSessionsPerUser(): number {\n const raw = process.env.MAX_CUSTOMER_SESSIONS_PER_USER\n if (!raw) return DEFAULT_MAX_SESSIONS_PER_USER\n const parsed = Number(raw)\n if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_MAX_SESSIONS_PER_USER\n return Math.floor(parsed)\n}\n\nexport class CustomerSessionService {\n constructor(private em: EntityManager) {}\n\n async createSession(\n user: CustomerUser,\n resolvedFeatures: string[],\n ip?: string | null,\n userAgent?: string | null,\n ): Promise<{ rawToken: string; jwt: string; session: CustomerUserSession }> {\n const rawToken = generateSecureToken()\n const tokenHash = hashToken(rawToken)\n const days = Number(process.env.CUSTOMER_SESSION_TTL_DAYS || DEFAULT_SESSION_TTL_DAYS)\n const expiresAt = new Date(Date.now() + days * 24 * 60 * 60 * 1000)\n\n await this.enforceSessionCap(user.id, user.tenantId, user.organizationId)\n\n const session = this.em.create(CustomerUserSession, {\n user,\n tokenHash,\n ipAddress: ip || null,\n userAgent: userAgent || null,\n expiresAt,\n lastUsedAt: new Date(),\n createdAt: new Date(),\n } as any) as CustomerUserSession\n await this.em.persist(session).flush()\n\n const jwt = this.signCustomerJwt(user, resolvedFeatures, session.id)\n\n return { rawToken, jwt, session }\n }\n\n signCustomerJwt(user: CustomerUser, resolvedFeatures: string[], sessionId: string): string {\n return signAudienceJwt(\n CUSTOMER_JWT_AUDIENCE,\n {\n sub: user.id,\n sid: sessionId,\n type: 'customer',\n tenantId: user.tenantId,\n orgId: user.organizationId,\n email: user.email,\n displayName: user.displayName || '',\n customerEntityId: user.customerEntityId || null,\n personEntityId: user.personEntityId || null,\n resolvedFeatures,\n },\n CUSTOMER_JWT_TTL_SECONDS,\n )\n }\n\n async findByToken(rawToken: string, tenantId?: string): Promise<CustomerUserSession | null> {\n const tokenHash = hashToken(rawToken)\n const session = await this.em.findOne(CustomerUserSession, {\n tokenHash,\n deletedAt: null,\n }, { populate: ['user'] })\n if (!session) return null\n if (session.expiresAt.getTime() < Date.now()) return null\n const user = session.user as CustomerUser\n if (tenantId && user?.tenantId !== tenantId) return null\n return session\n }\n\n async refreshSession(\n rawToken: string,\n resolvedFeatures: string[],\n ): Promise<{ jwt: string; user: CustomerUser } | null> {\n const session = await this.findByToken(rawToken)\n if (!session) return null\n const user = session.user as CustomerUser\n if (!user || user.deletedAt || !user.isActive) return null\n\n await this.em.nativeUpdate(CustomerUserSession, { id: session.id }, { lastUsedAt: new Date() })\n const jwt = this.signCustomerJwt(user, resolvedFeatures, session.id)\n return { jwt, user }\n }\n\n async findActiveSessionById(sessionId: string): Promise<CustomerUserSession | null> {\n const session = await this.em.findOne(CustomerUserSession, {\n id: sessionId,\n deletedAt: null,\n })\n if (!session) return null\n if (session.expiresAt.getTime() < Date.now()) return null\n return session\n }\n\n async findActiveSessionForClaims(input: {\n sessionId: string\n userId: string\n tenantId: string\n organizationId: string\n }): Promise<CustomerUserSession | null> {\n const session = await this.em.findOne(CustomerUserSession, {\n id: input.sessionId,\n user: {\n id: input.userId,\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n },\n deletedAt: null,\n })\n if (!session) return null\n if (session.expiresAt.getTime() < Date.now()) return null\n return session\n }\n\n /**\n * Liveness probe for tokens that carry no `sid` claim \u2014 the pre-migration customer JWTs the\n * legacy raw-secret fallback still admits during a key rotation. Such a token cannot name the\n * session it was issued for, so the strongest statement we can make about it is whether the\n * customer is still signed in anywhere. A customer whose sessions have all been revoked\n * (logout, per-device revoke, admin action) has none, and the token must stop working.\n *\n * Existence is the whole answer, so this reads no encrypted column and needs no decryption\n * round-trip on the authentication path \u2014 the same reason `findActiveSessionForClaims` next to\n * it queries directly. Scope comes from the owning user, since sessions carry no scope columns.\n */\n async hasActiveSessionForUser(input: {\n userId: string\n tenantId: string\n organizationId: string\n }): Promise<boolean> {\n const session = await this.em.findOne(CustomerUserSession, {\n user: {\n id: input.userId,\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n },\n deletedAt: null,\n expiresAt: { $gt: new Date() },\n })\n return session !== null\n }\n\n async revokeSession(sessionId: string): Promise<void> {\n await this.em.nativeUpdate(CustomerUserSession, { id: sessionId }, { deletedAt: new Date() })\n }\n\n private async enforceSessionCap(\n userId: string,\n tenantId: string,\n organizationId: string,\n ): Promise<void> {\n const cap = resolveMaxSessionsPerUser()\n const existing = await findWithDecryption(\n this.em,\n CustomerUserSession,\n {\n user: userId as any,\n deletedAt: null,\n expiresAt: { $gt: new Date() },\n },\n { orderBy: { createdAt: 'asc' } },\n { tenantId, organizationId },\n )\n const toRevoke = existing.length - (cap - 1)\n if (toRevoke <= 0) return\n const oldestIds = existing.slice(0, toRevoke).map((s) => s.id)\n await this.em.nativeUpdate(\n CustomerUserSession,\n { id: { $in: oldestIds } },\n { deletedAt: new Date() },\n )\n }\n\n async revokeAllUserSessions(userId: string, em?: EntityManager): Promise<void> {\n const now = new Date()\n const target = em ?? this.em\n await target.nativeUpdate(\n CustomerUserSession,\n { user: userId as any, deletedAt: null },\n { deletedAt: now },\n )\n await target.nativeUpdate(\n CustomerUser,\n { id: userId },\n { sessionsRevokedAt: now },\n )\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { EntityManager } from '@mikro-orm/postgresql'\nimport { CustomerUser, CustomerUserSession } from '@open-mercato/core/modules/customer_accounts/data/entities'\nimport { generateSecureToken, hashToken } from '@open-mercato/core/modules/customer_accounts/lib/tokenGenerator'\nimport { signAudienceJwt } from '@open-mercato/shared/lib/auth/jwt'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { isCustomerTenantActive } from '@open-mercato/core/modules/customer_accounts/lib/tenantEligibility'\n\nexport const CUSTOMER_JWT_AUDIENCE = 'customer'\nconst CUSTOMER_JWT_TTL_SECONDS = 60 * 60 * 8\n\nconst DEFAULT_SESSION_TTL_DAYS = 30\nconst DEFAULT_MAX_SESSIONS_PER_USER = 5\n\nfunction resolveMaxSessionsPerUser(): number {\n const raw = process.env.MAX_CUSTOMER_SESSIONS_PER_USER\n if (!raw) return DEFAULT_MAX_SESSIONS_PER_USER\n const parsed = Number(raw)\n if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_MAX_SESSIONS_PER_USER\n return Math.floor(parsed)\n}\n\nexport class CustomerSessionService {\n constructor(private em: EntityManager) {}\n\n async createSession(\n user: CustomerUser,\n resolvedFeatures: string[],\n ip?: string | null,\n userAgent?: string | null,\n ): Promise<{ rawToken: string; jwt: string; session: CustomerUserSession }> {\n const rawToken = generateSecureToken()\n const tokenHash = hashToken(rawToken)\n const days = Number(process.env.CUSTOMER_SESSION_TTL_DAYS || DEFAULT_SESSION_TTL_DAYS)\n const expiresAt = new Date(Date.now() + days * 24 * 60 * 60 * 1000)\n\n await this.enforceSessionCap(user.id, user.tenantId, user.organizationId)\n\n const session = this.em.create(CustomerUserSession, {\n user,\n tokenHash,\n ipAddress: ip || null,\n userAgent: userAgent || null,\n expiresAt,\n lastUsedAt: new Date(),\n createdAt: new Date(),\n } as any) as CustomerUserSession\n await this.em.persist(session).flush()\n\n const jwt = this.signCustomerJwt(user, resolvedFeatures, session.id)\n\n return { rawToken, jwt, session }\n }\n\n signCustomerJwt(user: CustomerUser, resolvedFeatures: string[], sessionId: string): string {\n return signAudienceJwt(\n CUSTOMER_JWT_AUDIENCE,\n {\n sub: user.id,\n sid: sessionId,\n type: 'customer',\n tenantId: user.tenantId,\n orgId: user.organizationId,\n email: user.email,\n displayName: user.displayName || '',\n customerEntityId: user.customerEntityId || null,\n personEntityId: user.personEntityId || null,\n resolvedFeatures,\n },\n CUSTOMER_JWT_TTL_SECONDS,\n )\n }\n\n async findByToken(rawToken: string, tenantId?: string): Promise<CustomerUserSession | null> {\n const tokenHash = hashToken(rawToken)\n const session = await this.em.findOne(CustomerUserSession, {\n tokenHash,\n deletedAt: null,\n }, { populate: ['user'] })\n if (!session) return null\n if (session.expiresAt.getTime() < Date.now()) return null\n const user = session.user as CustomerUser\n if (tenantId && user?.tenantId !== tenantId) return null\n return session\n }\n\n async refreshSession(\n rawToken: string,\n resolvedFeatures: string[],\n ): Promise<{ jwt: string; user: CustomerUser } | null> {\n const session = await this.findByToken(rawToken)\n if (!session) return null\n const user = session.user as CustomerUser\n if (!user || user.deletedAt || !user.isActive) return null\n if (!(await isCustomerTenantActive(this.em, user.tenantId))) return null\n\n await this.em.nativeUpdate(CustomerUserSession, { id: session.id }, { lastUsedAt: new Date() })\n const jwt = this.signCustomerJwt(user, resolvedFeatures, session.id)\n return { jwt, user }\n }\n\n async findActiveSessionById(sessionId: string): Promise<CustomerUserSession | null> {\n const session = await this.em.findOne(CustomerUserSession, {\n id: sessionId,\n deletedAt: null,\n })\n if (!session) return null\n if (session.expiresAt.getTime() < Date.now()) return null\n return session\n }\n\n async findActiveSessionForClaims(input: {\n sessionId: string\n userId: string\n tenantId: string\n organizationId: string\n }): Promise<CustomerUserSession | null> {\n const session = await this.em.findOne(CustomerUserSession, {\n id: input.sessionId,\n user: {\n id: input.userId,\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n },\n deletedAt: null,\n })\n if (!session) return null\n if (session.expiresAt.getTime() < Date.now()) return null\n return session\n }\n\n /**\n * Liveness probe for tokens that carry no `sid` claim \u2014 the pre-migration customer JWTs the\n * legacy raw-secret fallback still admits during a key rotation. Such a token cannot name the\n * session it was issued for, so the strongest statement we can make about it is whether the\n * customer is still signed in anywhere. A customer whose sessions have all been revoked\n * (logout, per-device revoke, admin action) has none, and the token must stop working.\n *\n * Existence is the whole answer, so this reads no encrypted column and needs no decryption\n * round-trip on the authentication path \u2014 the same reason `findActiveSessionForClaims` next to\n * it queries directly. Scope comes from the owning user, since sessions carry no scope columns.\n */\n async hasActiveSessionForUser(input: {\n userId: string\n tenantId: string\n organizationId: string\n }): Promise<boolean> {\n const session = await this.em.findOne(CustomerUserSession, {\n user: {\n id: input.userId,\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n },\n deletedAt: null,\n expiresAt: { $gt: new Date() },\n })\n return session !== null\n }\n\n async revokeSession(sessionId: string): Promise<void> {\n await this.em.nativeUpdate(CustomerUserSession, { id: sessionId }, { deletedAt: new Date() })\n }\n\n private async enforceSessionCap(\n userId: string,\n tenantId: string,\n organizationId: string,\n ): Promise<void> {\n const cap = resolveMaxSessionsPerUser()\n const existing = await findWithDecryption(\n this.em,\n CustomerUserSession,\n {\n user: userId as any,\n deletedAt: null,\n expiresAt: { $gt: new Date() },\n },\n { orderBy: { createdAt: 'asc' } },\n { tenantId, organizationId },\n )\n const toRevoke = existing.length - (cap - 1)\n if (toRevoke <= 0) return\n const oldestIds = existing.slice(0, toRevoke).map((s) => s.id)\n await this.em.nativeUpdate(\n CustomerUserSession,\n { id: { $in: oldestIds } },\n { deletedAt: new Date() },\n )\n }\n\n async revokeAllUserSessions(userId: string, em?: EntityManager): Promise<void> {\n const now = new Date()\n const target = em ?? this.em\n await target.nativeUpdate(\n CustomerUserSession,\n { user: userId as any, deletedAt: null },\n { deletedAt: now },\n )\n await target.nativeUpdate(\n CustomerUser,\n { id: userId },\n { sessionsRevokedAt: now },\n )\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -3,6 +3,7 @@ import {
3
3
  CustomerUserPasswordReset
4
4
  } from "@open-mercato/core/modules/customer_accounts/data/entities";
5
5
  import { generateSecureToken, hashToken } from "@open-mercato/core/modules/customer_accounts/lib/tokenGenerator";
6
+ import { isCustomerTenantActive } from "@open-mercato/core/modules/customer_accounts/lib/tenantEligibility";
6
7
  function getKysely(em) {
7
8
  return em.getKysely();
8
9
  }
@@ -65,6 +66,7 @@ class CustomerTokenService {
65
66
  if (record.expiresAt.getTime() < Date.now()) return null;
66
67
  const user = record.user;
67
68
  if (tenantId && user?.tenantId !== tenantId) return null;
69
+ if (purpose === "magic_link" && !await isCustomerTenantActive(this.em, user?.tenantId)) return null;
68
70
  const db = getKysely(this.em);
69
71
  const updateResult = await db.updateTable("customer_user_email_verifications").set({ used_at: /* @__PURE__ */ new Date() }).where("id", "=", record.id).where("used_at", "is", null).where("expires_at", ">", /* @__PURE__ */ new Date()).executeTakeFirst();
70
72
  const consumed = Number(updateResult?.numUpdatedRows ?? 0);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/customer_accounts/services/customerTokenService.ts"],
4
- "sourcesContent": ["import { EntityManager } from '@mikro-orm/postgresql'\nimport type { Kysely } from 'kysely'\nimport {\n CustomerUser,\n CustomerUserEmailVerification,\n CustomerUserPasswordReset,\n} from '@open-mercato/core/modules/customer_accounts/data/entities'\nimport { generateSecureToken, hashToken } from '@open-mercato/core/modules/customer_accounts/lib/tokenGenerator'\n\nfunction getKysely(em: EntityManager): Kysely<any> {\n return (em as unknown as { getKysely: () => Kysely<any> }).getKysely()\n}\n\nconst EMAIL_VERIFICATION_TTL_MS = 24 * 60 * 60 * 1000 // 24 hours\nconst MAGIC_LINK_TTL_MS = 15 * 60 * 1000 // 15 minutes\nconst PASSWORD_RESET_TTL_MS = 60 * 60 * 1000 // 60 minutes\n\nexport class CustomerTokenService {\n constructor(private em: EntityManager) {}\n\n async createEmailVerification(userId: string, tenantId: string): Promise<string> {\n const rawToken = generateSecureToken()\n const tokenHashed = hashToken(rawToken)\n const expiresAt = new Date(Date.now() + EMAIL_VERIFICATION_TTL_MS)\n const record = this.em.create(CustomerUserEmailVerification, {\n user: userId as any,\n token: tokenHashed,\n purpose: 'email_verification',\n expiresAt,\n createdAt: new Date(),\n } as any)\n await this.em.persist(record).flush()\n return rawToken\n }\n\n async createMagicLink(userId: string, tenantId: string): Promise<string> {\n const rawToken = generateSecureToken()\n const tokenHashed = hashToken(rawToken)\n const expiresAt = new Date(Date.now() + MAGIC_LINK_TTL_MS)\n const record = this.em.create(CustomerUserEmailVerification, {\n user: userId as any,\n token: tokenHashed,\n purpose: 'magic_link',\n expiresAt,\n createdAt: new Date(),\n } as any)\n await this.em.persist(record).flush()\n return rawToken\n }\n\n async createPasswordReset(userId: string, tenantId: string): Promise<string> {\n const rawToken = generateSecureToken()\n const tokenHashed = hashToken(rawToken)\n const expiresAt = new Date(Date.now() + PASSWORD_RESET_TTL_MS)\n const record = this.em.create(CustomerUserPasswordReset, {\n user: userId as any,\n token: tokenHashed,\n expiresAt,\n createdAt: new Date(),\n } as any)\n await this.em.persist(record).flush()\n return rawToken\n }\n\n async verifyEmailToken(token: string, purpose: string, tenantId?: string): Promise<{ userId: string; tenantId: string } | null> {\n const tokenHashed = hashToken(token)\n const record = await this.em.findOne(CustomerUserEmailVerification, {\n token: tokenHashed,\n purpose,\n }, { populate: ['user'] })\n if (!record) return null\n if (record.usedAt) return null\n if (record.expiresAt.getTime() < Date.now()) return null\n\n const user = record.user as CustomerUser\n if (tenantId && user?.tenantId !== tenantId) return null\n\n const db = getKysely(this.em)\n const updateResult = await db\n .updateTable('customer_user_email_verifications' as any)\n .set({ used_at: new Date() } as any)\n .where('id' as any, '=', record.id)\n .where('used_at' as any, 'is', null)\n .where('expires_at' as any, '>', new Date())\n .executeTakeFirst()\n const consumed = Number(updateResult?.numUpdatedRows ?? 0)\n if (consumed === 0) return null\n\n const resolvedUserId = typeof user === 'string' ? user : user.id\n const resolvedTenantId = typeof user === 'string' ? '' : user.tenantId\n return { userId: resolvedUserId, tenantId: resolvedTenantId }\n }\n\n async verifyPasswordResetToken(token: string, tenantId?: string): Promise<{ userId: string; tenantId: string } | null> {\n const tokenHashed = hashToken(token)\n const record = await this.em.findOne(CustomerUserPasswordReset, {\n token: tokenHashed,\n }, { populate: ['user'] })\n if (!record) return null\n if (record.usedAt) return null\n if (record.expiresAt.getTime() < Date.now()) return null\n\n const user = record.user as CustomerUser\n if (tenantId && user?.tenantId !== tenantId) return null\n\n const db = getKysely(this.em)\n const updateResult = await db\n .updateTable('customer_user_password_resets' as any)\n .set({ used_at: new Date() } as any)\n .where('id' as any, '=', record.id)\n .where('used_at' as any, 'is', null)\n .where('expires_at' as any, '>', new Date())\n .executeTakeFirst()\n const consumed = Number(updateResult?.numUpdatedRows ?? 0)\n if (consumed === 0) return null\n\n const resolvedUserId = typeof user === 'string' ? user : user.id\n const resolvedTenantId = typeof user === 'string' ? '' : user.tenantId\n return { userId: resolvedUserId, tenantId: resolvedTenantId }\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { EntityManager } from '@mikro-orm/postgresql'\nimport type { Kysely } from 'kysely'\nimport {\n CustomerUser,\n CustomerUserEmailVerification,\n CustomerUserPasswordReset,\n} from '@open-mercato/core/modules/customer_accounts/data/entities'\nimport { generateSecureToken, hashToken } from '@open-mercato/core/modules/customer_accounts/lib/tokenGenerator'\nimport { isCustomerTenantActive } from '@open-mercato/core/modules/customer_accounts/lib/tenantEligibility'\n\nfunction getKysely(em: EntityManager): Kysely<any> {\n return (em as unknown as { getKysely: () => Kysely<any> }).getKysely()\n}\n\nconst EMAIL_VERIFICATION_TTL_MS = 24 * 60 * 60 * 1000 // 24 hours\nconst MAGIC_LINK_TTL_MS = 15 * 60 * 1000 // 15 minutes\nconst PASSWORD_RESET_TTL_MS = 60 * 60 * 1000 // 60 minutes\n\nexport class CustomerTokenService {\n constructor(private em: EntityManager) {}\n\n async createEmailVerification(userId: string, tenantId: string): Promise<string> {\n const rawToken = generateSecureToken()\n const tokenHashed = hashToken(rawToken)\n const expiresAt = new Date(Date.now() + EMAIL_VERIFICATION_TTL_MS)\n const record = this.em.create(CustomerUserEmailVerification, {\n user: userId as any,\n token: tokenHashed,\n purpose: 'email_verification',\n expiresAt,\n createdAt: new Date(),\n } as any)\n await this.em.persist(record).flush()\n return rawToken\n }\n\n async createMagicLink(userId: string, tenantId: string): Promise<string> {\n const rawToken = generateSecureToken()\n const tokenHashed = hashToken(rawToken)\n const expiresAt = new Date(Date.now() + MAGIC_LINK_TTL_MS)\n const record = this.em.create(CustomerUserEmailVerification, {\n user: userId as any,\n token: tokenHashed,\n purpose: 'magic_link',\n expiresAt,\n createdAt: new Date(),\n } as any)\n await this.em.persist(record).flush()\n return rawToken\n }\n\n async createPasswordReset(userId: string, tenantId: string): Promise<string> {\n const rawToken = generateSecureToken()\n const tokenHashed = hashToken(rawToken)\n const expiresAt = new Date(Date.now() + PASSWORD_RESET_TTL_MS)\n const record = this.em.create(CustomerUserPasswordReset, {\n user: userId as any,\n token: tokenHashed,\n expiresAt,\n createdAt: new Date(),\n } as any)\n await this.em.persist(record).flush()\n return rawToken\n }\n\n async verifyEmailToken(token: string, purpose: string, tenantId?: string): Promise<{ userId: string; tenantId: string } | null> {\n const tokenHashed = hashToken(token)\n const record = await this.em.findOne(CustomerUserEmailVerification, {\n token: tokenHashed,\n purpose,\n }, { populate: ['user'] })\n if (!record) return null\n if (record.usedAt) return null\n if (record.expiresAt.getTime() < Date.now()) return null\n\n const user = record.user as CustomerUser\n if (tenantId && user?.tenantId !== tenantId) return null\n if (purpose === 'magic_link' && !(await isCustomerTenantActive(this.em, user?.tenantId))) return null\n\n const db = getKysely(this.em)\n const updateResult = await db\n .updateTable('customer_user_email_verifications' as any)\n .set({ used_at: new Date() } as any)\n .where('id' as any, '=', record.id)\n .where('used_at' as any, 'is', null)\n .where('expires_at' as any, '>', new Date())\n .executeTakeFirst()\n const consumed = Number(updateResult?.numUpdatedRows ?? 0)\n if (consumed === 0) return null\n\n const resolvedUserId = typeof user === 'string' ? user : user.id\n const resolvedTenantId = typeof user === 'string' ? '' : user.tenantId\n return { userId: resolvedUserId, tenantId: resolvedTenantId }\n }\n\n async verifyPasswordResetToken(token: string, tenantId?: string): Promise<{ userId: string; tenantId: string } | null> {\n const tokenHashed = hashToken(token)\n const record = await this.em.findOne(CustomerUserPasswordReset, {\n token: tokenHashed,\n }, { populate: ['user'] })\n if (!record) return null\n if (record.usedAt) return null\n if (record.expiresAt.getTime() < Date.now()) return null\n\n const user = record.user as CustomerUser\n if (tenantId && user?.tenantId !== tenantId) return null\n\n const db = getKysely(this.em)\n const updateResult = await db\n .updateTable('customer_user_password_resets' as any)\n .set({ used_at: new Date() } as any)\n .where('id' as any, '=', record.id)\n .where('used_at' as any, 'is', null)\n .where('expires_at' as any, '>', new Date())\n .executeTakeFirst()\n const consumed = Number(updateResult?.numUpdatedRows ?? 0)\n if (consumed === 0) return null\n\n const resolvedUserId = typeof user === 'string' ? user : user.id\n const resolvedTenantId = typeof user === 'string' ? '' : user.tenantId\n return { userId: resolvedUserId, tenantId: resolvedTenantId }\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -201,7 +201,8 @@ class DomainMappingService {
201
201
  if (input.replacesDomainId) {
202
202
  replacesDomain = await this.em.findOne(DomainMapping, {
203
203
  id: input.replacesDomainId,
204
- tenantId: input.tenantId
204
+ tenantId: input.tenantId,
205
+ organizationId: input.organizationId
205
206
  });
206
207
  }
207
208
  const entity = this.em.create(DomainMapping, {
@@ -285,7 +286,9 @@ class DomainMappingService {
285
286
  });
286
287
  if (entity.replacesDomain) {
287
288
  const replaced = await this.em.findOne(DomainMapping, {
288
- id: entity.replacesDomain.id
289
+ id: entity.replacesDomain.id,
290
+ tenantId: entity.tenantId,
291
+ organizationId: entity.organizationId
289
292
  });
290
293
  if (replaced) {
291
294
  const replacedHostname = replaced.hostname;
@@ -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 { runWithCacheTenant } from '@open-mercato/cache'\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 const cache = this.cache\n if (cache) {\n const cached = (await runWithCacheTenant(null, () => 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 (cache) {\n await runWithCacheTenant(null, () => 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 const cache = this.cache\n if (!cache) return\n const tags = [DOMAIN_ROUTING_TAG, `${DOMAIN_ROUTING_TAG}:${hostname}`]\n if (organizationId) tags.push(`${DOMAIN_ROUTING_TAG}:org:${organizationId}`)\n await Promise.allSettled([\n cache.deleteByTags(tags),\n runWithCacheTenant(null, () => cache.deleteByTags([`${DOMAIN_ROUTING_TAG}:${hostname}`])),\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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",
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 { runWithCacheTenant } from '@open-mercato/cache'\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 const cache = this.cache\n if (cache) {\n const cached = (await runWithCacheTenant(null, () => 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 (cache) {\n await runWithCacheTenant(null, () => 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 organizationId: input.organizationId,\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 tenantId: entity.tenantId,\n organizationId: entity.organizationId,\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 const cache = this.cache\n if (!cache) return\n const tags = [DOMAIN_ROUTING_TAG, `${DOMAIN_ROUTING_TAG}:${hostname}`]\n if (organizationId) tags.push(`${DOMAIN_ROUTING_TAG}:org:${organizationId}`)\n await Promise.allSettled([\n cache.deleteByTags(tags),\n runWithCacheTenant(null, () => cache.deleteByTags([`${DOMAIN_ROUTING_TAG}:${hostname}`])),\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
  }
@@ -241,11 +241,19 @@ function LinkEntityDialog({
241
241
  }, []);
242
242
  const handleRowClick = React.useCallback(
243
243
  (option) => {
244
- setFocusedId(option.id);
245
244
  const isSelected = draftSet.has(option.id);
246
245
  toggleDraftId(option.id, !isSelected);
246
+ if (!isSelected) {
247
+ setFocusedId(option.id);
248
+ return;
249
+ }
250
+ setFocusedId((current) => {
251
+ if (current !== option.id) return current;
252
+ const remaining = draftIds.filter((id) => id !== option.id);
253
+ return remaining.length > 0 ? remaining[0] : null;
254
+ });
247
255
  },
248
- [draftSet, toggleDraftId]
256
+ [draftIds, draftSet, toggleDraftId]
249
257
  );
250
258
  const handleAddNewCreated = React.useCallback(
251
259
  (option) => {