161 lines
4.6 KiB
JavaScript
161 lines
4.6 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import { readFileSync } from "node:fs";
|
|
|
|
const envExamplePath = ".env.example";
|
|
const source = readFileSync(envExamplePath, "utf8");
|
|
const lines = source.split(/\r?\n/);
|
|
|
|
const requiredKeys = [
|
|
"AUTH_SECRET",
|
|
"AUTH_URL",
|
|
"NEXTAUTH_URL",
|
|
"NEXT_PUBLIC_APP_URL",
|
|
"AUTH_TRUST_HOST",
|
|
"GOOGLE_CLIENT_ID",
|
|
"GOOGLE_CLIENT_SECRET",
|
|
"ADMIN_EMAIL",
|
|
"ADMIN_PASSWORD",
|
|
"RESEND_API_KEY",
|
|
"RESEND_FROM_EMAIL",
|
|
"EMAIL_VERIFICATION_SECRET",
|
|
"CRON_SECRET",
|
|
"POSTGRES_URL",
|
|
"POSTGRES_MAX_CONNECTIONS",
|
|
"POSTGRES_IDLE_TIMEOUT_SECONDS",
|
|
"POSTGRES_CONNECT_TIMEOUT_SECONDS",
|
|
"BLOB_READ_WRITE_TOKEN",
|
|
"S3_ENDPOINT",
|
|
"S3_REGION",
|
|
"S3_BUCKET",
|
|
"S3_ACCESS_KEY_ID",
|
|
"S3_SECRET_ACCESS_KEY",
|
|
"S3_PUBLIC_URL",
|
|
"AI_GATEWAY_API_KEY",
|
|
"NEXT_PUBLIC_DEFAULT_CHAT_MODEL",
|
|
"DASHSCOPE_BASE_URL",
|
|
"DASHSCOPE_API_KEY",
|
|
"GTP5_API_URL",
|
|
"GTP5_API_KEY",
|
|
"GTP5_MODEL",
|
|
"FAL_KEY",
|
|
"FAL_API_KEY",
|
|
"FAL_BASE_URL",
|
|
"FAL_MODEL",
|
|
"FAL_REALTIME_MODEL",
|
|
"REPLICATE_API_TOKEN",
|
|
"BFL_API_KEY",
|
|
"BFL_KEY",
|
|
"BFL_BASE_URL",
|
|
"BFL_MODEL",
|
|
"RUNWARE_MODEL",
|
|
"TRIPO_API_KEY",
|
|
"TRIPO_BASE_URL",
|
|
"WORLDLABS_API_KEY",
|
|
"WORLDLABS_BASE_URL",
|
|
"WORLDLABS_IMPORT_TOKEN",
|
|
"WORLDLABS_KEY_POOL_IMPORT_TOKEN",
|
|
"KITTYCAD_API_TOKEN",
|
|
"KITTYCAD_API_KEY",
|
|
"KITTYCAD_BASE_URL",
|
|
"ZOO_API_TOKEN",
|
|
"ZOO_BASE_URL",
|
|
"RUXA_CALLBACK_URL",
|
|
"RUXA_VIDEO_CALLBACK_URL",
|
|
"PRODUCT_PLANNING_DETAIL_PLUS_BASE_URL",
|
|
"PRODUCT_PLANNING_DETAIL_PLUS_INTERNAL_BASE_URL",
|
|
"PRODUCT_PLANNING_DETAIL_PLUS_API_KEY",
|
|
"PRODUCT_PLANNING_QA_TOKEN",
|
|
"TAOBAO_API_KEY",
|
|
"TAOBAO_API_SECRET",
|
|
"TAOBAO_SALE_API_TOKEN",
|
|
"PRODUCT_PLANNING_DB_HOST",
|
|
"PRODUCT_PLANNING_DB_PORT",
|
|
"PRODUCT_PLANNING_DB_DATABASE",
|
|
"PRODUCT_PLANNING_DB_NAME",
|
|
"PRODUCT_PLANNING_DB_USER",
|
|
"PRODUCT_PLANNING_DB_PASSWORD",
|
|
"PRODUCT_PLANNING_DB_CHARSET",
|
|
"PRODUCT_PLANNING_DB_SSL",
|
|
"CANVAS_IMAGE_TASK_CONCURRENCY",
|
|
"CANVAS_IMAGE_TASK_BATCH_DISPATCH_INTERVAL_MS",
|
|
"EXTERNAL_IMAGE_API_MAX_CONCURRENT",
|
|
"EXTERNAL_IMAGE_API_MAX_QUEUE",
|
|
"EXTERNAL_IMAGE_API_QUEUE_TIMEOUT_MS",
|
|
"AGENT_FORCE_LLM_PLANNER",
|
|
"AGENT_FORCE_QWEN3VL_FALLBACK",
|
|
"AGENT_IMAGE_GEN_LLM_OPTIMIZER_ENABLED",
|
|
"AGENT_LLM_PROMPT_OPTIMIZER_ENABLED",
|
|
"AGENT_REPLACEMENT_PARALLEL_WORKER_COUNT",
|
|
"AGENT_PSD_LOCAL_CACHE_DIR",
|
|
"AGENT_PSD_REMOTE_ASSET_CACHE_DISABLED",
|
|
"AGENT_PSD_REMOTE_ASSET_CACHE_MAX_BYTES",
|
|
"AGENT_PSD_REMOTE_ASSET_CACHE_MAX_ENTRY_BYTES",
|
|
"AGENT_PSD_REMOTE_ASSET_CACHE_TTL_MS",
|
|
"AGENT_PSD_RENDER_PNG_COMPRESSION_LEVEL",
|
|
"AGENT_PSD_RENDER_TASK_CONCURRENCY",
|
|
"NEXT_PUBLIC_AGENT_STYLE_PERF_MONITOR",
|
|
"NEXT_PUBLIC_AGENT_STYLE_PERF_SAMPLE_RATE",
|
|
"NEXT_PUBLIC_AGENT_STYLE_OPTIMIZED_RENDER",
|
|
"LOG_LEVEL",
|
|
"LOG_INCLUDE_STACK",
|
|
"NEXT_PUBLIC_ENABLE_CLIENT_LOGS",
|
|
"VIDEO_ARCHIVE_DELAY_HOURS",
|
|
"PORT",
|
|
"PLAYWRIGHT",
|
|
"CI_PLAYWRIGHT",
|
|
"PLAYWRIGHT_TEST_BASE_URL",
|
|
"SMOKE_BASE_URL",
|
|
"SMOKE_TIMEOUT_MS",
|
|
"DISABLE_PERF_GUARD_AUTO_INSTALL",
|
|
];
|
|
|
|
const secretLikeKey = /(SECRET|TOKEN|PASSWORD|API_KEY|ACCESS_KEY|CLIENT_SECRET|FAL_KEY|BFL_KEY|ZOO_API_TOKEN|KITTYCAD_API_TOKEN)$/;
|
|
const suspiciousSecretValue =
|
|
/(sk-[A-Za-z0-9_-]{12,}|gh[pousr]_[A-Za-z0-9_]{20,}|xox[baprs]-[A-Za-z0-9-]{20,}|AKIA[0-9A-Z]{16}|eyJ[A-Za-z0-9_-]{20,})/;
|
|
const allowedSecretPlaceholders = /^(|replace-with-[a-z0-9-]+|change-me[a-z0-9-]*|placeholder|example|your-[a-z0-9-]+)$/i;
|
|
const parsed = new Map();
|
|
const violations = [];
|
|
|
|
lines.forEach((line, index) => {
|
|
if (!line || line.trim().startsWith("#")) {
|
|
return;
|
|
}
|
|
|
|
const match = line.match(/^([A-Z0-9_]+)=(.*)$/);
|
|
if (!match) {
|
|
violations.push(`${envExamplePath}:${index + 1}: invalid env assignment: ${line}`);
|
|
return;
|
|
}
|
|
|
|
const [, key, rawValue] = match;
|
|
const value = rawValue.trim();
|
|
|
|
if (parsed.has(key)) {
|
|
violations.push(`${envExamplePath}:${index + 1}: duplicate key: ${key}`);
|
|
}
|
|
parsed.set(key, { value, line: index + 1 });
|
|
|
|
if (secretLikeKey.test(key) && value && !allowedSecretPlaceholders.test(value)) {
|
|
violations.push(`${envExamplePath}:${index + 1}: secret-like key must use an empty value or placeholder: ${key}`);
|
|
}
|
|
|
|
if (suspiciousSecretValue.test(value)) {
|
|
violations.push(`${envExamplePath}:${index + 1}: suspicious real secret value detected: ${key}`);
|
|
}
|
|
});
|
|
|
|
for (const key of requiredKeys) {
|
|
if (!parsed.has(key)) {
|
|
violations.push(`${envExamplePath}: missing documented env key: ${key}`);
|
|
}
|
|
}
|
|
|
|
if (violations.length > 0) {
|
|
console.error("Environment example hygiene check failed.");
|
|
console.error(violations.join("\n"));
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log(`Environment example hygiene check passed for ${parsed.size} keys.`);
|