fix(coolify): strip is_build_time from env writes; add reveal + GCS

Coolify v4's POST/PATCH /applications/{uuid}/envs only accepts key,
value, is_preview, is_literal, is_multiline, is_shown_once. Sending
is_build_time triggers a 422 "This field is not allowed." — it's now
a derived read-only flag (is_buildtime) computed from Dockerfile ARG
usage. Breaks agents trying to upsert env vars.

Three-layer fix so this can't regress:
  - lib/coolify.ts: COOLIFY_ENV_WRITE_FIELDS whitelist enforced at the
    network boundary, regardless of caller shape
  - app/api/workspaces/[slug]/apps/[uuid]/envs: stops forwarding the
    field; returns a deprecation warning when callers send it; GET
    reads both is_buildtime and is_build_time for version parity
  - app/api/mcp/route.ts: same treatment in the MCP dispatcher;
    AI_CAPABILITIES.md doc corrected

Also bundles (not related to the above):
  - Workspace API keys are now revealable from settings. New
    key_encrypted column stores AES-256-GCM(VIBN_SECRETS_KEY, token).
    POST /api/workspaces/[slug]/keys/[keyId]/reveal returns plaintext
    for session principals only; API-key principals cannot reveal
    siblings. Legacy keys stay valid for auth but can't reveal.
  - P5.3 Object storage: lib/gcp/storage.ts + lib/workspace-gcs.ts
    idempotently provision a per-workspace GCS bucket, service
    account, IAM binding and HMAC key. New POST /api/workspaces/
    [slug]/storage/buckets endpoint. Migration script + smoke test
    included. Proven end-to-end against prod master-ai-484822.

Made-with: Cursor
This commit is contained in:
2026-04-23 11:46:50 -07:00
parent 651ddf1e11
commit 3192e0f7b9
14 changed files with 1794 additions and 37 deletions

View File

@@ -61,15 +61,68 @@ export interface CoolifyApplication {
environment?: { id?: number; project_uuid?: string; project?: { uuid?: string } };
}
/**
* Coolify env var, as returned by GET /applications/{uuid}/envs.
*
* NOTE on build-time vars: Coolify removed `is_build_time` from the
* **write** schema some time ago. The flag is now a derived read-only
* attribute (`is_buildtime`, one word) computed from whether the var
* is referenced as a Dockerfile ARG. `is_build_time` (underscored) is
* kept here only to tolerate very old read responses — never send it
* on POST/PATCH. See `COOLIFY_ENV_WRITE_FIELDS` below.
*/
export interface CoolifyEnvVar {
uuid?: string;
key: string;
value: string;
is_preview?: boolean;
/** @deprecated read-only, derived server-side. Do not send on write. */
is_build_time?: boolean;
/** Newer one-word spelling of the same derived read-only flag. */
is_buildtime?: boolean;
is_runtime?: boolean;
is_literal?: boolean;
is_multiline?: boolean;
is_shown_once?: boolean;
is_shared?: boolean;
}
/**
* The only fields Coolify v4 accepts on POST/PATCH /applications/{uuid}/envs.
* Any other field (notably `is_build_time`) triggers a 422
* "This field is not allowed." Build-time vs runtime is no longer a
* writable flag — Coolify infers it at build time.
*
* Source of truth:
* https://coolify.io/docs/api-reference/api/operations/update-env-by-application-uuid
* https://coolify.io/docs/api-reference/api/operations/create-env-by-application-uuid
*/
const COOLIFY_ENV_WRITE_FIELDS = [
'key',
'value',
'is_preview',
'is_literal',
'is_multiline',
'is_shown_once',
] as const;
type CoolifyEnvWritePayload = {
key: string;
value: string;
is_preview?: boolean;
is_literal?: boolean;
is_multiline?: boolean;
is_shown_once?: boolean;
};
function toCoolifyEnvWritePayload(env: CoolifyEnvVar): CoolifyEnvWritePayload {
const src = env as unknown as Record<string, unknown>;
const out: Record<string, unknown> = {};
for (const k of COOLIFY_ENV_WRITE_FIELDS) {
const v = src[k];
if (v !== undefined) out[k] = v;
}
return out as CoolifyEnvWritePayload;
}
export interface CoolifyPrivateKey {
@@ -539,17 +592,22 @@ export async function upsertApplicationEnv(
uuid: string,
env: CoolifyEnvVar & { is_preview?: boolean }
): Promise<CoolifyEnvVar> {
// Strip any read-only/derived fields (`is_build_time`, `is_buildtime`,
// `is_runtime`, `is_shared`, `uuid`) before sending — Coolify returns
// 422 "This field is not allowed." for anything outside the write
// schema. See COOLIFY_ENV_WRITE_FIELDS.
const payload = toCoolifyEnvWritePayload(env);
try {
return await coolifyFetch(`/applications/${uuid}/envs`, {
method: 'PATCH',
body: JSON.stringify(env),
body: JSON.stringify(payload),
});
} 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),
body: JSON.stringify(payload),
});
}
throw err;