import { prisma } from './db.js'; const tenantCache = new Map(); const CACHE_TTL = 60_000; // 1 minute export interface TenantInfo { id: string; name: string; slug: string; } export async function resolveTenant(hostname: string): Promise { // Strip port from hostname const host = hostname.split(':')[0]; // Check cache const cached = tenantCache.get(host); if (cached && cached.expiresAt > Date.now()) { return cached.tenant; } // Look up existing hostname mapping const record = await prisma.tenantHostname.findUnique({ where: { hostname: host }, include: { tenant: true } }); if (record) { const tenant: TenantInfo = { id: record.tenant.id, name: record.tenant.name, slug: record.tenant.slug }; tenantCache.set(host, { tenant, expiresAt: Date.now() + CACHE_TTL }); return tenant; } // No mapping found — auto-provision // If a default tenant exists, map this hostname to it. // If no tenants exist at all, create the first one. const tenant = await autoProvisionTenant(host); if (tenant) { tenantCache.set(host, { tenant, expiresAt: Date.now() + CACHE_TTL }); } return tenant; } async function autoProvisionTenant(host: string): Promise { // Any hostname that reaches this app was pointed here via DNS, // so it's intentional. Auto-create a tenant for it. const slug = slugify(host); const name = humanizeDomain(host); const tenant = await prisma.tenant.create({ data: { name, slug, hostnames: { create: { hostname: host, isPrimary: true } } } }); return { id: tenant.id, name: tenant.name, slug: tenant.slug }; } function slugify(host: string): string { return host .replace(/[^a-zA-Z0-9]/g, '-') .replace(/-+/g, '-') .replace(/^-|-$/g, '') .toLowerCase() .slice(0, 48); } function humanizeDomain(host: string): string { // "pounce.catrenelle.com" -> "Pounce" // "192.168.3.65" -> "Pounce" // "localhost" -> "Pounce" const part = host.split('.')[0]; if (/^\d+$/.test(part) || part === 'localhost') { return 'Pounce'; } return part.charAt(0).toUpperCase() + part.slice(1); } export function clearTenantCache() { tenantCache.clear(); }