@theaux/clawdbot 2026.1.14 → 2026.1.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,317 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import JSON5 from "json5";
4
+ import { createConfigIO } from "../config/config.js";
5
+ import { resolveConfigPath, resolveOAuthDir, resolveStateDir } from "../config/paths.js";
6
+ import { resolveDefaultAgentId } from "../agents/agent-scope.js";
7
+ import { INCLUDE_KEY, MAX_INCLUDE_DEPTH } from "../config/includes.js";
8
+ import { normalizeAgentId } from "../routing/session-key.js";
9
+ import { readChannelAllowFromStore } from "../pairing/pairing-store.js";
10
+ async function safeChmod(params) {
11
+ try {
12
+ const st = await fs.lstat(params.path);
13
+ if (st.isSymbolicLink()) {
14
+ return {
15
+ kind: "chmod",
16
+ path: params.path,
17
+ mode: params.mode,
18
+ ok: false,
19
+ skipped: "symlink",
20
+ };
21
+ }
22
+ if (params.require === "dir" && !st.isDirectory()) {
23
+ return {
24
+ kind: "chmod",
25
+ path: params.path,
26
+ mode: params.mode,
27
+ ok: false,
28
+ skipped: "not-a-directory",
29
+ };
30
+ }
31
+ if (params.require === "file" && !st.isFile()) {
32
+ return {
33
+ kind: "chmod",
34
+ path: params.path,
35
+ mode: params.mode,
36
+ ok: false,
37
+ skipped: "not-a-file",
38
+ };
39
+ }
40
+ const current = st.mode & 0o777;
41
+ if (current === params.mode) {
42
+ return {
43
+ kind: "chmod",
44
+ path: params.path,
45
+ mode: params.mode,
46
+ ok: false,
47
+ skipped: "already",
48
+ };
49
+ }
50
+ await fs.chmod(params.path, params.mode);
51
+ return { kind: "chmod", path: params.path, mode: params.mode, ok: true };
52
+ }
53
+ catch (err) {
54
+ const code = err.code;
55
+ if (code === "ENOENT") {
56
+ return {
57
+ kind: "chmod",
58
+ path: params.path,
59
+ mode: params.mode,
60
+ ok: false,
61
+ skipped: "missing",
62
+ };
63
+ }
64
+ return {
65
+ kind: "chmod",
66
+ path: params.path,
67
+ mode: params.mode,
68
+ ok: false,
69
+ error: String(err),
70
+ };
71
+ }
72
+ }
73
+ function setGroupPolicyAllowlist(params) {
74
+ if (!params.cfg.channels)
75
+ return;
76
+ const section = params.cfg.channels[params.channel];
77
+ if (!section || typeof section !== "object")
78
+ return;
79
+ const topPolicy = section.groupPolicy;
80
+ if (topPolicy === "open") {
81
+ section.groupPolicy = "allowlist";
82
+ params.changes.push(`channels.${params.channel}.groupPolicy=open -> allowlist`);
83
+ params.policyFlips.add(`channels.${params.channel}.`);
84
+ }
85
+ const accounts = section.accounts;
86
+ if (!accounts || typeof accounts !== "object")
87
+ return;
88
+ for (const [accountId, accountValue] of Object.entries(accounts)) {
89
+ if (!accountId)
90
+ continue;
91
+ if (!accountValue || typeof accountValue !== "object")
92
+ continue;
93
+ const account = accountValue;
94
+ if (account.groupPolicy === "open") {
95
+ account.groupPolicy = "allowlist";
96
+ params.changes.push(`channels.${params.channel}.accounts.${accountId}.groupPolicy=open -> allowlist`);
97
+ params.policyFlips.add(`channels.${params.channel}.accounts.${accountId}.`);
98
+ }
99
+ }
100
+ }
101
+ function setWhatsAppGroupAllowFromFromStore(params) {
102
+ const section = params.cfg.channels?.whatsapp;
103
+ if (!section || typeof section !== "object")
104
+ return;
105
+ if (params.storeAllowFrom.length === 0)
106
+ return;
107
+ const maybeApply = (prefix, obj) => {
108
+ if (!params.policyFlips.has(prefix))
109
+ return;
110
+ const allowFrom = Array.isArray(obj.allowFrom) ? obj.allowFrom : [];
111
+ const groupAllowFrom = Array.isArray(obj.groupAllowFrom) ? obj.groupAllowFrom : [];
112
+ if (allowFrom.length > 0)
113
+ return;
114
+ if (groupAllowFrom.length > 0)
115
+ return;
116
+ obj.groupAllowFrom = params.storeAllowFrom;
117
+ params.changes.push(`${prefix}groupAllowFrom=pairing-store`);
118
+ };
119
+ maybeApply("channels.whatsapp.", section);
120
+ const accounts = section.accounts;
121
+ if (!accounts || typeof accounts !== "object")
122
+ return;
123
+ for (const [accountId, accountValue] of Object.entries(accounts)) {
124
+ if (!accountValue || typeof accountValue !== "object")
125
+ continue;
126
+ const account = accountValue;
127
+ maybeApply(`channels.whatsapp.accounts.${accountId}.`, account);
128
+ }
129
+ }
130
+ function applyConfigFixes(params) {
131
+ const next = structuredClone(params.cfg ?? {});
132
+ const changes = [];
133
+ const policyFlips = new Set();
134
+ if (next.logging?.redactSensitive === "off") {
135
+ next.logging = { ...next.logging, redactSensitive: "tools" };
136
+ changes.push('logging.redactSensitive=off -> "tools"');
137
+ }
138
+ for (const channel of [
139
+ "telegram",
140
+ "whatsapp",
141
+ "discord",
142
+ "signal",
143
+ "imessage",
144
+ "slack",
145
+ "msteams",
146
+ ]) {
147
+ setGroupPolicyAllowlist({ cfg: next, channel, changes, policyFlips });
148
+ }
149
+ return { cfg: next, changes, policyFlips };
150
+ }
151
+ function listDirectIncludes(parsed) {
152
+ const out = [];
153
+ const visit = (value) => {
154
+ if (!value)
155
+ return;
156
+ if (Array.isArray(value)) {
157
+ for (const item of value)
158
+ visit(item);
159
+ return;
160
+ }
161
+ if (typeof value !== "object")
162
+ return;
163
+ const rec = value;
164
+ const includeVal = rec[INCLUDE_KEY];
165
+ if (typeof includeVal === "string")
166
+ out.push(includeVal);
167
+ else if (Array.isArray(includeVal)) {
168
+ for (const item of includeVal) {
169
+ if (typeof item === "string")
170
+ out.push(item);
171
+ }
172
+ }
173
+ for (const v of Object.values(rec))
174
+ visit(v);
175
+ };
176
+ visit(parsed);
177
+ return out;
178
+ }
179
+ function resolveIncludePath(baseConfigPath, includePath) {
180
+ return path.normalize(path.isAbsolute(includePath)
181
+ ? includePath
182
+ : path.resolve(path.dirname(baseConfigPath), includePath));
183
+ }
184
+ async function collectIncludePathsRecursive(params) {
185
+ const visited = new Set();
186
+ const result = [];
187
+ const walk = async (basePath, parsed, depth) => {
188
+ if (depth > MAX_INCLUDE_DEPTH)
189
+ return;
190
+ for (const raw of listDirectIncludes(parsed)) {
191
+ const resolved = resolveIncludePath(basePath, raw);
192
+ if (visited.has(resolved))
193
+ continue;
194
+ visited.add(resolved);
195
+ result.push(resolved);
196
+ const rawText = await fs.readFile(resolved, "utf-8").catch(() => null);
197
+ if (!rawText)
198
+ continue;
199
+ const nestedParsed = (() => {
200
+ try {
201
+ return JSON5.parse(rawText);
202
+ }
203
+ catch {
204
+ return null;
205
+ }
206
+ })();
207
+ if (nestedParsed) {
208
+ // eslint-disable-next-line no-await-in-loop
209
+ await walk(resolved, nestedParsed, depth + 1);
210
+ }
211
+ }
212
+ };
213
+ await walk(params.configPath, params.parsed, 0);
214
+ return result;
215
+ }
216
+ async function chmodCredentialsAndAgentState(params) {
217
+ const credsDir = resolveOAuthDir(params.env, params.stateDir);
218
+ params.actions.push(await safeChmod({ path: credsDir, mode: 0o700, require: "dir" }));
219
+ const credsEntries = await fs.readdir(credsDir, { withFileTypes: true }).catch(() => []);
220
+ for (const entry of credsEntries) {
221
+ if (!entry.isFile())
222
+ continue;
223
+ if (!entry.name.endsWith(".json"))
224
+ continue;
225
+ const p = path.join(credsDir, entry.name);
226
+ // eslint-disable-next-line no-await-in-loop
227
+ params.actions.push(await safeChmod({ path: p, mode: 0o600, require: "file" }));
228
+ }
229
+ const ids = new Set();
230
+ ids.add(resolveDefaultAgentId(params.cfg));
231
+ const list = Array.isArray(params.cfg.agents?.list) ? params.cfg.agents?.list : [];
232
+ for (const agent of list ?? []) {
233
+ if (!agent || typeof agent !== "object")
234
+ continue;
235
+ const id = typeof agent.id === "string" ? agent.id.trim() : "";
236
+ if (id)
237
+ ids.add(id);
238
+ }
239
+ for (const agentId of ids) {
240
+ const normalizedAgentId = normalizeAgentId(agentId);
241
+ const agentRoot = path.join(params.stateDir, "agents", normalizedAgentId);
242
+ const agentDir = path.join(agentRoot, "agent");
243
+ const sessionsDir = path.join(agentRoot, "sessions");
244
+ // eslint-disable-next-line no-await-in-loop
245
+ params.actions.push(await safeChmod({ path: agentRoot, mode: 0o700, require: "dir" }));
246
+ // eslint-disable-next-line no-await-in-loop
247
+ params.actions.push(await safeChmod({ path: agentDir, mode: 0o700, require: "dir" }));
248
+ const authPath = path.join(agentDir, "auth-profiles.json");
249
+ // eslint-disable-next-line no-await-in-loop
250
+ params.actions.push(await safeChmod({ path: authPath, mode: 0o600, require: "file" }));
251
+ // eslint-disable-next-line no-await-in-loop
252
+ params.actions.push(await safeChmod({ path: sessionsDir, mode: 0o700, require: "dir" }));
253
+ const storePath = path.join(sessionsDir, "sessions.json");
254
+ // eslint-disable-next-line no-await-in-loop
255
+ params.actions.push(await safeChmod({ path: storePath, mode: 0o600, require: "file" }));
256
+ }
257
+ }
258
+ export async function fixSecurityFootguns(opts) {
259
+ const env = opts?.env ?? process.env;
260
+ const stateDir = opts?.stateDir ?? resolveStateDir(env);
261
+ const configPath = opts?.configPath ?? resolveConfigPath(env, stateDir);
262
+ const actions = [];
263
+ const errors = [];
264
+ const io = createConfigIO({ env, configPath });
265
+ const snap = await io.readConfigFileSnapshot();
266
+ if (!snap.valid) {
267
+ errors.push(...snap.issues.map((i) => `${i.path}: ${i.message}`));
268
+ }
269
+ let configWritten = false;
270
+ let changes = [];
271
+ if (snap.valid) {
272
+ const fixed = applyConfigFixes({ cfg: snap.config, env });
273
+ changes = fixed.changes;
274
+ const whatsappStoreAllowFrom = await readChannelAllowFromStore("whatsapp", env).catch(() => []);
275
+ if (whatsappStoreAllowFrom.length > 0) {
276
+ setWhatsAppGroupAllowFromFromStore({
277
+ cfg: fixed.cfg,
278
+ storeAllowFrom: whatsappStoreAllowFrom,
279
+ changes,
280
+ policyFlips: fixed.policyFlips,
281
+ });
282
+ }
283
+ if (changes.length > 0) {
284
+ try {
285
+ await io.writeConfigFile(fixed.cfg);
286
+ configWritten = true;
287
+ }
288
+ catch (err) {
289
+ errors.push(`writeConfigFile failed: ${String(err)}`);
290
+ }
291
+ }
292
+ }
293
+ actions.push(await safeChmod({ path: stateDir, mode: 0o700, require: "dir" }));
294
+ actions.push(await safeChmod({ path: configPath, mode: 0o600, require: "file" }));
295
+ if (snap.exists) {
296
+ const includePaths = await collectIncludePathsRecursive({
297
+ configPath: snap.path,
298
+ parsed: snap.parsed,
299
+ }).catch(() => []);
300
+ for (const p of includePaths) {
301
+ // eslint-disable-next-line no-await-in-loop
302
+ actions.push(await safeChmod({ path: p, mode: 0o600, require: "file" }));
303
+ }
304
+ }
305
+ await chmodCredentialsAndAgentState({ env, stateDir, cfg: snap.config ?? {}, actions }).catch((err) => {
306
+ errors.push(`chmodCredentialsAndAgentState failed: ${String(err)}`);
307
+ });
308
+ return {
309
+ ok: errors.length === 0,
310
+ stateDir,
311
+ configPath,
312
+ configWritten,
313
+ changes,
314
+ actions,
315
+ errors,
316
+ };
317
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theaux/clawdbot",
3
- "version": "2026.1.14",
3
+ "version": "2026.1.16",
4
4
  "description": "WhatsApp gateway CLI (Baileys web) with Pi RPC agent",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -30,6 +30,7 @@
30
30
  "dist/process/**",
31
31
  "dist/plugins/**",
32
32
  "dist/sessions/**",
33
+ "dist/security/**",
33
34
  "dist/providers/**",
34
35
  "dist/signal/**",
35
36
  "dist/slack/**",