@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,525 @@
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 { resolveOAuthDir } 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 { formatOctal, isGroupReadable, isGroupWritable, isWorldReadable, isWorldWritable, modeBits, safeStat, } from "./audit-fs.js";
10
+ function expandTilde(p, env) {
11
+ if (!p.startsWith("~"))
12
+ return p;
13
+ const home = typeof env.HOME === "string" && env.HOME.trim() ? env.HOME.trim() : null;
14
+ if (!home)
15
+ return null;
16
+ if (p === "~")
17
+ return home;
18
+ if (p.startsWith("~/") || p.startsWith("~\\"))
19
+ return path.join(home, p.slice(2));
20
+ return null;
21
+ }
22
+ function summarizeGroupPolicy(cfg) {
23
+ const channels = cfg.channels;
24
+ if (!channels || typeof channels !== "object")
25
+ return { open: 0, allowlist: 0, other: 0 };
26
+ let open = 0;
27
+ let allowlist = 0;
28
+ let other = 0;
29
+ for (const value of Object.values(channels)) {
30
+ if (!value || typeof value !== "object")
31
+ continue;
32
+ const section = value;
33
+ const policy = section.groupPolicy;
34
+ if (policy === "open")
35
+ open += 1;
36
+ else if (policy === "allowlist")
37
+ allowlist += 1;
38
+ else
39
+ other += 1;
40
+ }
41
+ return { open, allowlist, other };
42
+ }
43
+ export function collectAttackSurfaceSummaryFindings(cfg) {
44
+ const group = summarizeGroupPolicy(cfg);
45
+ const elevated = cfg.tools?.elevated?.enabled !== false;
46
+ const hooksEnabled = cfg.hooks?.enabled === true;
47
+ const browserEnabled = Boolean(cfg.browser?.enabled ?? cfg.browser?.controlUrl);
48
+ const detail = `groups: open=${group.open}, allowlist=${group.allowlist}` +
49
+ `\n` +
50
+ `tools.elevated: ${elevated ? "enabled" : "disabled"}` +
51
+ `\n` +
52
+ `hooks: ${hooksEnabled ? "enabled" : "disabled"}` +
53
+ `\n` +
54
+ `browser control: ${browserEnabled ? "enabled" : "disabled"}`;
55
+ return [
56
+ {
57
+ checkId: "summary.attack_surface",
58
+ severity: "info",
59
+ title: "Attack surface summary",
60
+ detail,
61
+ },
62
+ ];
63
+ }
64
+ function isProbablySyncedPath(p) {
65
+ const s = p.toLowerCase();
66
+ return (s.includes("icloud") ||
67
+ s.includes("dropbox") ||
68
+ s.includes("google drive") ||
69
+ s.includes("googledrive") ||
70
+ s.includes("onedrive"));
71
+ }
72
+ export function collectSyncedFolderFindings(params) {
73
+ const findings = [];
74
+ if (isProbablySyncedPath(params.stateDir) || isProbablySyncedPath(params.configPath)) {
75
+ findings.push({
76
+ checkId: "fs.synced_dir",
77
+ severity: "warn",
78
+ title: "State/config path looks like a synced folder",
79
+ detail: `stateDir=${params.stateDir}, configPath=${params.configPath}. Synced folders (iCloud/Dropbox/OneDrive/Google Drive) can leak tokens and transcripts onto other devices.`,
80
+ remediation: `Keep CLAWDBOT_STATE_DIR on a local-only volume and re-run "clawdbot security audit --fix".`,
81
+ });
82
+ }
83
+ return findings;
84
+ }
85
+ function looksLikeEnvRef(value) {
86
+ const v = value.trim();
87
+ return v.startsWith("${") && v.endsWith("}");
88
+ }
89
+ export function collectSecretsInConfigFindings(cfg) {
90
+ const findings = [];
91
+ const password = typeof cfg.gateway?.auth?.password === "string" ? cfg.gateway.auth.password.trim() : "";
92
+ if (password && !looksLikeEnvRef(password)) {
93
+ findings.push({
94
+ checkId: "config.secrets.gateway_password_in_config",
95
+ severity: "warn",
96
+ title: "Gateway password is stored in config",
97
+ detail: "gateway.auth.password is set in the config file; prefer environment variables for secrets when possible.",
98
+ remediation: "Prefer CLAWDBOT_GATEWAY_PASSWORD (env) and remove gateway.auth.password from disk.",
99
+ });
100
+ }
101
+ const browserToken = typeof cfg.browser?.controlToken === "string" ? cfg.browser.controlToken.trim() : "";
102
+ if (browserToken && !looksLikeEnvRef(browserToken)) {
103
+ findings.push({
104
+ checkId: "config.secrets.browser_control_token_in_config",
105
+ severity: "warn",
106
+ title: "Browser control token is stored in config",
107
+ detail: "browser.controlToken is set in the config file; prefer environment variables for secrets when possible.",
108
+ remediation: "Prefer CLAWDBOT_BROWSER_CONTROL_TOKEN (env) and remove browser.controlToken from disk.",
109
+ });
110
+ }
111
+ const hooksToken = typeof cfg.hooks?.token === "string" ? cfg.hooks.token.trim() : "";
112
+ if (cfg.hooks?.enabled === true && hooksToken && !looksLikeEnvRef(hooksToken)) {
113
+ findings.push({
114
+ checkId: "config.secrets.hooks_token_in_config",
115
+ severity: "info",
116
+ title: "Hooks token is stored in config",
117
+ detail: "hooks.token is set in the config file; keep config perms tight and treat it like an API secret.",
118
+ });
119
+ }
120
+ return findings;
121
+ }
122
+ export function collectHooksHardeningFindings(cfg) {
123
+ const findings = [];
124
+ if (cfg.hooks?.enabled !== true)
125
+ return findings;
126
+ const token = typeof cfg.hooks?.token === "string" ? cfg.hooks.token.trim() : "";
127
+ if (token && token.length < 24) {
128
+ findings.push({
129
+ checkId: "hooks.token_too_short",
130
+ severity: "warn",
131
+ title: "Hooks token looks short",
132
+ detail: `hooks.token is ${token.length} chars; prefer a long random token.`,
133
+ });
134
+ }
135
+ const gatewayToken = typeof cfg.gateway?.auth?.token === "string" && cfg.gateway.auth.token.trim()
136
+ ? cfg.gateway.auth.token.trim()
137
+ : null;
138
+ if (token && gatewayToken && token === gatewayToken) {
139
+ findings.push({
140
+ checkId: "hooks.token_reuse_gateway_token",
141
+ severity: "warn",
142
+ title: "Hooks token reuses the Gateway token",
143
+ detail: "hooks.token matches gateway.auth token; compromise of hooks expands blast radius to the Gateway API.",
144
+ remediation: "Use a separate hooks.token dedicated to hook ingress.",
145
+ });
146
+ }
147
+ const browserToken = typeof cfg.browser?.controlToken === "string" && cfg.browser.controlToken.trim()
148
+ ? cfg.browser.controlToken.trim()
149
+ : process.env.CLAWDBOT_BROWSER_CONTROL_TOKEN?.trim() || null;
150
+ if (token && browserToken && token === browserToken) {
151
+ findings.push({
152
+ checkId: "hooks.token_reuse_browser_token",
153
+ severity: "warn",
154
+ title: "Hooks token reuses the browser control token",
155
+ detail: "hooks.token matches browser control token; compromise of hooks may enable browser control endpoints.",
156
+ remediation: "Use a separate hooks.token dedicated to hook ingress.",
157
+ });
158
+ }
159
+ const rawPath = typeof cfg.hooks?.path === "string" ? cfg.hooks.path.trim() : "";
160
+ if (rawPath === "/") {
161
+ findings.push({
162
+ checkId: "hooks.path_root",
163
+ severity: "critical",
164
+ title: "Hooks base path is '/'",
165
+ detail: "hooks.path='/' would shadow other HTTP endpoints and is unsafe.",
166
+ remediation: "Use a dedicated path like '/hooks'.",
167
+ });
168
+ }
169
+ return findings;
170
+ }
171
+ function addModel(models, raw, source) {
172
+ if (typeof raw !== "string")
173
+ return;
174
+ const id = raw.trim();
175
+ if (!id)
176
+ return;
177
+ models.push({ id, source });
178
+ }
179
+ function collectModels(cfg) {
180
+ const out = [];
181
+ addModel(out, cfg.agents?.defaults?.model?.primary, "agents.defaults.model.primary");
182
+ for (const f of cfg.agents?.defaults?.model?.fallbacks ?? [])
183
+ addModel(out, f, "agents.defaults.model.fallbacks");
184
+ addModel(out, cfg.agents?.defaults?.imageModel?.primary, "agents.defaults.imageModel.primary");
185
+ for (const f of cfg.agents?.defaults?.imageModel?.fallbacks ?? [])
186
+ addModel(out, f, "agents.defaults.imageModel.fallbacks");
187
+ const list = Array.isArray(cfg.agents?.list) ? cfg.agents?.list : [];
188
+ for (const agent of list ?? []) {
189
+ if (!agent || typeof agent !== "object")
190
+ continue;
191
+ const id = typeof agent.id === "string" ? agent.id : "";
192
+ const model = agent.model;
193
+ if (typeof model === "string") {
194
+ addModel(out, model, `agents.list.${id}.model`);
195
+ }
196
+ else if (model && typeof model === "object") {
197
+ addModel(out, model.primary, `agents.list.${id}.model.primary`);
198
+ const fallbacks = model.fallbacks;
199
+ if (Array.isArray(fallbacks)) {
200
+ for (const f of fallbacks)
201
+ addModel(out, f, `agents.list.${id}.model.fallbacks`);
202
+ }
203
+ }
204
+ }
205
+ return out;
206
+ }
207
+ const LEGACY_MODEL_PATTERNS = [
208
+ { id: "openai.gpt35", re: /\bgpt-3\.5\b/i, label: "GPT-3.5 family" },
209
+ { id: "anthropic.claude2", re: /\bclaude-(instant|2)\b/i, label: "Claude 2/Instant family" },
210
+ { id: "openai.gpt4_legacy", re: /\bgpt-4-(0314|0613)\b/i, label: "Legacy GPT-4 snapshots" },
211
+ ];
212
+ export function collectModelHygieneFindings(cfg) {
213
+ const findings = [];
214
+ const models = collectModels(cfg);
215
+ if (models.length === 0)
216
+ return findings;
217
+ const matches = [];
218
+ for (const entry of models) {
219
+ for (const pat of LEGACY_MODEL_PATTERNS) {
220
+ if (pat.re.test(entry.id)) {
221
+ matches.push({ model: entry.id, source: entry.source, reason: pat.label });
222
+ break;
223
+ }
224
+ }
225
+ }
226
+ if (matches.length > 0) {
227
+ const lines = matches
228
+ .slice(0, 12)
229
+ .map((m) => `- ${m.model} (${m.reason}) @ ${m.source}`)
230
+ .join("\n");
231
+ const more = matches.length > 12 ? `\n…${matches.length - 12} more` : "";
232
+ findings.push({
233
+ checkId: "models.legacy",
234
+ severity: "warn",
235
+ title: "Some configured models look legacy",
236
+ detail: "Older/legacy models can be less robust against prompt injection and tool misuse.\n" +
237
+ lines +
238
+ more,
239
+ remediation: "Prefer modern, instruction-hardened models for any bot that can run tools.",
240
+ });
241
+ }
242
+ return findings;
243
+ }
244
+ export async function collectPluginsTrustFindings(params) {
245
+ const findings = [];
246
+ const extensionsDir = path.join(params.stateDir, "extensions");
247
+ const st = await safeStat(extensionsDir);
248
+ if (!st.ok || !st.isDir)
249
+ return findings;
250
+ const entries = await fs.readdir(extensionsDir, { withFileTypes: true }).catch(() => []);
251
+ const pluginDirs = entries
252
+ .filter((e) => e.isDirectory())
253
+ .map((e) => e.name)
254
+ .filter(Boolean);
255
+ if (pluginDirs.length === 0)
256
+ return findings;
257
+ const allow = params.cfg.plugins?.allow;
258
+ const allowConfigured = Array.isArray(allow) && allow.length > 0;
259
+ if (!allowConfigured) {
260
+ findings.push({
261
+ checkId: "plugins.extensions_no_allowlist",
262
+ severity: "warn",
263
+ title: "Extensions exist but plugins.allow is not set",
264
+ detail: `Found ${pluginDirs.length} extension(s) under ${extensionsDir}. Without plugins.allow, any discovered plugin id may load (depending on config and plugin behavior).`,
265
+ remediation: "Set plugins.allow to an explicit list of plugin ids you trust.",
266
+ });
267
+ }
268
+ return findings;
269
+ }
270
+ function resolveIncludePath(baseConfigPath, includePath) {
271
+ return path.normalize(path.isAbsolute(includePath)
272
+ ? includePath
273
+ : path.resolve(path.dirname(baseConfigPath), includePath));
274
+ }
275
+ function listDirectIncludes(parsed) {
276
+ const out = [];
277
+ const visit = (value) => {
278
+ if (!value)
279
+ return;
280
+ if (Array.isArray(value)) {
281
+ for (const item of value)
282
+ visit(item);
283
+ return;
284
+ }
285
+ if (typeof value !== "object")
286
+ return;
287
+ const rec = value;
288
+ const includeVal = rec[INCLUDE_KEY];
289
+ if (typeof includeVal === "string")
290
+ out.push(includeVal);
291
+ else if (Array.isArray(includeVal)) {
292
+ for (const item of includeVal) {
293
+ if (typeof item === "string")
294
+ out.push(item);
295
+ }
296
+ }
297
+ for (const v of Object.values(rec))
298
+ visit(v);
299
+ };
300
+ visit(parsed);
301
+ return out;
302
+ }
303
+ async function collectIncludePathsRecursive(params) {
304
+ const visited = new Set();
305
+ const result = [];
306
+ const walk = async (basePath, parsed, depth) => {
307
+ if (depth > MAX_INCLUDE_DEPTH)
308
+ return;
309
+ for (const raw of listDirectIncludes(parsed)) {
310
+ const resolved = resolveIncludePath(basePath, raw);
311
+ if (visited.has(resolved))
312
+ continue;
313
+ visited.add(resolved);
314
+ result.push(resolved);
315
+ const rawText = await fs.readFile(resolved, "utf-8").catch(() => null);
316
+ if (!rawText)
317
+ continue;
318
+ const nestedParsed = (() => {
319
+ try {
320
+ return JSON5.parse(rawText);
321
+ }
322
+ catch {
323
+ return null;
324
+ }
325
+ })();
326
+ if (nestedParsed) {
327
+ // eslint-disable-next-line no-await-in-loop
328
+ await walk(resolved, nestedParsed, depth + 1);
329
+ }
330
+ }
331
+ };
332
+ await walk(params.configPath, params.parsed, 0);
333
+ return result;
334
+ }
335
+ export async function collectIncludeFilePermFindings(params) {
336
+ const findings = [];
337
+ if (!params.configSnapshot.exists)
338
+ return findings;
339
+ const configPath = params.configSnapshot.path;
340
+ const includePaths = await collectIncludePathsRecursive({
341
+ configPath,
342
+ parsed: params.configSnapshot.parsed,
343
+ });
344
+ if (includePaths.length === 0)
345
+ return findings;
346
+ for (const p of includePaths) {
347
+ // eslint-disable-next-line no-await-in-loop
348
+ const st = await safeStat(p);
349
+ if (!st.ok)
350
+ continue;
351
+ const bits = modeBits(st.mode);
352
+ if (isWorldWritable(bits) || isGroupWritable(bits)) {
353
+ findings.push({
354
+ checkId: "fs.config_include.perms_writable",
355
+ severity: "critical",
356
+ title: "Config include file is writable by others",
357
+ detail: `${p} mode=${formatOctal(bits)}; another user could influence your effective config.`,
358
+ remediation: `chmod 600 ${p}`,
359
+ });
360
+ }
361
+ else if (isWorldReadable(bits)) {
362
+ findings.push({
363
+ checkId: "fs.config_include.perms_world_readable",
364
+ severity: "critical",
365
+ title: "Config include file is world-readable",
366
+ detail: `${p} mode=${formatOctal(bits)}; include files can contain tokens and private settings.`,
367
+ remediation: `chmod 600 ${p}`,
368
+ });
369
+ }
370
+ else if (isGroupReadable(bits)) {
371
+ findings.push({
372
+ checkId: "fs.config_include.perms_group_readable",
373
+ severity: "warn",
374
+ title: "Config include file is group-readable",
375
+ detail: `${p} mode=${formatOctal(bits)}; include files can contain tokens and private settings.`,
376
+ remediation: `chmod 600 ${p}`,
377
+ });
378
+ }
379
+ }
380
+ return findings;
381
+ }
382
+ export async function collectStateDeepFilesystemFindings(params) {
383
+ const findings = [];
384
+ const oauthDir = resolveOAuthDir(params.env, params.stateDir);
385
+ const oauthStat = await safeStat(oauthDir);
386
+ if (oauthStat.ok && oauthStat.isDir) {
387
+ const bits = modeBits(oauthStat.mode);
388
+ if (isWorldWritable(bits) || isGroupWritable(bits)) {
389
+ findings.push({
390
+ checkId: "fs.credentials_dir.perms_writable",
391
+ severity: "critical",
392
+ title: "Credentials dir is writable by others",
393
+ detail: `${oauthDir} mode=${formatOctal(bits)}; another user could drop/modify credential files.`,
394
+ remediation: `chmod 700 ${oauthDir}`,
395
+ });
396
+ }
397
+ else if (isGroupReadable(bits) || isWorldReadable(bits)) {
398
+ findings.push({
399
+ checkId: "fs.credentials_dir.perms_readable",
400
+ severity: "warn",
401
+ title: "Credentials dir is readable by others",
402
+ detail: `${oauthDir} mode=${formatOctal(bits)}; credentials and allowlists can be sensitive.`,
403
+ remediation: `chmod 700 ${oauthDir}`,
404
+ });
405
+ }
406
+ }
407
+ const agentIds = Array.isArray(params.cfg.agents?.list)
408
+ ? params.cfg.agents?.list
409
+ .map((a) => (a && typeof a === "object" && typeof a.id === "string" ? a.id.trim() : ""))
410
+ .filter(Boolean)
411
+ : [];
412
+ const defaultAgentId = resolveDefaultAgentId(params.cfg);
413
+ const ids = Array.from(new Set([defaultAgentId, ...agentIds])).map((id) => normalizeAgentId(id));
414
+ for (const agentId of ids) {
415
+ const agentDir = path.join(params.stateDir, "agents", agentId, "agent");
416
+ const authPath = path.join(agentDir, "auth-profiles.json");
417
+ // eslint-disable-next-line no-await-in-loop
418
+ const authStat = await safeStat(authPath);
419
+ if (authStat.ok) {
420
+ const bits = modeBits(authStat.mode);
421
+ if (isWorldWritable(bits) || isGroupWritable(bits)) {
422
+ findings.push({
423
+ checkId: "fs.auth_profiles.perms_writable",
424
+ severity: "critical",
425
+ title: "auth-profiles.json is writable by others",
426
+ detail: `${authPath} mode=${formatOctal(bits)}; another user could inject credentials.`,
427
+ remediation: `chmod 600 ${authPath}`,
428
+ });
429
+ }
430
+ else if (isWorldReadable(bits) || isGroupReadable(bits)) {
431
+ findings.push({
432
+ checkId: "fs.auth_profiles.perms_readable",
433
+ severity: "warn",
434
+ title: "auth-profiles.json is readable by others",
435
+ detail: `${authPath} mode=${formatOctal(bits)}; auth-profiles.json contains API keys and OAuth tokens.`,
436
+ remediation: `chmod 600 ${authPath}`,
437
+ });
438
+ }
439
+ }
440
+ const storePath = path.join(params.stateDir, "agents", agentId, "sessions", "sessions.json");
441
+ // eslint-disable-next-line no-await-in-loop
442
+ const storeStat = await safeStat(storePath);
443
+ if (storeStat.ok) {
444
+ const bits = modeBits(storeStat.mode);
445
+ if (isWorldReadable(bits) || isGroupReadable(bits)) {
446
+ findings.push({
447
+ checkId: "fs.sessions_store.perms_readable",
448
+ severity: "warn",
449
+ title: "sessions.json is readable by others",
450
+ detail: `${storePath} mode=${formatOctal(bits)}; routing and transcript metadata can be sensitive.`,
451
+ remediation: `chmod 600 ${storePath}`,
452
+ });
453
+ }
454
+ }
455
+ }
456
+ const logFile = typeof params.cfg.logging?.file === "string" ? params.cfg.logging.file.trim() : "";
457
+ if (logFile) {
458
+ const expanded = logFile.startsWith("~") ? expandTilde(logFile, params.env) : logFile;
459
+ if (expanded) {
460
+ const logPath = path.resolve(expanded);
461
+ const st = await safeStat(logPath);
462
+ if (st.ok) {
463
+ const bits = modeBits(st.mode);
464
+ if (isWorldReadable(bits) || isGroupReadable(bits)) {
465
+ findings.push({
466
+ checkId: "fs.log_file.perms_readable",
467
+ severity: "warn",
468
+ title: "Log file is readable by others",
469
+ detail: `${logPath} mode=${formatOctal(bits)}; logs can contain private messages and tool output.`,
470
+ remediation: `chmod 600 ${logPath}`,
471
+ });
472
+ }
473
+ }
474
+ }
475
+ }
476
+ return findings;
477
+ }
478
+ function listGroupPolicyOpen(cfg) {
479
+ const out = [];
480
+ const channels = cfg.channels;
481
+ if (!channels || typeof channels !== "object")
482
+ return out;
483
+ for (const [channelId, value] of Object.entries(channels)) {
484
+ if (!value || typeof value !== "object")
485
+ continue;
486
+ const section = value;
487
+ if (section.groupPolicy === "open")
488
+ out.push(`channels.${channelId}.groupPolicy`);
489
+ const accounts = section.accounts;
490
+ if (accounts && typeof accounts === "object") {
491
+ for (const [accountId, accountVal] of Object.entries(accounts)) {
492
+ if (!accountVal || typeof accountVal !== "object")
493
+ continue;
494
+ const acc = accountVal;
495
+ if (acc.groupPolicy === "open")
496
+ out.push(`channels.${channelId}.accounts.${accountId}.groupPolicy`);
497
+ }
498
+ }
499
+ }
500
+ return out;
501
+ }
502
+ export function collectExposureMatrixFindings(cfg) {
503
+ const findings = [];
504
+ const openGroups = listGroupPolicyOpen(cfg);
505
+ if (openGroups.length === 0)
506
+ return findings;
507
+ const elevatedEnabled = cfg.tools?.elevated?.enabled !== false;
508
+ if (elevatedEnabled) {
509
+ findings.push({
510
+ checkId: "security.exposure.open_groups_with_elevated",
511
+ severity: "critical",
512
+ title: "Open groupPolicy with elevated tools enabled",
513
+ detail: `Found groupPolicy="open" at:\n${openGroups.map((p) => `- ${p}`).join("\n")}\n` +
514
+ "With tools.elevated enabled, a prompt injection in those rooms can become a high-impact incident.",
515
+ remediation: `Set groupPolicy="allowlist" and keep elevated allowlists extremely tight.`,
516
+ });
517
+ }
518
+ return findings;
519
+ }
520
+ export async function readConfigSnapshotForAudit(params) {
521
+ return await createConfigIO({
522
+ env: params.env,
523
+ configPath: params.configPath,
524
+ }).readConfigFileSnapshot();
525
+ }
@@ -0,0 +1,55 @@
1
+ import fs from "node:fs/promises";
2
+ export async function safeStat(targetPath) {
3
+ try {
4
+ const lst = await fs.lstat(targetPath);
5
+ return {
6
+ ok: true,
7
+ isSymlink: lst.isSymbolicLink(),
8
+ isDir: lst.isDirectory(),
9
+ mode: typeof lst.mode === "number" ? lst.mode : null,
10
+ uid: typeof lst.uid === "number" ? lst.uid : null,
11
+ gid: typeof lst.gid === "number" ? lst.gid : null,
12
+ };
13
+ }
14
+ catch (err) {
15
+ return {
16
+ ok: false,
17
+ isSymlink: false,
18
+ isDir: false,
19
+ mode: null,
20
+ uid: null,
21
+ gid: null,
22
+ error: String(err),
23
+ };
24
+ }
25
+ }
26
+ export function modeBits(mode) {
27
+ if (mode == null)
28
+ return null;
29
+ return mode & 0o777;
30
+ }
31
+ export function formatOctal(bits) {
32
+ if (bits == null)
33
+ return "unknown";
34
+ return bits.toString(8).padStart(3, "0");
35
+ }
36
+ export function isWorldWritable(bits) {
37
+ if (bits == null)
38
+ return false;
39
+ return (bits & 0o002) !== 0;
40
+ }
41
+ export function isGroupWritable(bits) {
42
+ if (bits == null)
43
+ return false;
44
+ return (bits & 0o020) !== 0;
45
+ }
46
+ export function isWorldReadable(bits) {
47
+ if (bits == null)
48
+ return false;
49
+ return (bits & 0o004) !== 0;
50
+ }
51
+ export function isGroupReadable(bits) {
52
+ if (bits == null)
53
+ return false;
54
+ return (bits & 0o040) !== 0;
55
+ }