Files
maskanx_cm_backend/scripts/restore-secrets-from-company.cjs
T
AFFAANhandClaude Opus 5 19e1e84fb7 Initial commit: MaskanX backend
Independent FastAPI backend for the MaskanX agentic growth platform.

Includes the agent runtime, MCP client integrations (Meta Ads, LinkedIn,
HubSpot, Tavily, Exa, xAI, Citedy, image generation), PostgreSQL storage
for chats and cron jobs, provider and secret management, and the CLI.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 10:28:22 +05:30

162 lines
5.6 KiB
JavaScript

#!/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 <companyId>. Example: --from default");
process.exit(2);
}
if (!to) {
console.error("Could not determine the target company; pass --to <companyId>.");
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.");