Files
vibn-frontend/lib/naming.ts
Mark Henderson 0797717bc1 Phase 4: AI-driven app/database/auth lifecycle
Workspace-owned deploy infra so AI agents can create and destroy
Coolify resources without ever touching the root admin token.

  vibn_workspaces
    + coolify_server_uuid, coolify_destination_uuid
    + coolify_environment_name (default "production")
    + coolify_private_key_uuid, gitea_bot_ssh_key_id

  ensureWorkspaceProvisioned
    + generates an ed25519 keypair per workspace
    + pushes pubkey to the Gitea bot user (read/write scoped by team)
    + registers privkey in Coolify as a reusable deploy key

  New endpoints under /api/workspaces/[slug]/
    apps/                POST (private-deploy-key from Gitea repo)
    apps/[uuid]          PATCH, DELETE?confirm=<name>
    apps/[uuid]/domains  GET, PATCH (policy: *.{ws}.vibnai.com only)
    databases/           GET, POST (8 types incl. postgres, clickhouse, dragonfly)
    databases/[uuid]     GET, PATCH, DELETE?confirm=<name>
    auth/                GET, POST (Pocketbase, Authentik, Keycloak, Pocket-ID, Logto, Supertokens)
    auth/[uuid]          DELETE?confirm=<name>

  MCP (/api/mcp) gains 15 new tools that mirror the REST surface and
  enforce the same workspace tenancy + delete-confirm guard.

  Safety: destructive ops require ?confirm=<exact-resource-name>; volumes
  are kept by default (pass delete_volumes=true to drop).

Made-with: Cursor
2026-04-21 12:04:59 -07:00

70 lines
2.4 KiB
TypeScript

/**
* 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.
* Matches what Coolify's `private-deploy-key` flow expects.
*/
export function giteaSshUrl(org: string, repo: string, giteaHost = 'git.vibnai.com'): string {
return `git@${giteaHost}:${org}/${repo}.git`;
}
export { VIBN_BASE_DOMAIN };