/** * Canonical name + domain derivation for workspace-scoped resources. * * AI-generated Coolify apps live under a single subdomain namespace * per workspace: * * https://{app-slug}.{workspace-slug}.vibnai.com * * e.g. `api.mark.vibnai.com` for the `api` app in workspace `mark`. * * The DNS record `*.vibnai.com` (or its subdomain) must resolve to * the Coolify server. Traefik picks up the Host header and Coolify's * per-app ACME handshake provisions a Let's Encrypt cert per FQDN. */ const VIBN_BASE_DOMAIN = process.env.VIBN_BASE_DOMAIN ?? 'vibnai.com'; const SLUG_STRIP = /[^a-z0-9-]+/g; /** Lowercase, dash-sanitize a free-form name into a DNS-safe slug. */ export function slugify(name: string): string { return name .toLowerCase() .replace(/[_\s]+/g, '-') .replace(SLUG_STRIP, '') .replace(/-+/g, '-') .replace(/^-+|-+$/g, '') .slice(0, 40) || 'app'; } /** * The default public FQDN for an app inside a workspace, given the * workspace's slug (e.g. `mark`) and an app slug (e.g. `my-api`). * * workspaceAppFqdn('mark', 'my-api') === 'my-api.mark.vibnai.com' */ export function workspaceAppFqdn(workspaceSlug: string, appSlug: string): string { return `${appSlug}.${workspaceSlug}.${VIBN_BASE_DOMAIN}`; } /** `https://{fqdn}` — what Coolify's `domains` field expects. */ export function toDomainsString(fqdns: string[]): string { return fqdns.map(f => (f.startsWith('http') ? f : `https://${f}`)).join(','); } /** Parse a Coolify `domains` CSV back into bare FQDNs. */ export function parseDomainsString(domains: string | null | undefined): string[] { if (!domains) return []; return domains .split(/[,\s]+/) .map(d => d.trim()) .filter(Boolean) .map(d => d.replace(/^https?:\/\//, '').replace(/\/+$/, '')); } /** Guard against cross-workspace or disallowed domains. */ export function isDomainUnderWorkspace(fqdn: string, workspaceSlug: string): boolean { const f = fqdn.replace(/^https?:\/\//, '').toLowerCase(); return f === `${workspaceSlug}.${VIBN_BASE_DOMAIN}` || f.endsWith(`.${workspaceSlug}.${VIBN_BASE_DOMAIN}`); } /** * Build a Gitea SSH clone URL for a repo in a workspace's org. * * NOTE: As of 2026-04 this is deprecated for Coolify-driven deploys on * vibnai.com — Gitea's builtin SSH is bound to host port 22222 which is * not publicly reachable, and the default port 22 hits Ubuntu's host * sshd which doesn't know about Gitea keys. Use {@link giteaHttpsUrl} * instead and embed the workspace bot's PAT. Kept for read-only places * (e.g. UI display) that want the canonical "clone with SSH" form. */ export function giteaSshUrl(org: string, repo: string, giteaHost = 'git.vibnai.com'): string { return `git@${giteaHost}:${org}/${repo}.git`; } /** * Build a Gitea HTTPS clone URL with basic-auth credentials embedded. * * https://{username}:{token}@{host}/{org}/{repo}.git * * This is what we pass to Coolify's `git_repository` field for every * Vibn-provisioned app. Works regardless of SSH topology and scopes * access to whatever the bot user can see. The token is usually the * per-workspace Gitea bot PAT. * * We URL-encode the username and token so PATs with special chars * (`:`, `/`, `@`, `#`, `?`, etc.) don't break URL parsing. */ export function giteaHttpsUrl( org: string, repo: string, username: string, token: string, giteaHost = 'git.vibnai.com' ): string { const u = encodeURIComponent(username); const t = encodeURIComponent(token); return `https://${u}:${t}@${giteaHost}/${org}/${repo}.git`; } /** * Redact the credentials from a Gitea HTTPS URL for safe logging / * display. Leaves the repo path intact. No-op for non-HTTPS URLs. */ export function redactGiteaHttpsUrl(url: string): string { return url.replace(/^(https?:\/\/)[^@]+@/, '$1***:***@'); } export { VIBN_BASE_DOMAIN };