feat(security): add audit --fix
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
## 2026.1.14 (unreleased)
|
||||
|
||||
### Changes
|
||||
- Security: add `clawdbot security audit` (`--deep`) and surface it in `status --all` and `doctor`.
|
||||
- Security: add `clawdbot security audit` (`--deep`, `--fix`) and surface it in `status --all` and `doctor`.
|
||||
- Onboarding: add a security checkpoint prompt (docs link + sandboxing hint); require `--accept-risk` for `--non-interactive`.
|
||||
- Docs: expand gateway security hardening guidance and incident response checklist.
|
||||
- Docs: document DM history limits for channel DMs. (#883) — thanks @pkrmf.
|
||||
|
||||
@@ -48,6 +48,8 @@ clawdbot [--dev] [--profile <name>] <command>
|
||||
onboard
|
||||
configure (alias: config)
|
||||
doctor
|
||||
security
|
||||
audit
|
||||
reset
|
||||
uninstall
|
||||
update
|
||||
@@ -180,6 +182,12 @@ clawdbot [--dev] [--profile <name>] <command>
|
||||
|
||||
Note: plugins can add additional top-level commands (for example `clawdbot voicecall`).
|
||||
|
||||
## Security
|
||||
|
||||
- `clawdbot security audit` — audit config + local state for common security foot-guns.
|
||||
- `clawdbot security audit --deep` — best-effort live Gateway probe.
|
||||
- `clawdbot security audit --fix` — tighten safe defaults and chmod state/config.
|
||||
|
||||
## Plugins
|
||||
|
||||
Manage extensions and their config:
|
||||
|
||||
@@ -4,11 +4,13 @@ import type { Command } from "commander";
|
||||
import { loadConfig } from "../config/config.js";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
import { runSecurityAudit } from "../security/audit.js";
|
||||
import { fixSecurityFootguns } from "../security/fix.js";
|
||||
import { isRich, theme } from "../terminal/theme.js";
|
||||
|
||||
type SecurityAuditOptions = {
|
||||
json?: boolean;
|
||||
deep?: boolean;
|
||||
fix?: boolean;
|
||||
};
|
||||
|
||||
function formatSummary(summary: { critical: number; warn: number; info: number }): string {
|
||||
@@ -30,8 +32,11 @@ export function registerSecurityCli(program: Command) {
|
||||
.command("audit")
|
||||
.description("Audit config + local state for common security foot-guns")
|
||||
.option("--deep", "Attempt live Gateway probe (best-effort)", false)
|
||||
.option("--fix", "Apply safe fixes (tighten defaults + chmod state/config)", false)
|
||||
.option("--json", "Print JSON", false)
|
||||
.action(async (opts: SecurityAuditOptions) => {
|
||||
const fixResult = opts.fix ? await fixSecurityFootguns().catch((_err) => null) : null;
|
||||
|
||||
const cfg = loadConfig();
|
||||
const report = await runSecurityAudit({
|
||||
config: cfg,
|
||||
@@ -41,7 +46,9 @@ export function registerSecurityCli(program: Command) {
|
||||
});
|
||||
|
||||
if (opts.json) {
|
||||
defaultRuntime.log(JSON.stringify(report, null, 2));
|
||||
defaultRuntime.log(
|
||||
JSON.stringify(fixResult ? { fix: fixResult, report } : report, null, 2),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -54,6 +61,34 @@ export function registerSecurityCli(program: Command) {
|
||||
lines.push(muted(`Summary: ${formatSummary(report.summary)}`));
|
||||
lines.push(muted(`Run deeper: clawdbot security audit --deep`));
|
||||
|
||||
if (opts.fix) {
|
||||
lines.push(muted(`Fix: clawdbot security audit --fix`));
|
||||
if (!fixResult) {
|
||||
lines.push(muted("Fixes: failed to apply (unexpected error)"));
|
||||
} else if (
|
||||
fixResult.errors.length === 0 &&
|
||||
fixResult.changes.length === 0 &&
|
||||
fixResult.actions.every((a) => a.ok === false)
|
||||
) {
|
||||
lines.push(muted("Fixes: no changes applied"));
|
||||
} else {
|
||||
lines.push("");
|
||||
lines.push(heading("FIX"));
|
||||
for (const change of fixResult.changes) {
|
||||
lines.push(muted(` ${change}`));
|
||||
}
|
||||
for (const action of fixResult.actions) {
|
||||
const mode = action.mode.toString(8).padStart(3, "0");
|
||||
if (action.ok) lines.push(muted(` chmod ${mode} ${action.path}`));
|
||||
else if (action.skipped) lines.push(muted(` skip chmod ${mode} ${action.path} (${action.skipped})`));
|
||||
else if (action.error) lines.push(muted(` chmod ${mode} ${action.path} failed: ${action.error}`));
|
||||
}
|
||||
if (fixResult.errors.length > 0) {
|
||||
for (const err of fixResult.errors) lines.push(muted(` error: ${err}`));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const bySeverity = (sev: "critical" | "warn" | "info") =>
|
||||
report.findings.filter((f) => f.severity === sev);
|
||||
|
||||
|
||||
@@ -4,11 +4,10 @@ import { listChannelPlugins } from "../channels/plugins/index.js";
|
||||
import { resolveChannelDefaultAccountId } from "../channels/plugins/helpers.js";
|
||||
import type { ChannelId } from "../channels/plugins/types.js";
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
import { CONFIG_PATH_CLAWDBOT } from "../config/config.js";
|
||||
import { resolveConfigPath, resolveStateDir } from "../config/paths.js";
|
||||
import { resolveGatewayAuth } from "../gateway/auth.js";
|
||||
import { buildGatewayConnectionDetails } from "../gateway/call.js";
|
||||
import { probeGateway } from "../gateway/probe.js";
|
||||
import { CONFIG_DIR } from "../utils.js";
|
||||
|
||||
export type SecurityAuditSeverity = "info" | "warn" | "critical";
|
||||
|
||||
@@ -497,8 +496,8 @@ async function maybeProbeGateway(params: {
|
||||
export async function runSecurityAudit(opts: SecurityAuditOptions): Promise<SecurityAuditReport> {
|
||||
const findings: SecurityAuditFinding[] = [];
|
||||
const cfg = opts.config;
|
||||
const stateDir = opts.stateDir ?? CONFIG_DIR;
|
||||
const configPath = opts.configPath ?? CONFIG_PATH_CLAWDBOT;
|
||||
const stateDir = opts.stateDir ?? resolveStateDir();
|
||||
const configPath = opts.configPath ?? resolveConfigPath();
|
||||
|
||||
findings.push(...collectGatewayConfigFindings(cfg));
|
||||
findings.push(...collectLoggingFindings(cfg));
|
||||
|
||||
200
src/security/fix.test.ts
Normal file
200
src/security/fix.test.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { fixSecurityFootguns } from "./fix.js";
|
||||
|
||||
describe("security fix", () => {
|
||||
it("tightens groupPolicy + filesystem perms", async () => {
|
||||
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-security-fix-"));
|
||||
const stateDir = path.join(tmp, "state");
|
||||
await fs.mkdir(stateDir, { recursive: true });
|
||||
await fs.chmod(stateDir, 0o755);
|
||||
|
||||
const configPath = path.join(stateDir, "clawdbot.json");
|
||||
await fs.writeFile(
|
||||
configPath,
|
||||
`${JSON.stringify(
|
||||
{
|
||||
channels: {
|
||||
telegram: { groupPolicy: "open" },
|
||||
whatsapp: { groupPolicy: "open" },
|
||||
discord: { groupPolicy: "open" },
|
||||
signal: { groupPolicy: "open" },
|
||||
imessage: { groupPolicy: "open" },
|
||||
},
|
||||
logging: { redactSensitive: "off" },
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
"utf-8",
|
||||
);
|
||||
await fs.chmod(configPath, 0o644);
|
||||
|
||||
const credsDir = path.join(stateDir, "credentials");
|
||||
await fs.mkdir(credsDir, { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(credsDir, "whatsapp-allowFrom.json"),
|
||||
`${JSON.stringify({ version: 1, allowFrom: [" +15551234567 "] }, null, 2)}\n`,
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const env = {
|
||||
...process.env,
|
||||
CLAWDBOT_STATE_DIR: stateDir,
|
||||
CLAWDBOT_CONFIG_PATH: "",
|
||||
};
|
||||
|
||||
const res = await fixSecurityFootguns({ env });
|
||||
expect(res.ok).toBe(true);
|
||||
expect(res.configWritten).toBe(true);
|
||||
expect(res.changes).toEqual(
|
||||
expect.arrayContaining([
|
||||
"channels.telegram.groupPolicy=open -> allowlist",
|
||||
"channels.whatsapp.groupPolicy=open -> allowlist",
|
||||
"channels.discord.groupPolicy=open -> allowlist",
|
||||
"channels.signal.groupPolicy=open -> allowlist",
|
||||
"channels.imessage.groupPolicy=open -> allowlist",
|
||||
'logging.redactSensitive=off -> "tools"',
|
||||
]),
|
||||
);
|
||||
|
||||
const stateMode = (await fs.stat(stateDir)).mode & 0o777;
|
||||
expect(stateMode).toBe(0o700);
|
||||
|
||||
const configMode = (await fs.stat(configPath)).mode & 0o777;
|
||||
expect(configMode).toBe(0o600);
|
||||
|
||||
const parsed = JSON.parse(await fs.readFile(configPath, "utf-8")) as Record<string, unknown>;
|
||||
const channels = parsed.channels as Record<string, Record<string, unknown>>;
|
||||
expect(channels.telegram.groupPolicy).toBe("allowlist");
|
||||
expect(channels.whatsapp.groupPolicy).toBe("allowlist");
|
||||
expect(channels.discord.groupPolicy).toBe("allowlist");
|
||||
expect(channels.signal.groupPolicy).toBe("allowlist");
|
||||
expect(channels.imessage.groupPolicy).toBe("allowlist");
|
||||
|
||||
expect(channels.whatsapp.groupAllowFrom).toEqual(["+15551234567"]);
|
||||
});
|
||||
|
||||
it("applies allowlist per-account and seeds WhatsApp groupAllowFrom from store", async () => {
|
||||
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-security-fix-"));
|
||||
const stateDir = path.join(tmp, "state");
|
||||
await fs.mkdir(stateDir, { recursive: true });
|
||||
|
||||
const configPath = path.join(stateDir, "clawdbot.json");
|
||||
await fs.writeFile(
|
||||
configPath,
|
||||
`${JSON.stringify(
|
||||
{
|
||||
channels: {
|
||||
whatsapp: {
|
||||
accounts: {
|
||||
a1: { groupPolicy: "open" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const credsDir = path.join(stateDir, "credentials");
|
||||
await fs.mkdir(credsDir, { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(credsDir, "whatsapp-allowFrom.json"),
|
||||
`${JSON.stringify({ version: 1, allowFrom: ["+15550001111"] }, null, 2)}\n`,
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const env = {
|
||||
...process.env,
|
||||
CLAWDBOT_STATE_DIR: stateDir,
|
||||
CLAWDBOT_CONFIG_PATH: "",
|
||||
};
|
||||
|
||||
const res = await fixSecurityFootguns({ env });
|
||||
expect(res.ok).toBe(true);
|
||||
|
||||
const parsed = JSON.parse(await fs.readFile(configPath, "utf-8")) as Record<string, unknown>;
|
||||
const channels = parsed.channels as Record<string, Record<string, unknown>>;
|
||||
const whatsapp = channels.whatsapp as Record<string, unknown>;
|
||||
const accounts = whatsapp.accounts as Record<string, Record<string, unknown>>;
|
||||
|
||||
expect(accounts.a1.groupPolicy).toBe("allowlist");
|
||||
expect(accounts.a1.groupAllowFrom).toEqual(["+15550001111"]);
|
||||
});
|
||||
|
||||
it("does not seed WhatsApp groupAllowFrom if allowFrom is set", async () => {
|
||||
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-security-fix-"));
|
||||
const stateDir = path.join(tmp, "state");
|
||||
await fs.mkdir(stateDir, { recursive: true });
|
||||
|
||||
const configPath = path.join(stateDir, "clawdbot.json");
|
||||
await fs.writeFile(
|
||||
configPath,
|
||||
`${JSON.stringify(
|
||||
{
|
||||
channels: {
|
||||
whatsapp: { groupPolicy: "open", allowFrom: ["+15552223333"] },
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const credsDir = path.join(stateDir, "credentials");
|
||||
await fs.mkdir(credsDir, { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(credsDir, "whatsapp-allowFrom.json"),
|
||||
`${JSON.stringify({ version: 1, allowFrom: ["+15550001111"] }, null, 2)}\n`,
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const env = {
|
||||
...process.env,
|
||||
CLAWDBOT_STATE_DIR: stateDir,
|
||||
CLAWDBOT_CONFIG_PATH: "",
|
||||
};
|
||||
|
||||
const res = await fixSecurityFootguns({ env });
|
||||
expect(res.ok).toBe(true);
|
||||
|
||||
const parsed = JSON.parse(await fs.readFile(configPath, "utf-8")) as Record<string, unknown>;
|
||||
const channels = parsed.channels as Record<string, Record<string, unknown>>;
|
||||
expect(channels.whatsapp.groupPolicy).toBe("allowlist");
|
||||
expect(channels.whatsapp.groupAllowFrom).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns ok=false for invalid config but still tightens perms", async () => {
|
||||
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-security-fix-"));
|
||||
const stateDir = path.join(tmp, "state");
|
||||
await fs.mkdir(stateDir, { recursive: true });
|
||||
await fs.chmod(stateDir, 0o755);
|
||||
|
||||
const configPath = path.join(stateDir, "clawdbot.json");
|
||||
await fs.writeFile(configPath, "{ this is not json }\n", "utf-8");
|
||||
await fs.chmod(configPath, 0o644);
|
||||
|
||||
const env = {
|
||||
...process.env,
|
||||
CLAWDBOT_STATE_DIR: stateDir,
|
||||
CLAWDBOT_CONFIG_PATH: "",
|
||||
};
|
||||
|
||||
const res = await fixSecurityFootguns({ env });
|
||||
expect(res.ok).toBe(false);
|
||||
|
||||
const stateMode = (await fs.stat(stateDir)).mode & 0o777;
|
||||
expect(stateMode).toBe(0o700);
|
||||
|
||||
const configMode = (await fs.stat(configPath)).mode & 0o777;
|
||||
expect(configMode).toBe(0o600);
|
||||
});
|
||||
});
|
||||
235
src/security/fix.ts
Normal file
235
src/security/fix.ts
Normal file
@@ -0,0 +1,235 @@
|
||||
import fs from "node:fs/promises";
|
||||
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
import { createConfigIO } from "../config/config.js";
|
||||
import { resolveConfigPath, resolveStateDir } from "../config/paths.js";
|
||||
import { readChannelAllowFromStore } from "../pairing/pairing-store.js";
|
||||
|
||||
export type SecurityFixChmodAction = {
|
||||
kind: "chmod";
|
||||
path: string;
|
||||
mode: number;
|
||||
ok: boolean;
|
||||
skipped?: string;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
export type SecurityFixResult = {
|
||||
ok: boolean;
|
||||
stateDir: string;
|
||||
configPath: string;
|
||||
configWritten: boolean;
|
||||
changes: string[];
|
||||
actions: SecurityFixChmodAction[];
|
||||
errors: string[];
|
||||
};
|
||||
|
||||
async function safeChmod(params: {
|
||||
path: string;
|
||||
mode: number;
|
||||
require: "dir" | "file";
|
||||
}): Promise<SecurityFixChmodAction> {
|
||||
try {
|
||||
const st = await fs.lstat(params.path);
|
||||
if (st.isSymbolicLink()) {
|
||||
return {
|
||||
kind: "chmod",
|
||||
path: params.path,
|
||||
mode: params.mode,
|
||||
ok: false,
|
||||
skipped: "symlink",
|
||||
};
|
||||
}
|
||||
if (params.require === "dir" && !st.isDirectory()) {
|
||||
return {
|
||||
kind: "chmod",
|
||||
path: params.path,
|
||||
mode: params.mode,
|
||||
ok: false,
|
||||
skipped: "not-a-directory",
|
||||
};
|
||||
}
|
||||
if (params.require === "file" && !st.isFile()) {
|
||||
return {
|
||||
kind: "chmod",
|
||||
path: params.path,
|
||||
mode: params.mode,
|
||||
ok: false,
|
||||
skipped: "not-a-file",
|
||||
};
|
||||
}
|
||||
const current = st.mode & 0o777;
|
||||
if (current === params.mode) {
|
||||
return {
|
||||
kind: "chmod",
|
||||
path: params.path,
|
||||
mode: params.mode,
|
||||
ok: false,
|
||||
skipped: "already",
|
||||
};
|
||||
}
|
||||
await fs.chmod(params.path, params.mode);
|
||||
return { kind: "chmod", path: params.path, mode: params.mode, ok: true };
|
||||
} catch (err) {
|
||||
const code = (err as { code?: string }).code;
|
||||
if (code === "ENOENT") {
|
||||
return {
|
||||
kind: "chmod",
|
||||
path: params.path,
|
||||
mode: params.mode,
|
||||
ok: false,
|
||||
skipped: "missing",
|
||||
};
|
||||
}
|
||||
return {
|
||||
kind: "chmod",
|
||||
path: params.path,
|
||||
mode: params.mode,
|
||||
ok: false,
|
||||
error: String(err),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function setGroupPolicyAllowlist(params: {
|
||||
cfg: ClawdbotConfig;
|
||||
channel: string;
|
||||
changes: string[];
|
||||
policyFlips: Set<string>;
|
||||
}): void {
|
||||
if (!params.cfg.channels) return;
|
||||
const section = params.cfg.channels[params.channel as keyof ClawdbotConfig["channels"]] as
|
||||
| Record<string, unknown>
|
||||
| undefined;
|
||||
if (!section || typeof section !== "object") return;
|
||||
|
||||
const topPolicy = section.groupPolicy;
|
||||
if (topPolicy === "open") {
|
||||
section.groupPolicy = "allowlist";
|
||||
params.changes.push(`channels.${params.channel}.groupPolicy=open -> allowlist`);
|
||||
params.policyFlips.add(`channels.${params.channel}.`);
|
||||
}
|
||||
|
||||
const accounts = section.accounts;
|
||||
if (!accounts || typeof accounts !== "object") return;
|
||||
for (const [accountId, accountValue] of Object.entries(accounts)) {
|
||||
if (!accountId) continue;
|
||||
if (!accountValue || typeof accountValue !== "object") continue;
|
||||
const account = accountValue as Record<string, unknown>;
|
||||
if (account.groupPolicy === "open") {
|
||||
account.groupPolicy = "allowlist";
|
||||
params.changes.push(
|
||||
`channels.${params.channel}.accounts.${accountId}.groupPolicy=open -> allowlist`,
|
||||
);
|
||||
params.policyFlips.add(`channels.${params.channel}.accounts.${accountId}.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setWhatsAppGroupAllowFromFromStore(params: {
|
||||
cfg: ClawdbotConfig;
|
||||
storeAllowFrom: string[];
|
||||
changes: string[];
|
||||
policyFlips: Set<string>;
|
||||
}): void {
|
||||
const section = params.cfg.channels?.whatsapp as Record<string, unknown> | undefined;
|
||||
if (!section || typeof section !== "object") return;
|
||||
if (params.storeAllowFrom.length === 0) return;
|
||||
|
||||
const maybeApply = (prefix: string, obj: Record<string, unknown>) => {
|
||||
if (!params.policyFlips.has(prefix)) return;
|
||||
const allowFrom = Array.isArray(obj.allowFrom) ? obj.allowFrom : [];
|
||||
const groupAllowFrom = Array.isArray(obj.groupAllowFrom) ? obj.groupAllowFrom : [];
|
||||
if (allowFrom.length > 0) return;
|
||||
if (groupAllowFrom.length > 0) return;
|
||||
obj.groupAllowFrom = params.storeAllowFrom;
|
||||
params.changes.push(`${prefix}groupAllowFrom=pairing-store`);
|
||||
};
|
||||
|
||||
maybeApply("channels.whatsapp.", section);
|
||||
|
||||
const accounts = section.accounts;
|
||||
if (!accounts || typeof accounts !== "object") return;
|
||||
for (const [accountId, accountValue] of Object.entries(accounts)) {
|
||||
if (!accountValue || typeof accountValue !== "object") continue;
|
||||
const account = accountValue as Record<string, unknown>;
|
||||
maybeApply(`channels.whatsapp.accounts.${accountId}.`, account);
|
||||
}
|
||||
}
|
||||
|
||||
function applyConfigFixes(params: {
|
||||
cfg: ClawdbotConfig;
|
||||
env: NodeJS.ProcessEnv;
|
||||
}): { cfg: ClawdbotConfig; changes: string[]; policyFlips: Set<string> } {
|
||||
const next = structuredClone(params.cfg ?? {});
|
||||
const changes: string[] = [];
|
||||
const policyFlips = new Set<string>();
|
||||
|
||||
if (next.logging?.redactSensitive === "off") {
|
||||
next.logging = { ...next.logging, redactSensitive: "tools" };
|
||||
changes.push('logging.redactSensitive=off -> "tools"');
|
||||
}
|
||||
|
||||
for (const channel of ["telegram", "whatsapp", "discord", "signal", "imessage", "slack", "msteams"]) {
|
||||
setGroupPolicyAllowlist({ cfg: next, channel, changes, policyFlips });
|
||||
}
|
||||
|
||||
return { cfg: next, changes, policyFlips };
|
||||
}
|
||||
|
||||
export async function fixSecurityFootguns(opts?: {
|
||||
env?: NodeJS.ProcessEnv;
|
||||
stateDir?: string;
|
||||
configPath?: string;
|
||||
}): Promise<SecurityFixResult> {
|
||||
const env = opts?.env ?? process.env;
|
||||
const stateDir = opts?.stateDir ?? resolveStateDir(env);
|
||||
const configPath = opts?.configPath ?? resolveConfigPath(env, stateDir);
|
||||
const actions: SecurityFixChmodAction[] = [];
|
||||
const errors: string[] = [];
|
||||
|
||||
const io = createConfigIO({ env, configPath });
|
||||
const snap = await io.readConfigFileSnapshot();
|
||||
if (!snap.valid) {
|
||||
errors.push(...snap.issues.map((i) => `${i.path}: ${i.message}`));
|
||||
}
|
||||
|
||||
let configWritten = false;
|
||||
let changes: string[] = [];
|
||||
if (snap.valid) {
|
||||
const fixed = applyConfigFixes({ cfg: snap.config, env });
|
||||
changes = fixed.changes;
|
||||
|
||||
const whatsappStoreAllowFrom = await readChannelAllowFromStore("whatsapp", env).catch(() => []);
|
||||
if (whatsappStoreAllowFrom.length > 0) {
|
||||
setWhatsAppGroupAllowFromFromStore({
|
||||
cfg: fixed.cfg,
|
||||
storeAllowFrom: whatsappStoreAllowFrom,
|
||||
changes,
|
||||
policyFlips: fixed.policyFlips,
|
||||
});
|
||||
}
|
||||
|
||||
if (changes.length > 0) {
|
||||
try {
|
||||
await io.writeConfigFile(fixed.cfg);
|
||||
configWritten = true;
|
||||
} catch (err) {
|
||||
errors.push(`writeConfigFile failed: ${String(err)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
actions.push(await safeChmod({ path: stateDir, mode: 0o700, require: "dir" }));
|
||||
actions.push(await safeChmod({ path: configPath, mode: 0o600, require: "file" }));
|
||||
|
||||
return {
|
||||
ok: errors.length === 0,
|
||||
stateDir,
|
||||
configPath,
|
||||
configWritten,
|
||||
changes,
|
||||
actions,
|
||||
errors,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user