694 lines
23 KiB
TypeScript
694 lines
23 KiB
TypeScript
/**
|
|
* Coolify API client for Vibn project provisioning.
|
|
*
|
|
* Used server-side only. Credentials from env vars:
|
|
* COOLIFY_URL — e.g. https://coolify.vibnai.com
|
|
* COOLIFY_API_TOKEN — admin bearer token
|
|
* COOLIFY_DEFAULT_SERVER_UUID — which Coolify server workspaces deploy to
|
|
* COOLIFY_DEFAULT_DESTINATION_UUID — Docker destination on that server
|
|
*/
|
|
|
|
const COOLIFY_URL = process.env.COOLIFY_URL ?? 'https://coolify.vibnai.com';
|
|
const COOLIFY_API_TOKEN = process.env.COOLIFY_API_TOKEN ?? '';
|
|
const COOLIFY_DEFAULT_SERVER_UUID =
|
|
process.env.COOLIFY_DEFAULT_SERVER_UUID ?? 'jws4g4cgssss4cw48s488woc';
|
|
const COOLIFY_DEFAULT_DESTINATION_UUID =
|
|
process.env.COOLIFY_DEFAULT_DESTINATION_UUID ?? 'zkogkggkw0wg40gccks80oo0';
|
|
|
|
export interface CoolifyProject {
|
|
uuid: string;
|
|
name: string;
|
|
description?: string;
|
|
environments?: Array<{ id: number; uuid: string; name: string; project_id: number }>;
|
|
}
|
|
|
|
/** All database flavors Coolify v4 can provision. */
|
|
export type CoolifyDatabaseType =
|
|
| 'postgresql'
|
|
| 'mysql'
|
|
| 'mariadb'
|
|
| 'mongodb'
|
|
| 'redis'
|
|
| 'keydb'
|
|
| 'dragonfly'
|
|
| 'clickhouse';
|
|
|
|
export interface CoolifyDatabase {
|
|
uuid: string;
|
|
name: string;
|
|
type?: string;
|
|
status: string;
|
|
internal_db_url?: string;
|
|
external_db_url?: string;
|
|
is_public?: boolean;
|
|
public_port?: number;
|
|
project_uuid?: string;
|
|
environment?: { project_uuid?: string; project?: { uuid?: string } };
|
|
}
|
|
|
|
export interface CoolifyApplication {
|
|
uuid: string;
|
|
name: string;
|
|
status: string;
|
|
fqdn?: string;
|
|
domains?: string;
|
|
git_repository?: string;
|
|
git_branch?: string;
|
|
project_uuid?: string;
|
|
environment_name?: string;
|
|
environment?: { project_uuid?: string; project?: { uuid?: string } };
|
|
}
|
|
|
|
export interface CoolifyEnvVar {
|
|
uuid?: string;
|
|
key: string;
|
|
value: string;
|
|
is_preview?: boolean;
|
|
is_build_time?: boolean;
|
|
is_literal?: boolean;
|
|
is_multiline?: boolean;
|
|
is_shown_once?: boolean;
|
|
}
|
|
|
|
export interface CoolifyPrivateKey {
|
|
uuid: string;
|
|
name: string;
|
|
description?: string;
|
|
fingerprint?: string;
|
|
}
|
|
|
|
export interface CoolifyServer {
|
|
uuid: string;
|
|
name: string;
|
|
ip: string;
|
|
is_reachable?: boolean;
|
|
settings?: { wildcard_domain?: string | null };
|
|
}
|
|
|
|
export interface CoolifyDeployment {
|
|
uuid: string;
|
|
status: string;
|
|
created_at?: string;
|
|
finished_at?: string;
|
|
commit?: string;
|
|
}
|
|
|
|
async function coolifyFetch(path: string, options: RequestInit = {}) {
|
|
const url = `${COOLIFY_URL}/api/v1${path}`;
|
|
const res = await fetch(url, {
|
|
...options,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
Authorization: `Bearer ${COOLIFY_API_TOKEN}`,
|
|
...(options.headers ?? {}),
|
|
},
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const text = await res.text();
|
|
throw new Error(`Coolify API error ${res.status} on ${path}: ${text}`);
|
|
}
|
|
if (res.status === 204) return null;
|
|
return res.json();
|
|
}
|
|
|
|
// ──────────────────────────────────────────────────
|
|
// Projects
|
|
// ──────────────────────────────────────────────────
|
|
|
|
export async function listProjects(): Promise<CoolifyProject[]> {
|
|
return coolifyFetch('/projects');
|
|
}
|
|
|
|
export async function createProject(name: string, description?: string): Promise<CoolifyProject> {
|
|
return coolifyFetch('/projects', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ name, description }),
|
|
});
|
|
}
|
|
|
|
export async function getProject(uuid: string): Promise<CoolifyProject> {
|
|
return coolifyFetch(`/projects/${uuid}`);
|
|
}
|
|
|
|
export async function deleteProject(uuid: string): Promise<void> {
|
|
await coolifyFetch(`/projects/${uuid}`, { method: 'DELETE' });
|
|
}
|
|
|
|
// ──────────────────────────────────────────────────
|
|
// Servers
|
|
// ──────────────────────────────────────────────────
|
|
|
|
export async function listServers(): Promise<CoolifyServer[]> {
|
|
return coolifyFetch('/servers');
|
|
}
|
|
|
|
// ──────────────────────────────────────────────────
|
|
// Private keys (SSH deploy keys)
|
|
// ──────────────────────────────────────────────────
|
|
|
|
export async function listPrivateKeys(): Promise<CoolifyPrivateKey[]> {
|
|
return coolifyFetch('/security/keys');
|
|
}
|
|
|
|
export async function createPrivateKey(opts: {
|
|
name: string;
|
|
privateKeyPem: string;
|
|
description?: string;
|
|
}): Promise<CoolifyPrivateKey> {
|
|
return coolifyFetch('/security/keys', {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
name: opts.name,
|
|
private_key: opts.privateKeyPem,
|
|
description: opts.description ?? '',
|
|
}),
|
|
});
|
|
}
|
|
|
|
export async function deletePrivateKey(uuid: string): Promise<void> {
|
|
await coolifyFetch(`/security/keys/${uuid}`, { method: 'DELETE' });
|
|
}
|
|
|
|
// ──────────────────────────────────────────────────
|
|
// Databases (all 8 types)
|
|
// ──────────────────────────────────────────────────
|
|
|
|
/** Shared context used by every database-create call. */
|
|
export interface CoolifyDbCreateContext {
|
|
projectUuid: string;
|
|
serverUuid?: string;
|
|
environmentName?: string;
|
|
destinationUuid?: string;
|
|
instantDeploy?: boolean;
|
|
}
|
|
|
|
export interface CreateDatabaseOpts extends CoolifyDbCreateContext {
|
|
type: CoolifyDatabaseType;
|
|
name: string;
|
|
description?: string;
|
|
image?: string;
|
|
isPublic?: boolean;
|
|
publicPort?: number;
|
|
/** Type-specific credentials / config */
|
|
credentials?: Record<string, unknown>;
|
|
/** Resource limits */
|
|
limits?: { memory?: string; cpus?: string };
|
|
}
|
|
|
|
/**
|
|
* Create a database of the requested type. Dispatches to the right
|
|
* POST /databases/{type} endpoint and packs the right credential
|
|
* fields for each flavor.
|
|
*/
|
|
export async function createDatabase(opts: CreateDatabaseOpts): Promise<{ uuid: string }> {
|
|
const {
|
|
type, name,
|
|
projectUuid,
|
|
serverUuid = COOLIFY_DEFAULT_SERVER_UUID,
|
|
environmentName = 'production',
|
|
destinationUuid = COOLIFY_DEFAULT_DESTINATION_UUID,
|
|
instantDeploy = true,
|
|
description,
|
|
image,
|
|
isPublic,
|
|
publicPort,
|
|
credentials = {},
|
|
limits = {},
|
|
} = opts;
|
|
|
|
const common: Record<string, unknown> = {
|
|
project_uuid: projectUuid,
|
|
server_uuid: serverUuid,
|
|
environment_name: environmentName,
|
|
destination_uuid: destinationUuid,
|
|
name,
|
|
description,
|
|
image,
|
|
is_public: isPublic,
|
|
public_port: publicPort,
|
|
instant_deploy: instantDeploy,
|
|
limits_memory: limits.memory,
|
|
limits_cpus: limits.cpus,
|
|
};
|
|
|
|
return coolifyFetch(`/databases/${type}`, {
|
|
method: 'POST',
|
|
body: JSON.stringify(stripUndefined({ ...common, ...credentials })),
|
|
});
|
|
}
|
|
|
|
export async function listDatabases(): Promise<CoolifyDatabase[]> {
|
|
return coolifyFetch('/databases');
|
|
}
|
|
|
|
export async function getDatabase(uuid: string): Promise<CoolifyDatabase> {
|
|
return coolifyFetch(`/databases/${uuid}`);
|
|
}
|
|
|
|
export async function updateDatabase(uuid: string, patch: Record<string, unknown>): Promise<{ uuid: string }> {
|
|
return coolifyFetch(`/databases/${uuid}`, {
|
|
method: 'PATCH',
|
|
body: JSON.stringify(stripUndefined(patch)),
|
|
});
|
|
}
|
|
|
|
export async function deleteDatabase(
|
|
uuid: string,
|
|
opts: {
|
|
deleteConfigurations?: boolean;
|
|
deleteVolumes?: boolean;
|
|
dockerCleanup?: boolean;
|
|
deleteConnectedNetworks?: boolean;
|
|
} = {}
|
|
): Promise<void> {
|
|
const q = new URLSearchParams();
|
|
if (opts.deleteConfigurations !== undefined) q.set('delete_configurations', String(opts.deleteConfigurations));
|
|
if (opts.deleteVolumes !== undefined) q.set('delete_volumes', String(opts.deleteVolumes));
|
|
if (opts.dockerCleanup !== undefined) q.set('docker_cleanup', String(opts.dockerCleanup));
|
|
if (opts.deleteConnectedNetworks !== undefined) q.set('delete_connected_networks', String(opts.deleteConnectedNetworks));
|
|
const qs = q.toString();
|
|
await coolifyFetch(`/databases/${uuid}${qs ? '?' + qs : ''}`, { method: 'DELETE' });
|
|
}
|
|
|
|
export async function startDatabase(uuid: string): Promise<void> {
|
|
await coolifyFetch(`/databases/${uuid}/start`, { method: 'POST' });
|
|
}
|
|
|
|
export async function stopDatabase(uuid: string): Promise<void> {
|
|
await coolifyFetch(`/databases/${uuid}/stop`, { method: 'POST' });
|
|
}
|
|
|
|
export async function restartDatabase(uuid: string): Promise<void> {
|
|
await coolifyFetch(`/databases/${uuid}/restart`, { method: 'POST' });
|
|
}
|
|
|
|
// ──────────────────────────────────────────────────
|
|
// Applications
|
|
// ──────────────────────────────────────────────────
|
|
|
|
export type CoolifyBuildPack = 'nixpacks' | 'static' | 'dockerfile' | 'dockercompose';
|
|
|
|
export interface CreatePrivateDeployKeyAppOpts {
|
|
projectUuid: string;
|
|
privateKeyUuid: string;
|
|
gitRepository: string; // SSH URL: git@git.vibnai.com:vibn-mark/repo.git
|
|
gitBranch?: string;
|
|
portsExposes: string; // "3000"
|
|
serverUuid?: string;
|
|
environmentName?: string;
|
|
destinationUuid?: string;
|
|
buildPack?: CoolifyBuildPack;
|
|
name?: string;
|
|
description?: string;
|
|
domains?: string; // comma-separated FQDNs
|
|
isAutoDeployEnabled?: boolean;
|
|
isForceHttpsEnabled?: boolean;
|
|
instantDeploy?: boolean;
|
|
installCommand?: string;
|
|
buildCommand?: string;
|
|
startCommand?: string;
|
|
baseDirectory?: string;
|
|
dockerfileLocation?: string;
|
|
manualWebhookSecretGitea?: string;
|
|
}
|
|
|
|
export async function createPrivateDeployKeyApp(
|
|
opts: CreatePrivateDeployKeyAppOpts
|
|
): Promise<{ uuid: string }> {
|
|
const body = stripUndefined({
|
|
project_uuid: opts.projectUuid,
|
|
server_uuid: opts.serverUuid ?? COOLIFY_DEFAULT_SERVER_UUID,
|
|
environment_name: opts.environmentName ?? 'production',
|
|
destination_uuid: opts.destinationUuid ?? COOLIFY_DEFAULT_DESTINATION_UUID,
|
|
private_key_uuid: opts.privateKeyUuid,
|
|
git_repository: opts.gitRepository,
|
|
git_branch: opts.gitBranch ?? 'main',
|
|
build_pack: opts.buildPack ?? 'nixpacks',
|
|
ports_exposes: opts.portsExposes,
|
|
name: opts.name,
|
|
description: opts.description,
|
|
domains: opts.domains,
|
|
is_auto_deploy_enabled: opts.isAutoDeployEnabled ?? true,
|
|
is_force_https_enabled: opts.isForceHttpsEnabled ?? true,
|
|
instant_deploy: opts.instantDeploy ?? true,
|
|
install_command: opts.installCommand,
|
|
build_command: opts.buildCommand,
|
|
start_command: opts.startCommand,
|
|
base_directory: opts.baseDirectory,
|
|
dockerfile_location: opts.dockerfileLocation,
|
|
manual_webhook_secret_gitea: opts.manualWebhookSecretGitea,
|
|
});
|
|
return coolifyFetch('/applications/private-deploy-key', {
|
|
method: 'POST',
|
|
body: JSON.stringify(body),
|
|
});
|
|
}
|
|
|
|
export interface CreatePublicAppOpts {
|
|
projectUuid: string;
|
|
gitRepository: string; // https URL
|
|
gitBranch?: string;
|
|
portsExposes: string;
|
|
serverUuid?: string;
|
|
environmentName?: string;
|
|
destinationUuid?: string;
|
|
buildPack?: CoolifyBuildPack;
|
|
name?: string;
|
|
description?: string;
|
|
domains?: string;
|
|
isAutoDeployEnabled?: boolean;
|
|
isForceHttpsEnabled?: boolean;
|
|
instantDeploy?: boolean;
|
|
}
|
|
|
|
export async function createPublicApp(opts: CreatePublicAppOpts): Promise<{ uuid: string }> {
|
|
const body = stripUndefined({
|
|
project_uuid: opts.projectUuid,
|
|
server_uuid: opts.serverUuid ?? COOLIFY_DEFAULT_SERVER_UUID,
|
|
environment_name: opts.environmentName ?? 'production',
|
|
destination_uuid: opts.destinationUuid ?? COOLIFY_DEFAULT_DESTINATION_UUID,
|
|
git_repository: opts.gitRepository,
|
|
git_branch: opts.gitBranch ?? 'main',
|
|
build_pack: opts.buildPack ?? 'nixpacks',
|
|
ports_exposes: opts.portsExposes,
|
|
name: opts.name,
|
|
description: opts.description,
|
|
domains: opts.domains,
|
|
is_auto_deploy_enabled: opts.isAutoDeployEnabled ?? true,
|
|
is_force_https_enabled: opts.isForceHttpsEnabled ?? true,
|
|
instant_deploy: opts.instantDeploy ?? true,
|
|
});
|
|
return coolifyFetch('/applications/public', {
|
|
method: 'POST',
|
|
body: JSON.stringify(body),
|
|
});
|
|
}
|
|
|
|
export async function updateApplication(
|
|
uuid: string,
|
|
patch: Record<string, unknown>
|
|
): Promise<{ uuid: string }> {
|
|
return coolifyFetch(`/applications/${uuid}`, {
|
|
method: 'PATCH',
|
|
body: JSON.stringify(stripUndefined(patch)),
|
|
});
|
|
}
|
|
|
|
export async function setApplicationDomains(
|
|
uuid: string,
|
|
domains: string[],
|
|
opts: { forceOverride?: boolean } = {}
|
|
): Promise<{ uuid: string }> {
|
|
return updateApplication(uuid, {
|
|
domains: domains.join(','),
|
|
force_domain_override: opts.forceOverride ?? true,
|
|
is_force_https_enabled: true,
|
|
});
|
|
}
|
|
|
|
export async function deleteApplication(
|
|
uuid: string,
|
|
opts: {
|
|
deleteConfigurations?: boolean;
|
|
deleteVolumes?: boolean;
|
|
dockerCleanup?: boolean;
|
|
deleteConnectedNetworks?: boolean;
|
|
} = {}
|
|
): Promise<void> {
|
|
const q = new URLSearchParams();
|
|
if (opts.deleteConfigurations !== undefined) q.set('delete_configurations', String(opts.deleteConfigurations));
|
|
if (opts.deleteVolumes !== undefined) q.set('delete_volumes', String(opts.deleteVolumes));
|
|
if (opts.dockerCleanup !== undefined) q.set('docker_cleanup', String(opts.dockerCleanup));
|
|
if (opts.deleteConnectedNetworks !== undefined) q.set('delete_connected_networks', String(opts.deleteConnectedNetworks));
|
|
const qs = q.toString();
|
|
await coolifyFetch(`/applications/${uuid}${qs ? '?' + qs : ''}`, { method: 'DELETE' });
|
|
}
|
|
|
|
export async function listApplications(): Promise<CoolifyApplication[]> {
|
|
return coolifyFetch('/applications');
|
|
}
|
|
|
|
export async function deployApplication(uuid: string): Promise<{ deployment_uuid: string }> {
|
|
return coolifyFetch(`/applications/${uuid}/deploy`, { method: 'POST' });
|
|
}
|
|
|
|
export async function getApplication(uuid: string): Promise<CoolifyApplication> {
|
|
return coolifyFetch(`/applications/${uuid}`);
|
|
}
|
|
|
|
export async function startApplication(uuid: string): Promise<void> {
|
|
await coolifyFetch(`/applications/${uuid}/start`, { method: 'POST' });
|
|
}
|
|
|
|
export async function stopApplication(uuid: string): Promise<void> {
|
|
await coolifyFetch(`/applications/${uuid}/stop`, { method: 'POST' });
|
|
}
|
|
|
|
export async function restartApplication(uuid: string): Promise<void> {
|
|
await coolifyFetch(`/applications/${uuid}/restart`, { method: 'POST' });
|
|
}
|
|
|
|
export async function getDeploymentLogs(deploymentUuid: string): Promise<{ logs: string }> {
|
|
return coolifyFetch(`/deployments/${deploymentUuid}/logs`);
|
|
}
|
|
|
|
export async function listApplicationDeployments(uuid: string): Promise<CoolifyDeployment[]> {
|
|
// Coolify v4 nests this under /deployments/applications/{uuid}
|
|
// and returns { count, deployments }. Normalize to a flat array.
|
|
const raw = await coolifyFetch(`/deployments/applications/${uuid}?take=50`);
|
|
if (Array.isArray(raw)) return raw as CoolifyDeployment[];
|
|
return (raw?.deployments ?? []) as CoolifyDeployment[];
|
|
}
|
|
|
|
// ──────────────────────────────────────────────────
|
|
// Legacy monorepo helper (still used by older flows)
|
|
// ──────────────────────────────────────────────────
|
|
|
|
export async function createMonorepoAppService(opts: {
|
|
projectUuid: string;
|
|
appName: string;
|
|
gitRepo: string;
|
|
gitBranch?: string;
|
|
domain: string;
|
|
serverUuid?: string;
|
|
environmentName?: string;
|
|
}): Promise<CoolifyApplication> {
|
|
const {
|
|
projectUuid, appName, gitRepo,
|
|
gitBranch = 'main',
|
|
domain,
|
|
serverUuid = COOLIFY_DEFAULT_SERVER_UUID,
|
|
environmentName = 'production',
|
|
} = opts;
|
|
return coolifyFetch(`/applications`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
project_uuid: projectUuid,
|
|
name: appName,
|
|
git_repository: gitRepo,
|
|
git_branch: gitBranch,
|
|
server_uuid: serverUuid,
|
|
environment_name: environmentName,
|
|
build_pack: 'nixpacks',
|
|
build_command: `pnpm install && turbo run build --filter=${appName}`,
|
|
start_command: `turbo run start --filter=${appName}`,
|
|
ports_exposes: '3000',
|
|
fqdn: `https://${domain}`,
|
|
}),
|
|
});
|
|
}
|
|
|
|
// ──────────────────────────────────────────────────
|
|
// Environment variables
|
|
// ──────────────────────────────────────────────────
|
|
|
|
export async function listApplicationEnvs(uuid: string): Promise<CoolifyEnvVar[]> {
|
|
return coolifyFetch(`/applications/${uuid}/envs`);
|
|
}
|
|
|
|
export async function upsertApplicationEnv(
|
|
uuid: string,
|
|
env: CoolifyEnvVar & { is_preview?: boolean }
|
|
): Promise<CoolifyEnvVar> {
|
|
try {
|
|
return await coolifyFetch(`/applications/${uuid}/envs`, {
|
|
method: 'PATCH',
|
|
body: JSON.stringify(env),
|
|
});
|
|
} catch (err) {
|
|
const msg = err instanceof Error ? err.message : String(err);
|
|
if (msg.includes('404') || msg.includes('405')) {
|
|
return coolifyFetch(`/applications/${uuid}/envs`, {
|
|
method: 'POST',
|
|
body: JSON.stringify(env),
|
|
});
|
|
}
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
export async function deleteApplicationEnv(uuid: string, key: string): Promise<void> {
|
|
await coolifyFetch(`/applications/${uuid}/envs/${encodeURIComponent(key)}`, {
|
|
method: 'DELETE',
|
|
});
|
|
}
|
|
|
|
// ──────────────────────────────────────────────────
|
|
// Services (raw Coolify service resources)
|
|
// ──────────────────────────────────────────────────
|
|
|
|
export interface CoolifyService {
|
|
uuid: string;
|
|
name: string;
|
|
status?: string;
|
|
project_uuid?: string;
|
|
environment?: { project_uuid?: string; project?: { uuid?: string } };
|
|
}
|
|
|
|
export async function listServices(): Promise<CoolifyService[]> {
|
|
return coolifyFetch('/services');
|
|
}
|
|
|
|
export async function getService(uuid: string): Promise<CoolifyService> {
|
|
return coolifyFetch(`/services/${uuid}`);
|
|
}
|
|
|
|
export async function createService(opts: {
|
|
projectUuid: string;
|
|
type: string; // e.g. "pocketbase", "authentik", "zitadel"
|
|
name: string;
|
|
description?: string;
|
|
serverUuid?: string;
|
|
environmentName?: string;
|
|
destinationUuid?: string;
|
|
instantDeploy?: boolean;
|
|
}): Promise<{ uuid: string }> {
|
|
const body = stripUndefined({
|
|
project_uuid: opts.projectUuid,
|
|
server_uuid: opts.serverUuid ?? COOLIFY_DEFAULT_SERVER_UUID,
|
|
environment_name: opts.environmentName ?? 'production',
|
|
destination_uuid: opts.destinationUuid ?? COOLIFY_DEFAULT_DESTINATION_UUID,
|
|
type: opts.type,
|
|
name: opts.name,
|
|
description: opts.description,
|
|
instant_deploy: opts.instantDeploy ?? true,
|
|
});
|
|
return coolifyFetch('/services', { method: 'POST', body: JSON.stringify(body) });
|
|
}
|
|
|
|
export async function deleteService(
|
|
uuid: string,
|
|
opts: {
|
|
deleteConfigurations?: boolean;
|
|
deleteVolumes?: boolean;
|
|
dockerCleanup?: boolean;
|
|
deleteConnectedNetworks?: boolean;
|
|
} = {}
|
|
): Promise<void> {
|
|
const q = new URLSearchParams();
|
|
if (opts.deleteConfigurations !== undefined) q.set('delete_configurations', String(opts.deleteConfigurations));
|
|
if (opts.deleteVolumes !== undefined) q.set('delete_volumes', String(opts.deleteVolumes));
|
|
if (opts.dockerCleanup !== undefined) q.set('docker_cleanup', String(opts.dockerCleanup));
|
|
if (opts.deleteConnectedNetworks !== undefined) q.set('delete_connected_networks', String(opts.deleteConnectedNetworks));
|
|
const qs = q.toString();
|
|
await coolifyFetch(`/services/${uuid}${qs ? '?' + qs : ''}`, { method: 'DELETE' });
|
|
}
|
|
|
|
// ──────────────────────────────────────────────────
|
|
// Tenant helpers — every endpoint that returns an app/db/service runs
|
|
// through one of these so cross-project access is impossible.
|
|
// ──────────────────────────────────────────────────
|
|
|
|
export function projectUuidOf(
|
|
resource: CoolifyApplication | CoolifyDatabase | CoolifyService
|
|
): string | null {
|
|
return (
|
|
resource.project_uuid ??
|
|
resource.environment?.project_uuid ??
|
|
resource.environment?.project?.uuid ??
|
|
null
|
|
);
|
|
}
|
|
|
|
export class TenantError extends Error {
|
|
status = 403 as const;
|
|
}
|
|
|
|
export async function getApplicationInProject(
|
|
appUuid: string,
|
|
expectedProjectUuid: string
|
|
): Promise<CoolifyApplication> {
|
|
const app = await getApplication(appUuid);
|
|
const actualProject = projectUuidOf(app);
|
|
if (!actualProject || actualProject !== expectedProjectUuid) {
|
|
throw new TenantError(
|
|
`Application ${appUuid} does not belong to project ${expectedProjectUuid}`
|
|
);
|
|
}
|
|
return app;
|
|
}
|
|
|
|
export async function getDatabaseInProject(
|
|
dbUuid: string,
|
|
expectedProjectUuid: string
|
|
): Promise<CoolifyDatabase> {
|
|
const db = await getDatabase(dbUuid);
|
|
const actualProject = projectUuidOf(db);
|
|
if (!actualProject || actualProject !== expectedProjectUuid) {
|
|
throw new TenantError(
|
|
`Database ${dbUuid} does not belong to project ${expectedProjectUuid}`
|
|
);
|
|
}
|
|
return db;
|
|
}
|
|
|
|
export async function getServiceInProject(
|
|
serviceUuid: string,
|
|
expectedProjectUuid: string
|
|
): Promise<CoolifyService> {
|
|
const svc = await getService(serviceUuid);
|
|
const actualProject = projectUuidOf(svc);
|
|
if (!actualProject || actualProject !== expectedProjectUuid) {
|
|
throw new TenantError(
|
|
`Service ${serviceUuid} does not belong to project ${expectedProjectUuid}`
|
|
);
|
|
}
|
|
return svc;
|
|
}
|
|
|
|
export async function listApplicationsInProject(
|
|
projectUuid: string
|
|
): Promise<CoolifyApplication[]> {
|
|
const all = await listApplications();
|
|
return all.filter(a => projectUuidOf(a) === projectUuid);
|
|
}
|
|
|
|
export async function listDatabasesInProject(
|
|
projectUuid: string
|
|
): Promise<CoolifyDatabase[]> {
|
|
const all = await listDatabases();
|
|
return all.filter(d => projectUuidOf(d) === projectUuid);
|
|
}
|
|
|
|
export async function listServicesInProject(
|
|
projectUuid: string
|
|
): Promise<CoolifyService[]> {
|
|
const all = await listServices();
|
|
return all.filter(s => projectUuidOf(s) === projectUuid);
|
|
}
|
|
|
|
// ──────────────────────────────────────────────────
|
|
// util
|
|
// ──────────────────────────────────────────────────
|
|
|
|
function stripUndefined<T extends Record<string, unknown>>(obj: T): Partial<T> {
|
|
const out: Partial<T> = {};
|
|
for (const [k, v] of Object.entries(obj)) {
|
|
if (v !== undefined) (out as Record<string, unknown>)[k] = v;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
export { COOLIFY_DEFAULT_SERVER_UUID, COOLIFY_DEFAULT_DESTINATION_UUID };
|