total-recall-brain 3.31.0 → 3.31.2

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.
@@ -17,7 +17,7 @@
17
17
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
18
18
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
19
19
  <title>Total Recall — Portable Memory</title>
20
- <script type="module" crossorigin src="/assets/index-DGMNZi_1.js"></script>
20
+ <script type="module" crossorigin src="/assets/index-FOJG4jQh.js"></script>
21
21
  <link rel="stylesheet" crossorigin href="/assets/index-C7_Xpk27.css">
22
22
  </head>
23
23
  <body>
@@ -16,7 +16,7 @@
16
16
  "react": "^19.2.6",
17
17
  "react-dom": "^19.2.6",
18
18
  "react-force-graph-3d": "^1.29.1",
19
- "react-router-dom": "^7.15.0",
19
+ "react-router-dom": "^7.18.4",
20
20
  "recharts": "^3.9.2",
21
21
  "three": "^0.185.1",
22
22
  "three-spritetext": "^1.10.0"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "total-recall-brain",
3
- "version": "3.31.0",
3
+ "version": "3.31.2",
4
4
  "description": "Personal AI system — database-free, SSSS-powered intelligence engine deployable to any POSIX host.",
5
5
  "type": "module",
6
6
  "workspaces": [
@@ -16,7 +16,6 @@ repo_scoped: true
16
16
  > **Only agent skill in this package.** Implementation helpers live in `modules/` (not skills).
17
17
  > Openwiki ships with the brain at `openwiki/`.
18
18
 
19
-
20
19
  Welcome to the master control skill for the **Total Recall Autonomous AI OS**. > [!NOTE]
21
20
  > **Total Recall Architecture:**
22
21
  > The Total Recall memory kernel runs as a standalone REST service. Agents interact with the memory vault, perform semantic searches, and compile rules directly using lightweight, filesystem-native REST API calls via standard shell `curl` commands.
@@ -555,12 +554,6 @@ that dead-ends back at a manual dashboard step.
555
554
 
556
555
  ---
557
556
 
558
- <!-- BEGIN INJECTED MEMORY: do not edit by hand; rebuilt by total-recall surface -->
559
- <!-- @route: tfidf, generated_at: 2026-05-21T03:34:14.837Z -->
560
-
561
- <!-- END INJECTED MEMORY -->
562
-
563
-
564
557
  ---
565
558
 
566
559
  ## 🕸️ MESH / CONTROL SERVER (`mesh` subcommand)
@@ -2,6 +2,7 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import yaml from 'yaml';
4
4
  import { resolveBrainDir, parseLayerFlag } from './agent-dir.mjs';
5
+ import { writeFileSecure } from '../core/secure-file.mjs';
5
6
 
6
7
  // Pre-defined mapping of known settings
7
8
  const KEY_MAP = {
@@ -224,7 +225,7 @@ export default async function configCommand(args) {
224
225
  if (!fs.existsSync(configDir)) {
225
226
  fs.mkdirSync(configDir, { recursive: true });
226
227
  }
227
- fs.writeFileSync(targetPath, yaml.stringify(targetObj), { encoding: 'utf8', mode: 0o600 });
228
+ writeFileSecure(targetPath, yaml.stringify(targetObj), { encoding: 'utf8', mode: 0o600 });
228
229
 
229
230
  console.log(`\n ✅ Success! Config key "${key}" ${layerLabel} set to: ${JSON.stringify(parsedValue)}`);
230
231
  console.log(` 📍 Saved in: ${targetPath}\n`);
@@ -19,6 +19,7 @@ import os from 'node:os';
19
19
  import path from 'node:path';
20
20
  import bcrypt from 'bcrypt';
21
21
  import yaml from 'yaml';
22
+ import { writeFileSecure } from '../core/secure-file.mjs';
22
23
  import { BCRYPT_COST } from '../server/auth.mjs';
23
24
 
24
25
 
@@ -175,7 +176,7 @@ export function startDeployUI(port = 3001) {
175
176
  if (opts.cloudRegion) current['cfg-cloud-region'] = opts.cloudRegion;
176
177
  if (opts.installGlobal !== undefined) current['installGlobal'] = opts.installGlobal;
177
178
  if (opts.projectPaths) current['projectPaths'] = opts.projectPaths;
178
- fs.writeFileSync(configFile, JSON.stringify(current, null, 2), { encoding: 'utf8', mode: 0o600 });
179
+ writeFileSecure(configFile, JSON.stringify(current, null, 2), { encoding: 'utf8', mode: 0o600 });
179
180
 
180
181
  } catch (e) {
181
182
  console.error('Failed to auto-persist install options on disk:', e);
@@ -572,7 +573,7 @@ export function startDeployUI(port = 3001) {
572
573
  }
573
574
 
574
575
  const updated = { ...current, ...data };
575
- fs.writeFileSync(configFile, JSON.stringify(updated, null, 2), { encoding: 'utf8', mode: 0o600 });
576
+ writeFileSecure(configFile, JSON.stringify(updated, null, 2), { encoding: 'utf8', mode: 0o600 });
576
577
  syncInstallOptionsFromDisk();
577
578
 
578
579
  res.writeHead(200, { 'Content-Type': 'application/json' });
@@ -1114,7 +1115,7 @@ export function startDeployUI(port = 3001) {
1114
1115
  }
1115
1116
  current['cfg-github-token'] = token;
1116
1117
  current['backupRepo'] = backupUrl;
1117
- fs.writeFileSync(configFile, JSON.stringify(current, null, 2), { encoding: 'utf8', mode: 0o600 });
1118
+ writeFileSecure(configFile, JSON.stringify(current, null, 2), { encoding: 'utf8', mode: 0o600 });
1118
1119
 
1119
1120
  log('✅ Encryption keys and tokens written.');
1120
1121
 
@@ -1735,7 +1736,7 @@ console.log("REMOTE_PASS_OK");
1735
1736
  if (_installOptions.dashboardPassword) {
1736
1737
  current['cfg-dashboard-password'] = _installOptions.dashboardPassword;
1737
1738
  }
1738
- fs.writeFileSync(configFile, JSON.stringify(current, null, 2), { encoding: 'utf8', mode: 0o600 });
1739
+ writeFileSecure(configFile, JSON.stringify(current, null, 2), { encoding: 'utf8', mode: 0o600 });
1739
1740
  } catch (e) {
1740
1741
  console.error('Failed to auto-persist remote install options on disk:', e);
1741
1742
  }
@@ -43,6 +43,7 @@ import yaml from 'yaml';
43
43
  import bcrypt from 'bcrypt';
44
44
  import crypto from 'node:crypto';
45
45
  import { BCRYPT_COST } from '../server/auth.mjs';
46
+ import { writeFileSecure } from '../core/secure-file.mjs';
46
47
 
47
48
 
48
49
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -407,7 +408,7 @@ function hardenSecurityConfig(dryRun, dashboardPassword = null) {
407
408
  log(' 🔑 Dashboard credentials left unconfigured. You will set your admin password on first dashboard access.');
408
409
  }
409
410
 
410
- fs.writeFileSync(securityPath, yaml.stringify(config), { encoding: 'utf8', mode: 0o600 });
411
+ writeFileSecure(securityPath, yaml.stringify(config), { encoding: 'utf8', mode: 0o600 });
411
412
  logOk('Security config hardened: HTTPS required, Express bound to localhost, no public health, no legacy local PAT');
412
413
  }
413
414
 
@@ -1095,7 +1096,7 @@ export default async function deploy(args) {
1095
1096
  if (wizardOpts.dashboardPassword) {
1096
1097
  current['cfg-dashboard-password'] = wizardOpts.dashboardPassword;
1097
1098
  }
1098
- fs.writeFileSync(configFile, JSON.stringify(current, null, 2), { encoding: 'utf8', mode: 0o600 });
1099
+ writeFileSecure(configFile, JSON.stringify(current, null, 2), { encoding: 'utf8', mode: 0o600 });
1099
1100
  } catch (e) {
1100
1101
  console.error('Failed to auto-persist cloud install options on disk:', e);
1101
1102
  }
package/src/cli/init.mjs CHANGED
@@ -31,6 +31,7 @@ import os from 'node:os';
31
31
  import { fileURLToPath } from 'node:url';
32
32
  import { exec, spawn, spawnSync } from 'node:child_process';
33
33
  import { projectSkillsForScope, detectActiveSkillTargets } from './skill-projection.mjs';
34
+ import { writeFileSecure } from '../core/secure-file.mjs';
34
35
 
35
36
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
36
37
  const ROOT = path.join(__dirname, '..', '..');
@@ -408,10 +409,10 @@ export default async function init(args) {
408
409
  }
409
410
 
410
411
  if (!config.dashboard.password_hash) {
411
- fs.writeFileSync(securityPath, yaml.stringify(config), { encoding: "utf8", mode: 0o600 });
412
+ writeFileSecure(securityPath, yaml.stringify(config), { encoding: "utf8", mode: 0o600 });
412
413
  passwordMessage = "\n 🔑 Dashboard Access: Unconfigured\n (You will be prompted to set your password upon first browser access)";
413
414
  } else {
414
- fs.writeFileSync(securityPath, yaml.stringify(config), { encoding: 'utf8', mode: 0o600 });
415
+ writeFileSecure(securityPath, yaml.stringify(config), { encoding: 'utf8', mode: 0o600 });
415
416
  passwordMessage = '\n 🔑 Dashboard credentials preserved (custom password hash already configured)';
416
417
  }
417
418
  } catch (err) {
@@ -586,7 +587,7 @@ export default async function init(args) {
586
587
  current['cfg-health-url'] = 'http://localhost:3000/health';
587
588
  }
588
589
 
589
- fs.writeFileSync(configFile, JSON.stringify(current, null, 2), { encoding: 'utf8', mode: 0o600 });
590
+ writeFileSecure(configFile, JSON.stringify(current, null, 2), { encoding: 'utf8', mode: 0o600 });
590
591
  logOk('UI deploy configuration persisted to wizard-config.json!');
591
592
  }
592
593
 
@@ -670,7 +671,7 @@ export default async function init(args) {
670
671
  fs.mkdirSync(cfgDir, { recursive: true });
671
672
  const brainCfg = { url: opts.brain };
672
673
  if (opts.token) brainCfg.token = opts.token;
673
- fs.writeFileSync(path.join(cfgDir, 'brain.json'), JSON.stringify(brainCfg, null, 2), { mode: 0o600 });
674
+ writeFileSecure(path.join(cfgDir, 'brain.json'), JSON.stringify(brainCfg, null, 2), { mode: 0o600 });
674
675
  logOk(`Registered brain at ${opts.brain}. Run \`npx total-recall sync\` to pull instructions.`);
675
676
  } else {
676
677
  // Zero-config bootstrap: auto-generate localhost brain.json and valid Developer PAT out-of-the-box
@@ -696,7 +697,7 @@ export default async function init(args) {
696
697
  const keyData = issueKey('Default Local Developer Key', { scopes: ['*'] });
697
698
  brainCfg.url = brainCfg.url || 'http://localhost:3000';
698
699
  brainCfg.token = brainCfg.token || keyData.token;
699
- fs.writeFileSync(brainJsonPath, JSON.stringify(brainCfg, null, 2), { mode: 0o600 });
700
+ writeFileSecure(brainJsonPath, JSON.stringify(brainCfg, null, 2), { mode: 0o600 });
700
701
  logOk(`Bootstrap configuration successfully generated and pre-authorized at ${brainCfg.url}`);
701
702
  } catch (err) {
702
703
  logWarn(`Could not auto-generate bootstrap developer key: ${err.message}`);
@@ -786,7 +787,7 @@ export default async function init(args) {
786
787
  current['cfg-dash-url'] = `${tunnelUrl}/`;
787
788
  current['cfg-health-url'] = `${tunnelUrl}/health`;
788
789
 
789
- fs.writeFileSync(configFile, JSON.stringify(current, null, 2), { encoding: 'utf8', mode: 0o600 });
790
+ writeFileSecure(configFile, JSON.stringify(current, null, 2), { encoding: 'utf8', mode: 0o600 });
790
791
  logOk('Registered Quick Tunnel URL in wizard-config.json!');
791
792
  dashboardUrl = `${tunnelUrl}/`;
792
793
  } else {
@@ -5,6 +5,7 @@ import bcrypt from 'bcrypt';
5
5
  import yaml from 'yaml';
6
6
  import { BCRYPT_COST } from '../server/auth.mjs';
7
7
  import { brainDir } from '../core/config.mjs';
8
+ import { writeFileSecure } from '../core/secure-file.mjs';
8
9
 
9
10
  /**
10
11
  * Read a password without echoing it, and without it ever becoming an argv
@@ -128,7 +129,7 @@ export default async function resetPassword(args) {
128
129
  const isDefault = password === 'totalrecall';
129
130
  config.dashboard.force_password_reset = isDefault;
130
131
 
131
- fs.writeFileSync(securityPath, yaml.stringify(config), { encoding: 'utf8', mode: 0o600 });
132
+ writeFileSecure(securityPath, yaml.stringify(config), { encoding: 'utf8', mode: 0o600 });
132
133
 
133
134
  // Never echo the password back. The operator just typed it; repeating it to
134
135
  // stdout puts it into scrollback, into any log capturing this command, and
@@ -30,6 +30,7 @@ import os from 'node:os';
30
30
  import path from 'node:path';
31
31
  import { spawnSync, execFileSync } from 'node:child_process';
32
32
  import backup from './backup.mjs';
33
+ import { writeFileSecure } from '../core/secure-file.mjs';
33
34
 
34
35
  // ╔══════════════════════════════════════════════════════════════════════════╗
35
36
  // ║ FORBIDDEN FLAGS — These patterns must NEVER be accepted as arguments ║
@@ -531,7 +532,7 @@ export default async function uninstall(args = []) {
531
532
 
532
533
  if (Object.keys(secretsObj).length > 0) {
533
534
  fs.mkdirSync(agentDir, { recursive: true });
534
- fs.writeFileSync(
535
+ writeFileSecure(
535
536
  path.join(agentDir, 'secrets.enc'),
536
537
  JSON.stringify(secretsObj, null, 2),
537
538
  { encoding: 'utf8', mode: 0o600 }
@@ -19,6 +19,7 @@ import fs from 'fs';
19
19
  import path from 'path';
20
20
  import { fileURLToPath } from 'url';
21
21
  import YAML from 'yaml';
22
+ import { writeFileSecure } from './secure-file.mjs';
22
23
 
23
24
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
24
25
  const ROOT = path.resolve(__dirname, '../..');
@@ -162,7 +163,7 @@ export function writeBrainIdentity(brainDir, { name, role = 'project', tags = []
162
163
  updated_at: new Date().toISOString(),
163
164
  created_at: current.created_at || new Date().toISOString(),
164
165
  };
165
- fs.writeFileSync(brainJsonPath, JSON.stringify(next, null, 2), { encoding: 'utf8', mode: 0o600 });
166
+ writeFileSecure(brainJsonPath, JSON.stringify(next, null, 2), { encoding: 'utf8', mode: 0o600 });
166
167
  return next;
167
168
  }
168
169
 
@@ -13,6 +13,7 @@ import os from 'os';
13
13
  import YAML from 'yaml';
14
14
  import { encryptSecrets, decryptSecrets, encryptSecretsSync, decryptSecretsSync } from './crypto.mjs';
15
15
  import { DEFAULT_KEYCHAIN_SERVICE, readKeychainPassword } from './secrets-keychain.mjs';
16
+ import { writeFileSecure, appendFileSecure } from './secure-file.mjs';
16
17
 
17
18
  const META_KEY = '__tr_secrets_meta';
18
19
 
@@ -170,7 +171,7 @@ export function saveSecretsSync(brainDir, obj) {
170
171
  const password = secretsPassword();
171
172
  if (!password) throw new Error('TR_SECRETS_PASSWORD or TR_MASTER_PASSWORD is required to write secrets');
172
173
  const buf = encryptSecretsSync(obj, password);
173
- fs.writeFileSync(filePath, buf, { mode: 0o600 });
174
+ writeFileSecure(filePath, buf, { mode: 0o600 });
174
175
  }
175
176
 
176
177
  /**
@@ -221,7 +222,7 @@ export async function saveSecrets(brainDir, secrets) {
221
222
 
222
223
  if (!password) throw new Error('TR_SECRETS_PASSWORD or TR_MASTER_PASSWORD is required to write secrets');
223
224
  const buf = await encryptSecrets(secrets, password);
224
- fs.writeFileSync(filePath, buf, { mode: 0o600 });
225
+ writeFileSecure(filePath, buf, { mode: 0o600 });
225
226
  }
226
227
 
227
228
  /**
@@ -314,7 +315,7 @@ function appendAudit(brainDir, event) {
314
315
  ...event,
315
316
  // never include values
316
317
  });
317
- fs.appendFileSync(auditPath, line + '\n', { mode: 0o600 });
318
+ appendFileSecure(auditPath, line + '\n', { mode: 0o600 });
318
319
  }
319
320
 
320
321
  /**
@@ -983,7 +984,7 @@ export function recordUsage(brainDir, event) {
983
984
  source: event.source || 'cli',
984
985
  key_ref: event.key_ref || null, // secret *name* only, never value
985
986
  };
986
- fs.appendFileSync(usagePath, JSON.stringify(row) + '\n', { mode: 0o600 });
987
+ appendFileSecure(usagePath, JSON.stringify(row) + '\n', { mode: 0o600 });
987
988
  return row;
988
989
  }
989
990
 
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Secure file writes for credential-bearing files.
3
+ *
4
+ * `fs.writeFileSync`/`fs.appendFileSync` only apply their `mode` option when the
5
+ * file is CREATED. Rewriting a file that already exists silently preserves the
6
+ * old permissions, so a security.yml or secrets.enc that was ever created under
7
+ * a loose umask (or restored from a backup, or copied by a sync) stays
8
+ * world-readable forever no matter how many times it is rewritten with
9
+ * `{ mode: 0o600 }`.
10
+ *
11
+ * Observed live: config/security.yml at mode 644 while holding the dashboard
12
+ * bcrypt password_hash.
13
+ *
14
+ * Every writer of a file containing hashes, tokens, or key material must go
15
+ * through here so the mode is re-asserted on every write, not just the first.
16
+ */
17
+
18
+ import fs from 'fs';
19
+
20
+ export const SECRET_FILE_MODE = 0o600;
21
+
22
+ /**
23
+ * Re-assert permissions on an existing file.
24
+ *
25
+ * @param {string} filePath
26
+ * @param {number} [mode]
27
+ * @returns {boolean} true when the mode was applied
28
+ */
29
+ export function chmodSecure(filePath, mode = SECRET_FILE_MODE) {
30
+ try {
31
+ fs.chmodSync(filePath, mode);
32
+ return true;
33
+ } catch {
34
+ // Best effort: chmod can fail on non-POSIX filesystems.
35
+ return false;
36
+ }
37
+ }
38
+
39
+ function requireSecureMode(filePath, mode) {
40
+ if (!chmodSecure(filePath, mode)) {
41
+ throw new Error(`Could not secure file permissions: ${filePath}`);
42
+ }
43
+ }
44
+
45
+ /**
46
+ * writeFileSync that actually enforces `mode` on rewrite.
47
+ *
48
+ * @param {string} filePath
49
+ * @param {string|Buffer} data
50
+ * @param {object} [options] same as fs.writeFileSync; `mode` defaults to 0o600
51
+ */
52
+ export function writeFileSecure(filePath, data, options = {}) {
53
+ const { mode = SECRET_FILE_MODE, ...rest } = options;
54
+ if (fs.existsSync(filePath)) requireSecureMode(filePath, SECRET_FILE_MODE);
55
+ fs.writeFileSync(filePath, data, { ...rest, mode });
56
+ requireSecureMode(filePath, mode);
57
+ }
58
+
59
+ /**
60
+ * appendFileSync that actually enforces `mode` on an existing file.
61
+ *
62
+ * @param {string} filePath
63
+ * @param {string|Buffer} data
64
+ * @param {object} [options] same as fs.appendFileSync; `mode` defaults to 0o600
65
+ */
66
+ export function appendFileSecure(filePath, data, options = {}) {
67
+ const { mode = SECRET_FILE_MODE, ...rest } = options;
68
+ if (fs.existsSync(filePath)) requireSecureMode(filePath, SECRET_FILE_MODE);
69
+ fs.appendFileSync(filePath, data, { ...rest, mode });
70
+ requireSecureMode(filePath, mode);
71
+ }
@@ -9,6 +9,7 @@
9
9
  import fs from 'node:fs';
10
10
  import path from 'node:path';
11
11
  import crypto from 'node:crypto';
12
+ import { writeFileSecure } from './secure-file.mjs';
12
13
  import {
13
14
  generateRegistrationOptions,
14
15
  verifyRegistrationResponse,
@@ -49,7 +50,7 @@ function loadStore(brainDir) {
49
50
  function saveStore(brainDir, store) {
50
51
  const filePath = resolveWebAuthnPath(brainDir);
51
52
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
52
- fs.writeFileSync(filePath, JSON.stringify(store, null, 2) + '\n', {
53
+ writeFileSecure(filePath, JSON.stringify(store, null, 2) + '\n', {
53
54
  encoding: 'utf8',
54
55
  mode: 0o600,
55
56
  });
@@ -11,6 +11,7 @@ import { findValidKeyByToken, keyHasAnyScope, recordKeyUsage } from './keys.mjs'
11
11
  import { logger } from '../core/logger.mjs';
12
12
 
13
13
  import { agentDir, brainDir, sessionSecret, nodeEnv } from '../core/config.mjs';
14
+ import { writeFileSecure } from '../core/secure-file.mjs';
14
15
 
15
16
  const BRAIN_DIR = brainDir;
16
17
  const CONFIG_FILE = path.join(BRAIN_DIR, 'config', 'security.yml');
@@ -26,7 +27,7 @@ try {
26
27
  JWT_SECRET = crypto.randomBytes(32).toString('hex');
27
28
  try {
28
29
  fs.mkdirSync(path.dirname(JWT_SECRET_PATH), { recursive: true });
29
- fs.writeFileSync(JWT_SECRET_PATH, JWT_SECRET, { mode: 0o600 });
30
+ writeFileSecure(JWT_SECRET_PATH, JWT_SECRET, { mode: 0o600 });
30
31
  } catch { /* non-fatal — will regenerate on next restart */ }
31
32
  }
32
33
 
@@ -450,7 +451,7 @@ export async function changePasswordHandler(req, res) {
450
451
  config.dashboard.password_hash = hash;
451
452
  config.dashboard.force_password_reset = false;
452
453
 
453
- fs.writeFileSync(CONFIG_FILE, yaml.stringify(config));
454
+ writeFileSecure(CONFIG_FILE, yaml.stringify(config), { encoding: 'utf8', mode: 0o600 });
454
455
 
455
456
  // Dynamically update the backup secrets.enc file to preserve the password hash
456
457
  try {
@@ -18,6 +18,7 @@ import path from 'node:path';
18
18
  import { fileURLToPath } from 'node:url';
19
19
  import zlib from 'node:zlib';
20
20
  import fs from 'node:fs';
21
+ import { writeFileSecure } from '../core/secure-file.mjs';
21
22
  import {
22
23
  apiRateLimiter,
23
24
  corsOptions,
@@ -1078,7 +1079,7 @@ if (HOST !== '127.0.0.1' && HOST !== '0.0.0.0') {
1078
1079
  wizardCfg['cfg-dash-url'] = `${tunnelUrl}/dashboard`;
1079
1080
  wizardCfg['cfg-health-url'] = `${tunnelUrl}/health`;
1080
1081
 
1081
- fs.writeFileSync(configFile, JSON.stringify(wizardCfg, null, 2), { encoding: 'utf8', mode: 0o600 });
1082
+ writeFileSecure(configFile, JSON.stringify(wizardCfg, null, 2), { encoding: 'utf8', mode: 0o600 });
1082
1083
  logger.info('server', `Updated wizard-config.json with active Quick Tunnel URL: ${tunnelUrl}`);
1083
1084
  } else {
1084
1085
  logger.warn('server', 'Could not allocate Quick Tunnel URL. Please check logs in brainDir/logs/cloudflared.log');
@@ -6,6 +6,7 @@ import bcrypt from 'bcrypt';
6
6
  import jwt from 'jsonwebtoken';
7
7
  import { WebSocket } from 'ws';
8
8
  import { brainDir as configBrainDir } from '../../core/config.mjs';
9
+ import { chmodSecure, writeFileSecure } from '../../core/secure-file.mjs';
9
10
 
10
11
  // Setup database paths in brainDir
11
12
  const DATA_DIR = path.join(configBrainDir, 'collab');
@@ -22,10 +23,13 @@ export function resolveJwtSecret(env = process.env, secretFile = JWT_SECRET_FILE
22
23
  if (env.JWT_SECRET) return env.JWT_SECRET;
23
24
  try {
24
25
  const existing = fs.readFileSync(secretFile, 'utf8').trim();
25
- if (existing.length >= 32) return existing;
26
+ if (existing.length >= 32) {
27
+ chmodSecure(secretFile);
28
+ return existing;
29
+ }
26
30
  } catch { /* not created yet */ }
27
31
  const generated = crypto.randomBytes(48).toString('base64url');
28
- fs.writeFileSync(secretFile, generated, { encoding: 'utf8', mode: 0o600 });
32
+ writeFileSecure(secretFile, generated, { encoding: 'utf8' });
29
33
  return generated;
30
34
  }
31
35
  const JWT_SECRET = resolveJwtSecret();
@@ -44,7 +48,7 @@ function readJsonFile(filePath, defaultData = []) {
44
48
  }
45
49
 
46
50
  function writeJsonFile(filePath, data) {
47
- fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8');
51
+ writeFileSecure(filePath, JSON.stringify(data, null, 2), { encoding: 'utf8' });
48
52
  }
49
53
 
50
54
  // ─── Database Helpers ────────────────────────────────────────────────────────
@@ -15,6 +15,7 @@ import yaml from 'yaml';
15
15
  import { requireAuth, requireScope, loadSecurityConfig } from '../auth.mjs';
16
16
  import { BRAIN_DIR, CONFIG_DIR, AGENT_DIR, serverError, badRequest } from './_shared.mjs';
17
17
  import { loadRuntimeConfig } from '../../core/runtime.mjs';
18
+ import { writeFileSecure } from '../../core/secure-file.mjs';
18
19
 
19
20
  const router = Router();
20
21
 
@@ -152,10 +153,10 @@ router.post('/api/config-json', requireAuth, requireScope('config:write'), async
152
153
  }
153
154
 
154
155
  if (security) {
155
- fs.writeFileSync(securityPath, yaml.stringify(security), { encoding: 'utf8', mode: 0o600 });
156
+ writeFileSecure(securityPath, yaml.stringify(security), { encoding: 'utf8', mode: 0o600 });
156
157
  }
157
158
  if (budget) {
158
- fs.writeFileSync(budgetPath, yaml.stringify(budget), { encoding: 'utf8', mode: 0o600 });
159
+ writeFileSecure(budgetPath, yaml.stringify(budget), { encoding: 'utf8', mode: 0o600 });
159
160
  }
160
161
  if (brain) {
161
162
  let existingBrain = {};
@@ -167,7 +168,7 @@ router.post('/api/config-json', requireAuth, requireScope('config:write'), async
167
168
  nextBrain.token = existingBrain.token;
168
169
  }
169
170
  delete nextBrain.has_token;
170
- fs.writeFileSync(brainPath, JSON.stringify(nextBrain, null, 2), { encoding: 'utf8', mode: 0o600 });
171
+ writeFileSecure(brainPath, JSON.stringify(nextBrain, null, 2), { encoding: 'utf8', mode: 0o600 });
171
172
  }
172
173
  if (secrets) {
173
174
  // Only accept real secret values — ignore masked placeholders from GET /api/config-json.