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

@@ -1,10 +1,15 @@
/**
* GET /api/workspaces/[slug]/apps/[uuid]/envs — list env vars
* PATCH /api/workspaces/[slug]/apps/[uuid]/envs — upsert one env var
* body: { key, value, is_preview?, is_build_time?, is_literal?, is_multiline? }
* body: { key, value, is_preview?, is_literal?, is_multiline?, is_shown_once? }
* DELETE /api/workspaces/[slug]/apps/[uuid]/envs?key=FOO — delete one env var
*
* Tenant boundary: the app must belong to the workspace's Coolify project.
*
* NOTE: `is_build_time` is **not** a writable flag in Coolify v4 — it's a
* derived read-only attribute. We silently drop it from incoming request
* bodies for back-compat with older agents; the value is computed by
* Coolify at build time based on Dockerfile ARG usage.
*/
import { NextResponse } from 'next/server';
@@ -66,7 +71,11 @@ export async function GET(
key: e.key,
value: reveal ? e.value : maskValue(e.value),
isPreview: e.is_preview ?? false,
isBuildTime: e.is_build_time ?? false,
// Coolify spells the read-only build-time flag two different ways
// depending on version — `is_buildtime` (new, one word) and
// `is_build_time` (old, underscored). Fall through both.
isBuildTime: e.is_buildtime ?? e.is_build_time ?? false,
isRuntime: e.is_runtime ?? true,
isLiteral: e.is_literal ?? false,
isMultiline: e.is_multiline ?? false,
})),
@@ -91,9 +100,11 @@ export async function PATCH(
key?: string;
value?: string;
is_preview?: boolean;
/** @deprecated silently dropped — Coolify no longer accepts this on write. */
is_build_time?: boolean;
is_literal?: boolean;
is_multiline?: boolean;
is_shown_once?: boolean;
};
try {
body = await request.json();
@@ -110,11 +121,22 @@ export async function PATCH(
key: body.key,
value: body.value,
is_preview: body.is_preview ?? false,
is_build_time: body.is_build_time ?? false,
is_literal: body.is_literal ?? false,
is_multiline: body.is_multiline ?? false,
is_shown_once: body.is_shown_once ?? false,
});
return NextResponse.json({
ok: true,
key: env.key,
// Soft-deprecation signal so the caller's agent can learn to stop
// sending the flag without hard-breaking today.
warnings:
body.is_build_time !== undefined
? [
'is_build_time is ignored — Coolify derives build-vs-runtime from Dockerfile ARG usage. Omit this field going forward.',
]
: undefined,
});
return NextResponse.json({ ok: true, key: env.key });
} catch (err) {
return NextResponse.json(
{ error: 'Coolify request failed', details: String(err) },