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
70 lines
2.4 KiB
TypeScript
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 };
|