feat(ai-workspace): add local relay profiles
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
#!/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);
|
||||
Reference in New Issue
Block a user