#!/usr/bin/env node "use strict"; /** * Restore MaskanX credentials from an intact company store. * * `clear-secrets.cjs` wipes the active company's stores and the shared secret * directory, but a per-company copy can survive (for example when only one of * several companies was active at the time). This script copies the credential * files from a healthy company back into the active company and the shared * secret directory. * * Credential files are copied wholesale. config.json is patched surgically - * only MCP env values and enabled flags are taken from the source - so * company-specific settings are preserved. * * Usage: * node scripts/restore-secrets-from-company.cjs --from default [--to maskanx] * node scripts/restore-secrets-from-company.cjs --from default --dry-run */ const fs = require("fs"); const os = require("os"); const path = require("path"); const argv = process.argv.slice(2); const flag = (name, fallback = null) => { const i = argv.indexOf(`--${name}`); return i !== -1 && argv[i + 1] ? argv[i + 1] : fallback; }; const dryRun = argv.includes("--dry-run"); const homeDir = os.homedir(); const workingDir = path.resolve( process.env.ADCLAW_WORKING_DIR || path.join(homeDir, ".adclaw"), ); const secretDir = path.resolve( process.env.ADCLAW_SECRET_DIR || `${workingDir}.secret`, ); const companiesDir = path.join(workingDir, "companies"); function readJson(file) { return JSON.parse(fs.readFileSync(file, "utf8")); } function activeCompanyId() { try { return readJson(path.join(companiesDir, "index.json")).active_company_id; } catch { return null; } } const from = flag("from"); const to = flag("to", activeCompanyId()); if (!from) { console.error("Missing --from . Example: --from default"); process.exit(2); } if (!to) { console.error("Could not determine the target company; pass --to ."); process.exit(2); } const srcDir = path.join(companiesDir, from); const dstDir = path.join(companiesDir, to); if (!fs.existsSync(srcDir)) { console.error(`Source company not found: ${srcDir}`); process.exit(2); } const stamp = new Date().toISOString().replace(/[:.]/g, "").replace("T", "-").slice(0, 15); const backupRoot = path.join(secretDir, "restore-backups", stamp); function backup(file) { if (dryRun || !fs.existsSync(file)) return; const target = path.join(backupRoot, path.resolve(file).replace(/[:\\/]/g, "_")); fs.mkdirSync(path.dirname(target), { recursive: true }); fs.copyFileSync(file, target); } function copyFile(src, dst, label) { if (!fs.existsSync(src)) { console.log(` skip ${label}: source missing`); return; } console.log(` ${dryRun ? "would restore" : "restored"} ${label}`); if (dryRun) return; backup(dst); fs.mkdirSync(path.dirname(dst), { recursive: true }); fs.copyFileSync(src, dst); } console.log(`Restoring credentials from company "${from}" to "${to}".`); if (!dryRun) console.log(`Backups: ${backupRoot}`); console.log(""); // ---- credential stores -------------------------------------------------- for (const name of ["envs.json", "providers.json"]) { copyFile(path.join(srcDir, name), path.join(dstDir, name), `${to}/${name}`); copyFile(path.join(srcDir, name), path.join(secretDir, name), `secret-dir/${name}`); } // ---- LinkedIn OAuth ----------------------------------------------------- // The server reads tokens from ~/.linkedin-mcp (see _linkedin_dir() in // app/routers/mcp.py), so the home-level copy is the one that actually // restores authentication. The per-company copy is kept in sync as well. for (const name of ["tokens_default.json", "users.json"]) { const src = path.join(srcDir, "linkedin-mcp", name); copyFile(src, path.join(dstDir, "linkedin-mcp", name), `${to}/linkedin-mcp/${name}`); copyFile(src, path.join(homeDir, ".linkedin-mcp", name), `~/.linkedin-mcp/${name}`); } // ---- config.json: patch MCP env values and enabled flags only ----------- function patchConfig(targetFile) { const srcCfg = path.join(srcDir, "config.json"); if (!fs.existsSync(srcCfg) || !fs.existsSync(targetFile)) { console.log(` skip config patch: ${targetFile} or source missing`); return; } const src = readJson(srcCfg); const dst = readJson(targetFile); const srcClients = (src.mcp && src.mcp.clients) || {}; const dstClients = (dst.mcp && dst.mcp.clients) || {}; const restoredEnv = []; const reEnabled = []; for (const [name, srcClient] of Object.entries(srcClients)) { const dstClient = dstClients[name]; if (!dstClient || !srcClient) continue; for (const [k, v] of Object.entries(srcClient.env || {})) { const current = (dstClient.env || {})[k]; if (typeof v === "string" && v.trim() && (!current || !String(current).trim())) { dstClient.env = dstClient.env || {}; dstClient.env[k] = v; restoredEnv.push(`${name}.${k}`); } } if (srcClient.enabled && !dstClient.enabled) { dstClient.enabled = true; reEnabled.push(name); } } console.log(` config ${path.basename(path.dirname(targetFile))}: ` + `${restoredEnv.length} env value(s), ${reEnabled.length} client(s) re-enabled`); for (const e of restoredEnv) console.log(` + ${e}`); for (const e of reEnabled) console.log(` * enabled ${e}`); if (dryRun) return; backup(targetFile); fs.writeFileSync(targetFile, `${JSON.stringify(dst, null, 2)}\n`, "utf8"); } patchConfig(path.join(dstDir, "config.json")); patchConfig(path.join(workingDir, "config.json")); console.log(""); console.log(dryRun ? "Dry run complete; nothing was written." : "Restore complete."); console.log("Stop MaskanX before running this, then start it again afterwards.");