Skip to content

feat: add env var drift detection endpoints for Vercel deployments#756

Open
kilo-code-bot[bot] wants to merge 2 commits intomainfrom
add-vercel-env-drift-check-endpoints
Open

feat: add env var drift detection endpoints for Vercel deployments#756
kilo-code-bot[bot] wants to merge 2 commits intomainfrom
add-vercel-env-drift-check-endpoints

Conversation

@kilo-code-bot
Copy link
Contributor

@kilo-code-bot kilo-code-bot bot commented Mar 3, 2026

Summary

  • Adds GET /api/env-keys — returns a map of runtime process.env keys to SHA-256 hashed values (excluding Vercel-managed vars like VERCEL_URL, VERCEL_GIT_COMMIT_SHA, etc.), protected by ENV_CHECK_SECRET bearer token. Undefined values are hashed as "__undefined__".
  • Adds GET /api/env-check — fetches /api/env-keys from all three Vercel deployments (kilocode-app, kilocode-global-app, kilocode-app-staging), compares both key presence and hashed values, and reports which keys are missing or have mismatched values across deployments.
  • Never exposes actual env values or hashes in the /api/env-check response — only key names and drift type (missing or mismatched).

Required env vars

Variable Purpose
ENV_CHECK_SECRET Shared bearer token protecting both endpoints
KILOCODE_APP_URL Base URL for the kilocode-app deployment
KILOCODE_GLOBAL_APP_URL Base URL for the kilocode-global-app deployment
KILOCODE_STAGING_APP_URL Base URL for the kilocode-app-staging deployment

Response examples

/api/env-keys — returns object mapping env key names to SHA-256 hashed values:

{"APP_BUILDER_AUTH_TOKEN": "a1b2c3...", "APP_BUILDER_URL": "d4e5f6...", ...}

/api/env-check — all match:

{ "status": "ok", "message": "All deployments have matching env vars", "deployments": ["kilocode-app", "kilocode-global-app", "kilocode-app-staging"] }

/api/env-check — drift detected (missing key):

{ "status": "drift", "message": "Found 2 env var(s) with drift across deployments", "differences": [{ "key": "SOME_NEW_VAR", "missingFrom": ["kilocode-app-staging"] }] }

/api/env-check — drift detected (value mismatch):

{ "status": "drift", "message": "Found 1 env var(s) with drift across deployments", "differences": [{ "key": "DATABASE_URL", "mismatchBetween": ["kilocode-app", "kilocode-global-app", "kilocode-app-staging"] }] }

Built for Mark by Kilo for Slack

Add two endpoints to detect environment variable drift across kilocode-app,
kilocode-global-app, and kilocode-app-staging deployments:

- GET /api/env-keys: returns runtime process.env keys (filtered to exclude
  Vercel-managed vars), protected by ENV_CHECK_SECRET bearer token
- GET /api/env-check: fetches /api/env-keys from all three deployments,
  compares key sets, and reports missing keys per deployment
};
}

const keys: string[] = await response.json();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[WARNING]: No validation of the response body

The response from /api/env-keys is cast to string[] without any runtime validation. If a remote deployment returns an error object (e.g., {"error": "Unauthorized"}) that still has a 200 status, or if the response shape changes, this will silently produce incorrect results — new Set(...) on a non-array could behave unexpectedly.

Consider adding a runtime check, e.g.:

const body = await response.json();
if (!Array.isArray(body)) {
  return { name: deployment.name, keys: null, error: `Unexpected response format` };
}

secret: string
): Promise<{ name: string; keys: Set<string> | null; error: string | null }> {
try {
const response = await fetch(`${deployment.url}/api/env-keys`, {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[WARNING]: No fetch timeout

The fetch call has no timeout configured. If a deployment is unresponsive, this request will hang indefinitely since Promise.all waits for all promises. Consider adding an AbortSignal.timeout() to prevent long hangs:

const response = await fetch(`${deployment.url}/api/env-keys`, {
  headers: { Authorization: `Bearer ${secret}` },
  signal: AbortSignal.timeout(10_000),
});

return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}

const configuredDeployments = DEPLOYMENTS.filter(d => d.url);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[WARNING]: No minimum deployment count check

If only one deployment URL is configured, the drift check runs but has nothing meaningful to compare against — it will always report "ok" since there's no second deployment to differ from. Consider requiring at least 2 configured deployments for a meaningful comparison:

if (configuredDeployments.length < 2) {
  return NextResponse.json(
    { error: 'At least 2 deployment URLs must be configured for drift detection' },
    { status: 500 }
  );
}

return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}

const keys = Object.keys(process.env)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[WARNING]: Exposes system-level environment variable names

Object.keys(process.env) includes not just application env vars but also OS/system-level variables like PATH, HOME, USER, HOSTNAME, LANG, NODE_VERSION, etc. These can leak infrastructure details (OS type, Node version, container info). The IGNORED_KEYS set only filters Vercel-managed vars.

Consider either:

  1. Using an allowlist approach (only return keys matching a known prefix like NEXT_PUBLIC_, or keys from your .env schema)
  2. Adding a broader denylist that also excludes common system vars
  3. Filtering to only keys that start with known application prefixes

@kilo-code-bot
Copy link
Contributor Author

kilo-code-bot bot commented Mar 3, 2026

Code Review Summary

Status: 7 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 6
SUGGESTION 1
Issue Details (click to expand)

WARNING

File Line Issue
src/app/api/env-check/route.ts N/A No validation of the response body from /api/env-keys
src/app/api/env-check/route.ts 50 No fetch timeout — requests to deployments could hang indefinitely
src/app/api/env-check/route.ts 82 Timing-unsafe secret comparison — use crypto.timingSafeEqual consistent with rest of codebase
src/app/api/env-check/route.ts 86 No minimum deployment count check
src/app/api/env-keys/route.ts 45 Timing-unsafe secret comparison — use crypto.timingSafeEqual consistent with rest of codebase
src/app/api/env-keys/route.ts 49 Exposes system-level environment variable names

SUGGESTION

File Line Issue
src/app/api/env-keys/route.ts 55 Dead fallback ?? '__undefined__'Object.keys(process.env) guarantees the key exists
Other Observations (not in diff)

Issues found that cannot receive inline comments:

File Line Issue
N/A N/A No tests for either endpoint — consider adding tests for auth, drift detection, and error handling
Files Reviewed (2 files)
  • src/app/api/env-check/route.ts - 4 issues
  • src/app/api/env-keys/route.ts - 3 issues

Fix these issues in Kilo Cloud

/api/env-keys now returns key→SHA-256(value) pairs instead of just keys.
/api/env-check compares both key presence and hashed values, reporting
missing keys and value mismatches without exposing hashes in the response.
request: NextRequest
): Promise<NextResponse<EnvCheckSuccess | EnvCheckDrift | EnvCheckError | { error: string }>> {
const authHeader = request.headers.get('authorization');
if (!ENV_CHECK_SECRET || authHeader !== `Bearer ${ENV_CHECK_SECRET}`) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[WARNING]: Timing-unsafe secret comparison

The !== operator is vulnerable to timing attacks. The rest of the codebase consistently uses crypto.timingSafeEqual for secret comparisons (see src/lib/slack/verify-request.ts, src/lib/integrations/platforms/gitlab/adapter.ts, src/app/discord/webhook/route.ts, etc.).

Use crypto.timingSafeEqual(Buffer.from(authHeader ?? ''), Buffer.from(Bearer ${ENV_CHECK_SECRET})) instead, with a length check guard to prevent the timingSafeEqual throw on mismatched lengths.

request: NextRequest
): Promise<NextResponse<{ error: string } | Record<string, string>>> {
const authHeader = request.headers.get('authorization');
if (!ENV_CHECK_SECRET || authHeader !== `Bearer ${ENV_CHECK_SECRET}`) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[WARNING]: Timing-unsafe secret comparison

Same issue as in env-check/route.ts — the !== operator leaks timing information about the secret. Use crypto.timingSafeEqual with a length guard, consistent with the rest of the codebase.


const hashedEntries: Record<string, string> = {};
for (const key of keys) {
hashedEntries[key] = sha256(process.env[key] ?? '__undefined__');
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SUGGESTION]: Dead fallback — ?? '__undefined__' can never trigger

Since keys comes from Object.keys(process.env), every key is guaranteed to exist in process.env, so process.env[key] will always be a string (never undefined). The ?? '__undefined__' fallback is dead code and could mislead future readers into thinking undefined values are a real possibility here.

Suggested change
hashedEntries[key] = sha256(process.env[key] ?? '__undefined__');
hashedEntries[key] = sha256(process.env[key] ?? '');

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants