const fs = require('fs'); const path = require('path'); const os = require('os'); const { PACKAGE_ROOT, outputsRoot } = require('./output-paths'); const RELAY_CONFIG_FILE = path.join(outputsRoot(), 'webhook', 'relay-config.json'); const CROSS_PROJECT_CREDENTIALS_FILE = path.join(os.homedir(), '.qiwe-skill', 'relay-credentials.json'); function ensureDir(filePath) { const dir = path.dirname(filePath); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } } function readJsonMaybe(filePath) { try { if (!filePath || !fs.existsSync(filePath)) return {}; return JSON.parse(fs.readFileSync(filePath, 'utf8').replace(/^\uFEFF/, '')); } catch { return {}; } } function readEnvLocal() { const candidates = [ path.resolve(process.cwd(), '.env.local'), path.resolve(process.cwd(), '.env'), path.join(PACKAGE_ROOT, '.env.local'), path.join(PACKAGE_ROOT, '.env') ]; const env = {}; for (const envPath of [...new Set(candidates)]) { if (!fs.existsSync(envPath)) continue; const content = fs.readFileSync(envPath, 'utf8').replace(/^\uFEFF/, ''); for (const rawLine of content.split(/\r?\n/)) { const line = rawLine.trim(); if (!line || line.startsWith('#')) continue; const match = line.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/); if (!match || env[match[1]]) continue; let value = match[2].trim(); if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) { value = value.slice(1, -1); } env[match[1]] = value; } } return env; } function readRelayConfigFile() { return readJsonMaybe(RELAY_CONFIG_FILE); } function writeRelayConfigFile(config) { ensureDir(RELAY_CONFIG_FILE); const current = readRelayConfigFile(); fs.writeFileSync( RELAY_CONFIG_FILE, JSON.stringify({ ...current, ...config, updatedAt: new Date().toISOString() }, null, 2), 'utf8' ); } function isRelayEnabled() { const env = readEnvLocal(); const baseUrl = process.env.RELAY_BASE_URL || env.RELAY_BASE_URL || readRelayConfigFile().relayBaseUrl || ''; const apiKey = process.env.TENANT_API_KEY || env.TENANT_API_KEY || ''; const apiSecret = process.env.TENANT_API_SECRET || env.TENANT_API_SECRET || ''; return !!(baseUrl && apiKey && apiSecret); } function getRelayBaseUrl() { const env = readEnvLocal(); return (process.env.RELAY_BASE_URL || env.RELAY_BASE_URL || readRelayConfigFile().relayBaseUrl || 'http://8.138.37.248:4000').replace(/\/$/, ''); } function getTenantApiKey() { const env = readEnvLocal(); return process.env.TENANT_API_KEY || env.TENANT_API_KEY || ''; } function getTenantApiSecret() { const env = readEnvLocal(); return process.env.TENANT_API_SECRET || env.TENANT_API_SECRET || ''; } function getTenantId() { const env = readEnvLocal(); return process.env.TENANT_ID || env.TENANT_ID || readRelayConfigFile().tenantId || ''; } function getRelayPrivateKey() { const env = readEnvLocal(); return (process.env.RELAY_PRIVATE_KEY || env.RELAY_PRIVATE_KEY || '').replace(/\\n/g, '\n'); } function getRelayPublicKey() { return readRelayConfigFile().publicKey || ''; } function getRelayDeviceGuid() { return readRelayConfigFile().deviceGuid || ''; } function getRelayCredentials() { return { relayBaseUrl: getRelayBaseUrl(), tenantId: getTenantId(), tenantApiKey: getTenantApiKey(), tenantApiSecret: getTenantApiSecret(), privateKey: getRelayPrivateKey(), publicKey: getRelayPublicKey(), deviceGuid: getRelayDeviceGuid() }; } function normalizePrivateKey(privateKey) { return privateKey .replace(/\r\n/g, '\n') .replace(/\n/g, '\\n'); } function saveRelayCredentialsToEnv(creds, envPath) { const targetPath = envPath || path.join(PACKAGE_ROOT, '.env.local'); ensureDir(targetPath); let content = ''; if (fs.existsSync(targetPath)) { content = fs.readFileSync(targetPath, 'utf8'); } const normalizedPrivateKey = creds.privateKey ? normalizePrivateKey(creds.privateKey) : ''; const entries = { RELAY_BASE_URL: creds.relayBaseUrl || getRelayBaseUrl(), TENANT_ID: creds.tenantId || '', TENANT_API_KEY: creds.tenantApiKey || '', TENANT_API_SECRET: creds.tenantApiSecret || '', RELAY_PRIVATE_KEY: normalizedPrivateKey }; for (const [key, value] of Object.entries(entries)) { if (!value) continue; const regex = new RegExp(`^${key}\\s*=.*$`, 'm'); const line = `${key}=${value}`; if (regex.test(content)) { content = content.replace(regex, line); } else { content = `${content.replace(/\n*$/, '')}${content ? '\n' : ''}${line}\n`; } } fs.writeFileSync(targetPath, content.trim() + '\n', 'utf8'); for (const [key, value] of Object.entries(entries)) { if (value) process.env[key] = value; } return targetPath; } function saveRelayCredentials(creds) { const saved = []; try { saved.push(saveRelayCredentialsToEnv(creds)); } catch (err) { console.warn('[RelayConfig] Failed to save .env.local:', err.message); } try { writeRelayConfigFile({ relayBaseUrl: creds.relayBaseUrl || getRelayBaseUrl(), tenantId: creds.tenantId || '', publicKey: creds.publicKey || '', deviceGuid: creds.deviceGuid || '' }); saved.push(RELAY_CONFIG_FILE); } catch (err) { console.warn('[RelayConfig] Failed to save relay-config.json:', err.message); } return saved; } function saveRelayCredentialsToFile(creds) { try { ensureDir(CROSS_PROJECT_CREDENTIALS_FILE); fs.writeFileSync( CROSS_PROJECT_CREDENTIALS_FILE, JSON.stringify({ ...creds, updatedAt: new Date().toISOString() }, null, 2), 'utf8' ); return CROSS_PROJECT_CREDENTIALS_FILE; } catch (err) { console.warn('[RelayConfig] Failed to save cross-project credentials:', err.message); return null; } } function loadRelayCredentialsFromFile() { return readJsonMaybe(CROSS_PROJECT_CREDENTIALS_FILE); } function ensureRelayConfigDir() { ensureDir(RELAY_CONFIG_FILE); } module.exports = { RELAY_CONFIG_FILE, isRelayEnabled, getRelayBaseUrl, getTenantApiKey, getTenantApiSecret, getTenantId, getRelayPrivateKey, getRelayPublicKey, getRelayDeviceGuid, getRelayCredentials, saveRelayCredentials, saveRelayCredentialsToEnv, saveRelayCredentialsToFile, loadRelayCredentialsFromFile, writeRelayConfigFile, readRelayConfigFile, ensureRelayConfigDir, normalizePrivateKey };