Files
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

343 lines
9.6 KiB
JavaScript

#!/usr/bin/env node
"use strict";
const fs = require("fs");
const os = require("os");
const path = require("path");
const args = new Set(process.argv.slice(2));
const dryRun = args.has("--dry-run");
const noBackup = args.has("--no-backup");
const includeEnvFiles = args.has("--include-env-files");
const repoRoot = path.resolve(__dirname, "..");
const homeDir = os.homedir();
function expandHome(value) {
if (!value) return value;
if (value === "~") return homeDir;
if (value.startsWith("~/") || value.startsWith("~\\")) {
return path.join(homeDir, value.slice(2));
}
return value;
}
const workingDir = path.resolve(
expandHome(process.env.ADCLAW_WORKING_DIR || path.join(homeDir, ".adclaw")),
);
const secretDir = path.resolve(
expandHome(process.env.ADCLAW_SECRET_DIR || `${workingDir}.secret`),
);
const stamp = new Date()
.toISOString()
.replace(/[-:]/g, "")
.replace(/\..+$/, "")
.replace("T", "-");
const backupRoot = path.join(secretDir, "backups", stamp);
const secretNamePattern =
/(^|[_\-.])(api[_\-.]?key|token|secret|credential|authorization|bearer)($|[_\-.])/i;
const touched = [];
const skipped = [];
function exists(filePath) {
try {
return fs.statSync(filePath).isFile();
} catch {
return false;
}
}
function backupPathFor(filePath) {
const safeName = path
.resolve(filePath)
.replace(/^[A-Za-z]:/, "")
.replace(/[\\/]+/g, "__")
.replace(/^__/, "");
return path.join(backupRoot, safeName);
}
function backupFile(filePath) {
if (noBackup || dryRun || !exists(filePath)) return;
const target = backupPathFor(filePath);
fs.mkdirSync(path.dirname(target), { recursive: true });
fs.copyFileSync(filePath, target);
}
function removeFile(filePath, reason) {
if (!exists(filePath)) {
skipped.push({ filePath, reason: "not found" });
return;
}
touched.push({ action: noBackup ? "deleted" : "backed up and removed", filePath, reason });
if (dryRun) return;
backupFile(filePath);
fs.unlinkSync(filePath);
}
function sanitizeValue(value, parentKey = "") {
if (Array.isArray(value)) {
let changed = false;
const next = value.map((item) => {
const result = sanitizeValue(item, parentKey);
changed = changed || result.changed;
return result.value;
});
return { value: next, changed };
}
if (value && typeof value === "object") {
let changed = false;
const next = {};
for (const [key, child] of Object.entries(value)) {
const keyLooksSecret = secretNamePattern.test(key);
if (keyLooksSecret) {
changed = true;
if (typeof child === "string") {
next[key] = "";
} else if (Array.isArray(child)) {
next[key] = [];
} else if (child && typeof child === "object") {
next[key] = {};
} else {
next[key] = null;
}
continue;
}
const result = sanitizeValue(child, key);
changed = changed || result.changed;
next[key] = result.value;
}
return { value: next, changed };
}
if (typeof value === "string" && secretNamePattern.test(parentKey) && value) {
return { value: "", changed: true };
}
return { value, changed: false };
}
function sanitizeJsonFile(filePath, reason) {
if (!exists(filePath)) {
skipped.push({ filePath, reason: "not found" });
return;
}
let parsed;
try {
parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
} catch (error) {
skipped.push({ filePath, reason: `invalid JSON: ${error.message}` });
return;
}
const result = sanitizeValue(parsed);
if (!result.changed) {
skipped.push({ filePath, reason: "no secret-like fields found" });
return;
}
touched.push({ action: "sanitized", filePath, reason });
if (dryRun) return;
backupFile(filePath);
fs.writeFileSync(filePath, `${JSON.stringify(result.value, null, 2)}\n`, "utf8");
}
function sanitizeEnvFile(filePath) {
if (!exists(filePath)) {
skipped.push({ filePath, reason: "not found" });
return;
}
const original = fs.readFileSync(filePath, "utf8");
const lines = original.split(/\r?\n/);
let changed = false;
const next = lines.map((line) => {
const match = line.match(/^(\s*export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/);
if (!match) return line;
const key = match[2];
if (!secretNamePattern.test(key)) return line;
changed = true;
return `${match[1] || ""}${key}=`;
});
if (!changed) {
skipped.push({ filePath, reason: "no API key/token/secret env vars found" });
return;
}
touched.push({ action: "sanitized", filePath, reason: "env file API key/token/secret lines" });
if (dryRun) return;
backupFile(filePath);
fs.writeFileSync(filePath, next.join(os.EOL), "utf8");
}
function listCompanyDirs() {
const companiesDir = path.join(workingDir, "companies");
try {
return fs
.readdirSync(companiesDir, { withFileTypes: true })
.filter((entry) => entry.isDirectory())
.map((entry) => path.join(companiesDir, entry.name));
} catch {
return [];
}
}
function clearRuntimeSecrets() {
const filesToRemove = [
path.join(secretDir, "providers.json"),
path.join(secretDir, "envs.json"),
path.join(workingDir, "providers.json"),
path.join(workingDir, "envs.json"),
path.join(workingDir, ".secret", "providers.json"),
path.join(workingDir, ".secret", "envs.json"),
path.join(homeDir, ".linkedin-mcp", "tokens_default.json"),
path.join(homeDir, ".linkedin-mcp", "users.json"),
];
for (const companyDir of listCompanyDirs()) {
filesToRemove.push(
path.join(companyDir, "providers.json"),
path.join(companyDir, "envs.json"),
path.join(companyDir, "linkedin-mcp", "tokens_default.json"),
path.join(companyDir, "linkedin-mcp", "users.json"),
);
sanitizeJsonFile(
path.join(companyDir, "config.json"),
"company MCP/API config secret-like values",
);
}
sanitizeJsonFile(path.join(workingDir, "config.json"), "MCP/API config secret-like values");
for (const filePath of filesToRemove) {
removeFile(filePath, "runtime/provider/env/OAuth secret store");
}
}
function clearEnvFiles() {
for (const name of [
".env",
".env.local",
".env.development",
".env.testing",
".env.production",
]) {
sanitizeEnvFile(path.join(repoRoot, name));
}
}
if (noBackup && !dryRun) {
console.warn(
"WARNING: --no-backup is set. Cleared credentials cannot be recovered.",
);
}
if (!dryRun) {
console.warn(
"Stop MaskanX before clearing: a running app can rewrite its config from " +
"memory and restore the secrets this script removes.",
);
}
clearRuntimeSecrets();
if (includeEnvFiles) {
clearEnvFiles();
}
console.log("");
console.log(`MaskanX secret cleanup ${dryRun ? "dry run" : "complete"}.`);
console.log(`Working dir: ${workingDir}`);
console.log(`Secret dir: ${secretDir}`);
if (!noBackup) {
console.log(`Backups: ${dryRun ? "(dry run only)" : backupRoot}`);
}
console.log("");
if (touched.length) {
console.log("Changed:");
for (const item of touched) {
console.log(`- ${item.action}: ${item.filePath} (${item.reason})`);
}
} else {
console.log("Changed: none");
}
console.log("");
console.log("Skipped:");
for (const item of skipped) {
console.log(`- ${item.filePath} (${item.reason})`);
}
if (!includeEnvFiles) {
console.log("");
console.log("Tip: run `npm run secrets:clear:all` to also blank API key/token/secret lines in .env files.");
}
// ---------------------------------------------------------------------------
// Verification pass.
//
// Clearing is not trustworthy on its own: if the MaskanX app is running while
// this script executes, it can rewrite config.json / providers.json from its
// in-memory state and silently restore the very values we just removed. Re-read
// the files afterwards and fail loudly when anything secret-looking survives,
// so "secrets cleared" can never be reported when it is not true.
// ---------------------------------------------------------------------------
function collectRemainingSecrets(value, keyPath, found) {
if (Array.isArray(value)) {
value.forEach((item, i) => collectRemainingSecrets(item, `${keyPath}[${i}]`, found));
return found;
}
if (value && typeof value === "object") {
for (const [key, child] of Object.entries(value)) {
const next = keyPath ? `${keyPath}.${key}` : key;
if (secretNamePattern.test(key) && typeof child === "string" && child.trim()) {
found.push(next);
continue;
}
collectRemainingSecrets(child, next, found);
}
}
return found;
}
function verifyJsonFile(filePath) {
if (!exists(filePath)) return [];
try {
const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
return collectRemainingSecrets(parsed, "", []).map((k) => `${filePath} -> ${k}`);
} catch {
return [];
}
}
if (!dryRun) {
const leftovers = [
...verifyJsonFile(path.join(workingDir, "config.json")),
...verifyJsonFile(path.join(secretDir, "providers.json")),
...verifyJsonFile(path.join(secretDir, "envs.json")),
];
for (const companyDir of listCompanyDirs()) {
leftovers.push(...verifyJsonFile(path.join(companyDir, "config.json")));
}
console.log("");
if (leftovers.length) {
console.error("VERIFICATION FAILED - secrets are still present:");
for (const item of leftovers) console.error(`- ${item}`);
console.error("");
console.error(
"This usually means MaskanX was running and rewrote its config from memory.",
);
console.error("Stop the app (and any `npm run local`/`npm start`), then re-run.");
process.exitCode = 1;
} else {
console.log("Verification passed: no secret-like values remain.");
}
}