NODEDC_PLATFORM/infra/scripts/check-local-environment-saf...

148 lines
4.7 KiB
JavaScript

#!/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);