#!/usr/bin/env node import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; const scriptDir = path.dirname(fileURLToPath(import.meta.url)); const platformRoot = path.resolve(scriptDir, "../.."); const monorepoRoot = path.resolve(platformRoot, ".."); const prodHosts = new Set([ "ai-hub.nodedc.ru", "hub.nodedc.ru", "engine.nodedc.ru", "ops.nodedc.ru", "id.nodedc.ru", "ops-agents.nodedc.ru", ]); const relayKeysAllowedInLocal = new Set([ "AI_WORKSPACE_HUB_PUBLIC_URL", "AI_WORKSPACE_HUB_INTERNAL_URL", "NDC_AI_WORKSPACE_HUB_URL", "NDC_AI_WORKSPACE_HUB_HTTP_URL", "AI_WORKSPACE_ASSISTANT_ACTION_GATEWAY_URL", "NDC_AI_WORKSPACE_ASSISTANT_ACTION_GATEWAY_URL", ]); const localOnlyFiles = [ { file: path.join(platformRoot, "infra", ".env.example"), required: true, requiredKeys: ["NODEDC_ENV", "NODEDC_INTERNAL_ACCESS_TOKEN", "AI_WORKSPACE_HUB_PUBLIC_URL", "AI_WORKSPACE_HUB_INTERNAL_URL", "AI_WORKSPACE_ASSISTANT_ACTION_RELAY_ENABLED", "AI_WORKSPACE_ASSISTANT_ACTION_RELAY_ID"], }, { file: path.join(platformRoot, "infra", ".env"), required: false, requiredKeys: ["NODEDC_ENV", "NODEDC_INTERNAL_ACCESS_TOKEN", "AI_WORKSPACE_HUB_PUBLIC_URL", "AI_WORKSPACE_HUB_INTERNAL_URL", "AI_WORKSPACE_ASSISTANT_ACTION_RELAY_ENABLED", "AI_WORKSPACE_ASSISTANT_ACTION_RELAY_ID"], }, { file: path.join(monorepoRoot, "NODEDC_ENGINE_INFRA", "nodedc-source", "server", ".env.local"), required: false, requiredKeys: [ "NODEDC_ENV", "NODEDC_LAUNCHER_INTERNAL_URL", "NODEDC_LAUNCHER_ORIGIN", "AUTHENTIK_PUBLIC_BASE_URL", "NODEDC_AI_WORKSPACE_ASSISTANT_URL", ], }, ]; const failures = []; const warnings = []; const passes = []; function parseEnvFile(file) { const env = new Map(); const raw = fs.readFileSync(file, "utf8"); raw.split(/\r?\n/).forEach((line, index) => { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith("#")) return; const match = /^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/.exec(trimmed); if (!match) return; let value = match[2].trim(); if ( (value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'")) ) { value = value.slice(1, -1); } env.set(match[1], { value, line: index + 1 }); }); return env; } function urlHost(value) { try { return new URL(value).hostname.toLowerCase(); } catch { return ""; } } function isTunnelOrPrivateRelay(value) { const host = urlHost(value); return ( host.endsWith(".ts.net") || host.startsWith("100.") || host.startsWith("172.22.") || host.startsWith("192.168.") || host.startsWith("10.") ); } function checkLocalEnvFile({ file, required, requiredKeys }) { if (!fs.existsSync(file)) { if (required) failures.push(`${file}: missing required local env file`); else warnings.push(`${file}: not present, skipped`); return; } const env = parseEnvFile(file); const profile = env.get("NODEDC_ENV")?.value || "local"; if (profile !== "local" && profile !== "tunnel-local-e2e") { failures.push(`${file}: expected NODEDC_ENV=local or tunnel-local-e2e, got ${profile}`); } requiredKeys.forEach((key) => { if (!env.get(key)?.value) failures.push(`${file}: missing required ${key}`); }); for (const [key, entry] of env.entries()) { const value = entry.value; if (!/^https?:\/\//i.test(value) && !/^wss?:\/\//i.test(value)) continue; const host = urlHost(value); if (!host) continue; if (prodHosts.has(host) && !relayKeysAllowedInLocal.has(key)) { failures.push(`${file}:${entry.line}: ${key} points local profile at prod host ${host}`); } if (isTunnelOrPrivateRelay(value) && profile !== "tunnel-local-e2e") { failures.push(`${file}:${entry.line}: ${key} uses tunnel/private host ${host}; set NODEDC_ENV=tunnel-local-e2e explicitly or use a normal local/public-relay URL`); } } const hubInternal = env.get("AI_WORKSPACE_HUB_INTERNAL_URL")?.value || ""; if (hubInternal && profile === "local") { const host = urlHost(hubInternal); const allowed = host === "ai-hub.nodedc.ru" || host === "127.0.0.1" || host === "localhost"; if (!allowed) { failures.push(`${file}: AI_WORKSPACE_HUB_INTERNAL_URL must be loopback or deployed relay in local profile, got ${hubInternal}`); } } passes.push(`${file}: checked`); } localOnlyFiles.forEach(checkLocalEnvFile); passes.forEach((item) => console.log(`PASS ${item}`)); warnings.forEach((item) => console.warn(`WARN ${item}`)); failures.forEach((item) => console.error(`FAIL ${item}`)); console.log(`summary: passed=${passes.length} warnings=${warnings.length} failed=${failures.length}`); if (failures.length) process.exit(1);