@ijfw/memory-server 1.6.0 → 1.6.1

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ijfw/memory-server",
3
- "version": "1.6.0",
3
+ "version": "1.6.1",
4
4
  "description": "Cross-platform persistent memory server for IJFW. 14 MCP tools (memory + admin/update + brain). Works with 15 platforms: 14 via MCP (Claude Code, Codex, Gemini CLI, Cursor, Windsurf, Copilot, Hermes, Wayland, OpenCode, QwenCode, Cline, KimiCode, OpenClaw, Antigravity) plus Aider via the rules-only tier.",
5
5
  "author": "Sean Donahoe",
6
6
  "contributors": [
@@ -319,6 +319,10 @@ function parseArgsInner(args) {
319
319
  return { cmd: 'doctor' };
320
320
  }
321
321
 
322
+ if (args[0] === 'init') {
323
+ return { cmd: 'init', force: args.includes('--force') };
324
+ }
325
+
322
326
  if (args[0] === 'update') {
323
327
  const opts = { cmd: 'update' };
324
328
  for (let i = 1; i < args.length; i++) {
@@ -2865,6 +2869,8 @@ if (isMainModule) {
2865
2869
  cmdImport(parsed).catch(err => { console.error(err.message); process.exit(1); });
2866
2870
  } else if (parsed.cmd === 'doctor') {
2867
2871
  cmdDoctor(parsed);
2872
+ } else if (parsed.cmd === 'init') {
2873
+ cmdInit(parsed);
2868
2874
  } else if (parsed.cmd === 'update') {
2869
2875
  cmdUpdate(parsed);
2870
2876
  } else if (parsed.cmd === 'version') {
@@ -2954,6 +2960,46 @@ function findCliAsset(...rel) {
2954
2960
  ].filter(Boolean);
2955
2961
  return candidates.find(p => existsSync(p)) || null;
2956
2962
  }
2963
+ // `ijfw init` -- explicitly bless the current folder for codebase indexing.
2964
+ // The indexer (scripts/build-codebase-index.sh) refuses any folder that has no
2965
+ // project marker (issue #16). For a plain working folder with no .git/package.json
2966
+ // etc, this drops a .ijfw/project marker so the indexer will index it. It will
2967
+ // NOT bless the home directory or filesystem root -- that is the whole point of
2968
+ // the guard.
2969
+ function cmdInit(parsed = {}) {
2970
+ const cwd = process.cwd();
2971
+ let phys;
2972
+ try { phys = realpathSync(cwd); } catch { phys = resolve(cwd); }
2973
+ let homePhys;
2974
+ try { homePhys = realpathSync(homedir()); } catch { homePhys = homedir(); }
2975
+ if (phys === '/' || phys === homePhys) {
2976
+ console.error('ijfw init: refusing to bless your home directory or the filesystem root for indexing.');
2977
+ console.error('Run `ijfw init` from inside an actual project folder.');
2978
+ process.exit(1);
2979
+ }
2980
+ const marker = join(cwd, '.ijfw', 'project');
2981
+ try {
2982
+ mkdirSync(dirname(marker), { recursive: true });
2983
+ if (existsSync(marker) && !parsed.force) {
2984
+ console.log(`This folder is already initialised for IJFW indexing (${marker}).`);
2985
+ process.exit(0);
2986
+ }
2987
+ const stamp = new Date().toISOString();
2988
+ writeFileSync(
2989
+ marker,
2990
+ `# IJFW project marker\n` +
2991
+ `# Created by \`ijfw init\`. This folder is approved for codebase indexing.\n` +
2992
+ `# Safe to commit. Delete this file to stop IJFW indexing this folder.\n` +
2993
+ `created_at: ${stamp}\n`,
2994
+ { mode: 0o644 }
2995
+ );
2996
+ console.log('IJFW initialised. This folder is now approved for codebase indexing.');
2997
+ console.log(`Marker: ${marker}`);
2998
+ } catch (err) {
2999
+ console.error(`ijfw init: could not write marker -- ${err.message}`);
3000
+ process.exit(1);
3001
+ }
3002
+ }
2957
3003
  function cmdInstall() {
2958
3004
  const script = findCliAsset('scripts', 'install.sh');
2959
3005
  if (!script) {
@@ -174,10 +174,26 @@ function requireLocalhost(req, res) {
174
174
  return false;
175
175
  }
176
176
 
177
+ // CSRF guard: reject cross-origin browser requests to the data API. Browsers
178
+ // stamp Sec-Fetch-Site; the dashboard's own page is 'same-origin', direct tools
179
+ // (curl, address bar) send 'none'/nothing. Only same-machine cross-origin pages
180
+ // hit 'cross-site'/'same-site' -- block those on /api.
181
+ function rejectCrossSiteApi(req, res, path) {
182
+ if (!path.startsWith('/api')) return false;
183
+ const sfs = req.headers['sec-fetch-site'];
184
+ if (sfs === 'cross-site' || sfs === 'same-site') {
185
+ res.writeHead(403, { 'Content-Type': 'application/json' });
186
+ res.end('{"error":"cross-origin request rejected"}');
187
+ return true;
188
+ }
189
+ return false;
190
+ }
191
+
177
192
  // ---------- simple router ----------
178
193
  function route(req, res, routes) {
179
194
  const url = new URL(req.url, 'http://localhost');
180
195
  const path = url.pathname;
196
+ if (rejectCrossSiteApi(req, res, path)) return;
181
197
  for (const [pattern, handler] of routes) {
182
198
  if (typeof pattern === 'string' ? path === pattern : pattern.test(path)) {
183
199
  handler(req, res, url);
@@ -232,9 +232,11 @@ async function probeGoogle(env, fetchImpl) {
232
232
  if (!key) return null;
233
233
  const { signal, cancel } = makeAbortable();
234
234
  try {
235
+ // Pass the key as a header, not a URL query param, so it never lands in
236
+ // proxy / CDN / firewall access logs (privacy audit finding).
235
237
  const r = await fetchImpl(
236
- `https://generativelanguage.googleapis.com/v1beta/models?key=${encodeURIComponent(key)}`,
237
- { signal },
238
+ 'https://generativelanguage.googleapis.com/v1beta/models',
239
+ { signal, headers: { 'x-goog-api-key': key } },
238
240
  );
239
241
  if (!r.ok) return null;
240
242
  const json = await r.json();
package/src/server.js CHANGED
@@ -2117,7 +2117,7 @@ function handleMessage(msg) {
2117
2117
  try {
2118
2118
  const fs = await import('node:fs');
2119
2119
  const path = await import('node:path');
2120
- const home = process.env.IJFW_HOME || path.join(process.env.HOME || '', '.ijfw');
2120
+ const home = process.env.IJFW_HOME || path.join(process.env.HOME || homedir(), '.ijfw');
2121
2121
  const s = JSON.parse(fs.readFileSync(path.join(home, 'settings.json'), 'utf8'));
2122
2122
  injectOn = s && s.profile && s.profile.inject === 'on';
2123
2123
  } catch { injectOn = false; }
@@ -2424,9 +2424,16 @@ function handleMessage(msg) {
2424
2424
  case 'ijfw_metrics':
2425
2425
  result = handleMetrics(args || {});
2426
2426
  break;
2427
- case 'ijfw_cross_project_search':
2428
- result = handleCrossProjectSearch(args || {});
2427
+ case 'ijfw_cross_project_search': {
2428
+ // Privacy opt-out: a dedicated off-switch + the IJFW_MINIMAL master
2429
+ // switch disable cross-project memory surfacing entirely.
2430
+ const xpOff = /^(1|true|yes|on)$/i.test(process.env.IJFW_NO_CROSS_PROJECT || '')
2431
+ || process.env.IJFW_MINIMAL === '1';
2432
+ result = xpOff
2433
+ ? { text: 'Cross-project search is disabled (IJFW_NO_CROSS_PROJECT / IJFW_MINIMAL).' }
2434
+ : handleCrossProjectSearch(args || {});
2429
2435
  break;
2436
+ }
2430
2437
  case 'ijfw_prompt_check': {
2431
2438
  const pc = checkPrompt((args && args.prompt) || '');
2432
2439
  const text = pc.vague
@@ -2550,7 +2557,7 @@ function handleMessage(msg) {
2550
2557
  try {
2551
2558
  const fs = await import('node:fs');
2552
2559
  const path = await import('node:path');
2553
- const home = process.env.IJFW_HOME || path.join(process.env.HOME || '', '.ijfw');
2560
+ const home = process.env.IJFW_HOME || path.join(process.env.HOME || homedir(), '.ijfw');
2554
2561
  const s = JSON.parse(fs.readFileSync(path.join(home, 'settings.json'), 'utf8'));
2555
2562
  injectOn = s && s.profile && s.profile.inject === 'on';
2556
2563
  } catch { injectOn = false; }