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:
57
app/api/workspaces/[slug]/keys/[keyId]/reveal/route.ts
Normal file
57
app/api/workspaces/[slug]/keys/[keyId]/reveal/route.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* POST /api/workspaces/[slug]/keys/[keyId]/reveal
|
||||
*
|
||||
* Returns the plaintext `vibn_sk_...` token for an active workspace key.
|
||||
*
|
||||
* Intentionally restricted to SESSION principals. An API-key principal
|
||||
* cannot reveal keys — this prevents a leaked agent token from being
|
||||
* used to exfiltrate sibling keys. We use POST (not GET) to keep the
|
||||
* secret out of server logs / the browser history / referrer headers.
|
||||
*
|
||||
* Returns 409 with { revealable: false } for legacy keys minted before
|
||||
* the key_encrypted column existed — those plaintexts were never stored
|
||||
* and can never be recovered. The caller should prompt the user to
|
||||
* rotate (revoke + mint new).
|
||||
*/
|
||||
|
||||
import { NextResponse } from 'next/server';
|
||||
import {
|
||||
requireWorkspacePrincipal,
|
||||
revealWorkspaceApiKey,
|
||||
} from '@/lib/auth/workspace-auth';
|
||||
|
||||
export async function POST(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ slug: string; keyId: string }> },
|
||||
) {
|
||||
const { slug, keyId } = await params;
|
||||
const principal = await requireWorkspacePrincipal(request, { targetSlug: slug });
|
||||
if (principal instanceof NextResponse) return principal;
|
||||
|
||||
if (principal.source !== 'session') {
|
||||
return NextResponse.json(
|
||||
{ error: 'API keys can only be revealed from a signed-in session' },
|
||||
{ status: 403 },
|
||||
);
|
||||
}
|
||||
|
||||
const revealed = await revealWorkspaceApiKey(principal.workspace.id, keyId);
|
||||
if (!revealed) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error:
|
||||
'Key not found, already revoked, or was minted before reveal was enabled. ' +
|
||||
'Rotate the key (revoke + create new) if you need the plaintext.',
|
||||
revealable: false,
|
||||
},
|
||||
{ status: 409 },
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
id: revealed.id,
|
||||
name: revealed.name,
|
||||
prefix: revealed.prefix,
|
||||
token: revealed.token,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user