fix: require server env vars (#129)

This commit is contained in:
2026-06-14 14:50:17 +01:00
committed by GitHub
parent 789b6a285b
commit 083f6d2695
5 changed files with 187 additions and 36 deletions

View File

@@ -0,0 +1,98 @@
import { describe, expect, test } from "bun:test"
import { ensureEnv } from "./env.ts"
describe("ensureEnv", () => {
test("returns trimmed required env values", () => {
const env = ensureEnv({
BETTER_AUTH_SECRET: " auth-secret ",
CREDENTIAL_ENCRYPTION_KEY: " credential-key ",
DATABASE_URL: " postgres://example ",
EXA_API_KEY: " exa-key ",
GOOGLE_MAPS_API_KEY: " google-maps-key ",
OPENROUTER_API_KEY: " openrouter-key ",
OPENROUTER_MODEL: " model-name ",
TFL_API_KEY: " tfl-key ",
WEATHERKIT_KEY_ID: " weather-key-id ",
WEATHERKIT_PRIVATE_KEY: " weather-private-key ",
WEATHERKIT_SERVICE_ID: " weather-service-id ",
WEATHERKIT_TEAM_ID: " weather-team-id ",
})
expect(env).toEqual({
betterAuthSecret: "auth-secret",
credentialEncryptionKey: "credential-key",
databaseUrl: "postgres://example",
exaApiKey: "exa-key",
googleMapsApiKey: "google-maps-key",
openrouterApiKey: "openrouter-key",
openrouterModel: "model-name",
tflApiKey: "tfl-key",
weatherkitKeyId: "weather-key-id",
weatherkitPrivateKey: "weather-private-key",
weatherkitServiceId: "weather-service-id",
weatherkitTeamId: "weather-team-id",
})
})
test("does not allow the old Google Maps MCP fallback key", () => {
expect(() =>
ensureEnv({
BETTER_AUTH_SECRET: "auth-secret",
CREDENTIAL_ENCRYPTION_KEY: "credential-key",
DATABASE_URL: "postgres://example",
EXA_API_KEY: "exa-key",
GOOGLE_MAPS_MCP_API_KEY: "google-maps-mcp-key",
OPENROUTER_API_KEY: "openrouter-key",
TFL_API_KEY: "tfl-key",
WEATHERKIT_KEY_ID: "weather-key-id",
WEATHERKIT_PRIVATE_KEY: "weather-private-key",
WEATHERKIT_SERVICE_ID: "weather-service-id",
WEATHERKIT_TEAM_ID: "weather-team-id",
}),
).toThrow("Missing required environment variables: GOOGLE_MAPS_API_KEY")
})
test("allows openrouter model to be omitted", () => {
const env = ensureEnv({
BETTER_AUTH_SECRET: "auth-secret",
CREDENTIAL_ENCRYPTION_KEY: "credential-key",
DATABASE_URL: "postgres://example",
EXA_API_KEY: "exa-key",
GOOGLE_MAPS_API_KEY: "google-maps-key",
OPENROUTER_API_KEY: "openrouter-key",
TFL_API_KEY: "tfl-key",
WEATHERKIT_KEY_ID: "weather-key-id",
WEATHERKIT_PRIVATE_KEY: "weather-private-key",
WEATHERKIT_SERVICE_ID: "weather-service-id",
WEATHERKIT_TEAM_ID: "weather-team-id",
})
expect(env.googleMapsApiKey).toBe("google-maps-key")
expect(env.openrouterModel).toBeUndefined()
})
test("throws with all missing required env names", () => {
expect(() => ensureEnv({})).toThrow(
"Missing required environment variables: BETTER_AUTH_SECRET, CREDENTIAL_ENCRYPTION_KEY, DATABASE_URL, EXA_API_KEY, OPENROUTER_API_KEY, TFL_API_KEY, WEATHERKIT_PRIVATE_KEY, WEATHERKIT_KEY_ID, WEATHERKIT_TEAM_ID, WEATHERKIT_SERVICE_ID, GOOGLE_MAPS_API_KEY",
)
})
test("treats whitespace-only values as missing", () => {
expect(() =>
ensureEnv({
BETTER_AUTH_SECRET: "auth-secret",
CREDENTIAL_ENCRYPTION_KEY: "credential-key",
DATABASE_URL: "postgres://example",
EXA_API_KEY: " ",
GOOGLE_MAPS_API_KEY: "google-maps-key",
OPENROUTER_API_KEY: "openrouter-key",
TFL_API_KEY: "tfl-key",
WEATHERKIT_KEY_ID: "weather-key-id",
WEATHERKIT_PRIVATE_KEY: "weather-private-key",
WEATHERKIT_SERVICE_ID: "weather-service-id",
WEATHERKIT_TEAM_ID: "weather-team-id",
}),
).toThrow("Missing required environment variables: EXA_API_KEY")
})
})

View File

@@ -0,0 +1,69 @@
export interface ServerEnv {
betterAuthSecret: string
credentialEncryptionKey: string
databaseUrl: string
exaApiKey: string
googleMapsApiKey: string
openrouterApiKey: string
openrouterModel: string | undefined
tflApiKey: string
weatherkitKeyId: string
weatherkitPrivateKey: string
weatherkitServiceId: string
weatherkitTeamId: string
}
export function ensureEnv(env: Record<string, string | undefined>): ServerEnv {
const missing: string[] = []
const betterAuthSecret = readRequiredEnv(env, "BETTER_AUTH_SECRET", missing)
const credentialEncryptionKey = readRequiredEnv(env, "CREDENTIAL_ENCRYPTION_KEY", missing)
const databaseUrl = readRequiredEnv(env, "DATABASE_URL", missing)
const exaApiKey = readRequiredEnv(env, "EXA_API_KEY", missing)
const openrouterApiKey = readRequiredEnv(env, "OPENROUTER_API_KEY", missing)
const tflApiKey = readRequiredEnv(env, "TFL_API_KEY", missing)
const weatherkitPrivateKey = readRequiredEnv(env, "WEATHERKIT_PRIVATE_KEY", missing)
const weatherkitKeyId = readRequiredEnv(env, "WEATHERKIT_KEY_ID", missing)
const weatherkitTeamId = readRequiredEnv(env, "WEATHERKIT_TEAM_ID", missing)
const weatherkitServiceId = readRequiredEnv(env, "WEATHERKIT_SERVICE_ID", missing)
const googleMapsApiKey = readRequiredEnv(env, "GOOGLE_MAPS_API_KEY", missing)
if (missing.length > 0) {
throw new Error(`Missing required environment variables: ${missing.join(", ")}`)
}
return {
betterAuthSecret,
credentialEncryptionKey,
databaseUrl,
exaApiKey,
googleMapsApiKey,
openrouterApiKey,
openrouterModel: readOptionalEnv(env, "OPENROUTER_MODEL"),
tflApiKey,
weatherkitKeyId,
weatherkitPrivateKey,
weatherkitServiceId,
weatherkitTeamId,
}
}
function readRequiredEnv(
env: Record<string, string | undefined>,
name: string,
missing: string[],
): string {
const value = readOptionalEnv(env, name)
if (!value) {
missing.push(name)
}
return value ?? ""
}
function readOptionalEnv(
env: Record<string, string | undefined>,
name: string,
): string | undefined {
const value = env[name]?.trim()
return value ? value : undefined
}