shieldcortex 3.4.12 → 3.4.14

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.
Files changed (33) hide show
  1. package/dashboard/.next/standalone/dashboard/.next/BUILD_ID +1 -1
  2. package/dashboard/.next/standalone/dashboard/.next/build-manifest.json +2 -2
  3. package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.html +2 -2
  4. package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.rsc +1 -1
  5. package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  6. package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  7. package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  8. package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  9. package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  10. package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.html +1 -1
  11. package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.rsc +1 -1
  12. package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  13. package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  14. package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  15. package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  16. package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  17. package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  18. package/dashboard/.next/standalone/dashboard/.next/server/app/index.html +1 -1
  19. package/dashboard/.next/standalone/dashboard/.next/server/app/index.rsc +1 -1
  20. package/dashboard/.next/standalone/dashboard/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  21. package/dashboard/.next/standalone/dashboard/.next/server/app/index.segments/_full.segment.rsc +1 -1
  22. package/dashboard/.next/standalone/dashboard/.next/server/app/index.segments/_head.segment.rsc +1 -1
  23. package/dashboard/.next/standalone/dashboard/.next/server/app/index.segments/_index.segment.rsc +1 -1
  24. package/dashboard/.next/standalone/dashboard/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  25. package/dashboard/.next/standalone/dashboard/.next/server/pages/404.html +1 -1
  26. package/dashboard/.next/standalone/dashboard/.next/server/pages/500.html +2 -2
  27. package/dist/api/iron-dome-route-guard.js +7 -1
  28. package/dist/database/init.d.ts +22 -0
  29. package/dist/database/init.js +242 -50
  30. package/package.json +1 -1
  31. /package/dashboard/.next/standalone/dashboard/.next/static/{IEheIaoI03DPDkMm1iq_v → uiyXVE418p8MtLu_mVA4X}/_buildManifest.js +0 -0
  32. /package/dashboard/.next/standalone/dashboard/.next/static/{IEheIaoI03DPDkMm1iq_v → uiyXVE418p8MtLu_mVA4X}/_clientMiddlewareManifest.json +0 -0
  33. /package/dashboard/.next/standalone/dashboard/.next/static/{IEheIaoI03DPDkMm1iq_v → uiyXVE418p8MtLu_mVA4X}/_ssgManifest.js +0 -0
@@ -2,8 +2,8 @@
2
2
  * Database initialization and connection management
3
3
  */
4
4
  import Database from 'better-sqlite3';
5
- import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync, unlinkSync, renameSync, copyFileSync } from 'fs';
6
- import { dirname, join } from 'path';
5
+ import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync, unlinkSync, renameSync, copyFileSync, readdirSync, openSync, closeSync, realpathSync } from 'fs';
6
+ import { basename, dirname, join } from 'path';
7
7
  import { homedir } from 'os';
8
8
  import { fileURLToPath } from 'url';
9
9
  import { execSync } from 'child_process';
@@ -13,6 +13,7 @@ const _currentDir = dirname(_currentFile);
13
13
  let db = null;
14
14
  let currentDbPath = null;
15
15
  let lockFilePath = null;
16
+ let lockFileFd = null;
16
17
  // Anti-bloat: Database size limits
17
18
  const MAX_DB_SIZE = 100 * 1024 * 1024; // 100MB hard limit
18
19
  const WARN_DB_SIZE = 50 * 1024 * 1024; // 50MB warning threshold
@@ -40,6 +41,177 @@ function getDefaultDbPath() {
40
41
  // Fall back to legacy path for existing users
41
42
  return legacyPath;
42
43
  }
44
+ function resolveRuntimeInfo() {
45
+ let entryPath = process.argv[1] ?? '';
46
+ try {
47
+ if (entryPath) {
48
+ entryPath = realpathSync(entryPath);
49
+ }
50
+ }
51
+ catch {
52
+ // Fall back to the raw argv path.
53
+ }
54
+ if (entryPath.includes('/.npm/_npx/')) {
55
+ return { kind: 'npx-cache', entryPath };
56
+ }
57
+ if (entryPath.includes('/node_modules/shieldcortex/')) {
58
+ return { kind: 'installed', entryPath };
59
+ }
60
+ if (entryPath.includes('/ShieldCortex/') || entryPath.includes('\\ShieldCortex\\')) {
61
+ return { kind: 'project-checkout', entryPath };
62
+ }
63
+ return { kind: 'unknown', entryPath };
64
+ }
65
+ function enforceSafeRuntimePath(expandedPath, explicitDbPath) {
66
+ if (explicitDbPath || process.env.SHIELDCORTEX_ALLOW_UNSAFE_RUNTIME === '1') {
67
+ return;
68
+ }
69
+ const defaultPath = expandPath(getDefaultDbPath());
70
+ if (expandedPath !== defaultPath) {
71
+ return;
72
+ }
73
+ const runtime = resolveRuntimeInfo();
74
+ if (runtime.kind === 'npx-cache' || runtime.kind === 'project-checkout') {
75
+ throw new Error(`[database] Refusing to open ${defaultPath} from ${runtime.kind === 'npx-cache' ? 'an npx cache' : 'a project checkout'}.\n` +
76
+ `Use the installed CLI (\`shieldcortex\`), pass \`--db\` to use a separate database, or set SHIELDCORTEX_ALLOW_UNSAFE_RUNTIME=1 if you really mean to do this.\n` +
77
+ `Runtime path: ${runtime.entryPath}`);
78
+ }
79
+ }
80
+ function inspectDatabaseFile(dbPath) {
81
+ let inspectionDb = null;
82
+ try {
83
+ inspectionDb = new Database(dbPath, {
84
+ readonly: true,
85
+ fileMustExist: true,
86
+ });
87
+ const integrity = runIntegrityCheck(inspectionDb);
88
+ let count = null;
89
+ try {
90
+ count = inspectionDb.prepare('SELECT COUNT(*) AS count FROM memories').get().count;
91
+ }
92
+ catch {
93
+ count = null;
94
+ }
95
+ return { integrity, count };
96
+ }
97
+ catch (error) {
98
+ return { integrity: `inspect threw: ${error}`, count: null };
99
+ }
100
+ finally {
101
+ try {
102
+ inspectionDb?.close();
103
+ }
104
+ catch {
105
+ // Best-effort close.
106
+ }
107
+ }
108
+ }
109
+ function listHealthyBackups(dbPath) {
110
+ const dir = dirname(dbPath);
111
+ const prefix = `${basename(dbPath)}.corrupt.`;
112
+ return readdirSync(dir)
113
+ .filter((name) => name.startsWith(prefix) && !name.endsWith('-wal') && !name.endsWith('-shm'))
114
+ .map((name) => join(dir, name))
115
+ .map((backupPath) => {
116
+ const inspection = inspectDatabaseFile(backupPath);
117
+ const stats = statSync(backupPath);
118
+ return {
119
+ path: backupPath,
120
+ integrity: inspection.integrity,
121
+ count: inspection.count,
122
+ mtimeMs: stats.mtimeMs,
123
+ };
124
+ })
125
+ .filter((candidate) => candidate.integrity === 'ok' && typeof candidate.count === 'number' && candidate.count > 0)
126
+ .sort((a, b) => b.mtimeMs - a.mtimeMs)
127
+ .map(({ path, count, mtimeMs }) => ({ path, count, mtimeMs }));
128
+ }
129
+ function stashLiveDatabase(dbPath, suffix) {
130
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
131
+ const stashPath = `${dbPath}.${suffix}.${timestamp}`;
132
+ if (existsSync(dbPath)) {
133
+ try {
134
+ renameSync(dbPath, stashPath);
135
+ }
136
+ catch {
137
+ try {
138
+ copyFileSync(dbPath, stashPath);
139
+ unlinkSync(dbPath);
140
+ }
141
+ catch {
142
+ // Last resort: leave the original in place.
143
+ }
144
+ }
145
+ }
146
+ for (const ext of ['-wal', '-shm']) {
147
+ const liveSidecar = dbPath + ext;
148
+ if (existsSync(liveSidecar)) {
149
+ try {
150
+ renameSync(liveSidecar, stashPath + ext);
151
+ }
152
+ catch {
153
+ try {
154
+ unlinkSync(liveSidecar);
155
+ }
156
+ catch {
157
+ // Best-effort cleanup.
158
+ }
159
+ }
160
+ }
161
+ }
162
+ }
163
+ function restoreBackupAsLive(dbPath, backupPath, reason) {
164
+ stashLiveDatabase(dbPath, reason);
165
+ copyFileSync(backupPath, dbPath);
166
+ }
167
+ function acquireStartupLock(dbPath) {
168
+ lockFilePath = `${dbPath}.lock`;
169
+ const runtime = resolveRuntimeInfo();
170
+ const payload = JSON.stringify({
171
+ pid: process.pid,
172
+ startedAt: new Date().toISOString(),
173
+ entryPath: runtime.entryPath,
174
+ }, null, 2);
175
+ const tryOpen = () => {
176
+ lockFileFd = openSync(lockFilePath, 'wx');
177
+ writeFileSync(lockFileFd, payload, 'utf-8');
178
+ };
179
+ try {
180
+ tryOpen();
181
+ return;
182
+ }
183
+ catch {
184
+ let activeProcessError = null;
185
+ try {
186
+ const existing = JSON.parse(readFileSync(lockFilePath, 'utf-8'));
187
+ if (typeof existing.pid === 'number') {
188
+ try {
189
+ process.kill(existing.pid, 0);
190
+ activeProcessError = new Error(`[database] Refusing startup because ShieldCortex PID ${existing.pid} is already using ${dbPath}` +
191
+ (existing.entryPath ? ` (${existing.entryPath})` : ''));
192
+ }
193
+ catch (error) {
194
+ if (error.code !== 'ESRCH') {
195
+ activeProcessError = error;
196
+ }
197
+ }
198
+ }
199
+ }
200
+ catch {
201
+ // Stale or unreadable lock file — remove and retry.
202
+ }
203
+ if (activeProcessError) {
204
+ throw activeProcessError;
205
+ }
206
+ try {
207
+ unlinkSync(lockFilePath);
208
+ }
209
+ catch {
210
+ // Best-effort stale lock cleanup.
211
+ }
212
+ tryOpen();
213
+ }
214
+ }
43
215
  /**
44
216
  * Back up a corrupt database file with a timestamped name.
45
217
  * Returns the backup path.
@@ -164,30 +336,16 @@ function attemptFtsRecovery(database) {
164
336
  }
165
337
  }
166
338
  function verifyOnDiskIntegrity(dbPath) {
167
- let verificationDb = null;
168
- try {
169
- verificationDb = new Database(dbPath, {
170
- readonly: true,
171
- fileMustExist: true,
172
- });
173
- return runIntegrityCheck(verificationDb);
174
- }
175
- catch (error) {
176
- return `fresh integrity check threw: ${error}`;
177
- }
178
- finally {
179
- try {
180
- verificationDb?.close();
181
- }
182
- catch {
183
- // Best-effort close for verification handles.
184
- }
185
- }
339
+ return inspectDatabaseFile(dbPath).integrity;
186
340
  }
187
341
  export const __databaseTestUtils = {
188
342
  isLikelyFtsIntegrityIssue,
189
343
  attemptFtsRecovery,
190
344
  verifyOnDiskIntegrity,
345
+ inspectDatabaseFile,
346
+ listHealthyBackups,
347
+ resolveRuntimeInfo,
348
+ enforceSafeRuntimePath,
191
349
  };
192
350
  /**
193
351
  * Initialize the database connection
@@ -195,10 +353,12 @@ export const __databaseTestUtils = {
195
353
  export function initDatabase(dbPath) {
196
354
  // Use auto-detected path if not specified
197
355
  const resolvedPath = dbPath || getDefaultDbPath();
356
+ const explicitDbPath = Boolean(dbPath);
198
357
  if (db) {
199
358
  return db;
200
359
  }
201
360
  const expandedPath = expandPath(resolvedPath);
361
+ enforceSafeRuntimePath(expandedPath, explicitDbPath);
202
362
  const dir = dirname(expandedPath);
203
363
  // Create directory if it doesn't exist
204
364
  if (!existsSync(dir)) {
@@ -206,6 +366,9 @@ export function initDatabase(dbPath) {
206
366
  }
207
367
  // Store path for size monitoring
208
368
  currentDbPath = expandedPath;
369
+ acquireStartupLock(expandedPath);
370
+ console.log(`[database] Startup runtime=${resolveRuntimeInfo().kind} db=${expandedPath} wal=${existsSync(expandedPath + '-wal')} shm=${existsSync(expandedPath + '-shm')}`);
371
+ const healthyBackups = listHealthyBackups(expandedPath);
209
372
  // Wrap the initial open in try/catch to handle corrupt files gracefully
210
373
  let database;
211
374
  try {
@@ -213,11 +376,19 @@ export function initDatabase(dbPath) {
213
376
  }
214
377
  catch (openError) {
215
378
  // Database file is corrupt or not a valid SQLite database
216
- console.error('❌ Database file is corrupt or not a valid SQLite database.');
217
- const backupPath = backupCorruptDatabase(expandedPath);
218
- console.error(` Backed up to ${backupPath}`);
219
- console.error(' Creating fresh database...');
220
- database = new Database(expandedPath);
379
+ console.error(`❌ Database open failed for ${expandedPath}: ${openError}`);
380
+ const latestHealthyBackup = healthyBackups[0];
381
+ if (latestHealthyBackup) {
382
+ console.error(`[database] Restoring latest healthy backup with ${latestHealthyBackup.count} memories: ${latestHealthyBackup.path}`);
383
+ restoreBackupAsLive(expandedPath, latestHealthyBackup.path, 'failed-open');
384
+ database = new Database(expandedPath);
385
+ }
386
+ else {
387
+ const backupPath = backupCorruptDatabase(expandedPath);
388
+ console.error(` Backed up to ${backupPath}`);
389
+ console.error(' Creating fresh database...');
390
+ database = new Database(expandedPath);
391
+ }
221
392
  }
222
393
  // Integrity check on existing databases (skip for newly created files)
223
394
  if (existsSync(expandedPath) && statSync(expandedPath).size > 0) {
@@ -244,18 +415,40 @@ export function initDatabase(dbPath) {
244
415
  database = recovered;
245
416
  }
246
417
  else {
247
- // Recovery failed — backup and create fresh
248
- if (existsSync(expandedPath)) {
249
- const backupPath = backupCorruptDatabase(expandedPath);
250
- console.error(`[database] Recovery failed. Backed up corrupt file to: ${backupPath}`);
418
+ const latestHealthyBackup = healthyBackups[0];
419
+ if (latestHealthyBackup) {
420
+ console.error(`[database] Recovery failed. Restoring latest healthy backup with ${latestHealthyBackup.count} memories: ${latestHealthyBackup.path}`);
421
+ restoreBackupAsLive(expandedPath, latestHealthyBackup.path, 'recovery-failed');
422
+ database = new Database(expandedPath);
423
+ }
424
+ else {
425
+ // Recovery failed — backup and create fresh
426
+ if (existsSync(expandedPath)) {
427
+ const backupPath = backupCorruptDatabase(expandedPath);
428
+ console.error(`[database] Recovery failed. Backed up corrupt file to: ${backupPath}`);
429
+ }
430
+ console.error('[database] Creating fresh database...');
431
+ database = new Database(expandedPath);
251
432
  }
252
- console.error('[database] Creating fresh database...');
253
- database = new Database(expandedPath);
254
433
  }
255
434
  }
256
435
  }
257
436
  }
258
437
  }
438
+ const currentInspection = inspectDatabaseFile(expandedPath);
439
+ const latestHealthyBackup = healthyBackups[0];
440
+ const liveStats = existsSync(expandedPath) ? statSync(expandedPath) : null;
441
+ if (currentInspection.integrity === 'ok'
442
+ && currentInspection.count === 0
443
+ && latestHealthyBackup
444
+ && latestHealthyBackup.count >= 100
445
+ && liveStats
446
+ && (liveStats.mtimeMs - latestHealthyBackup.mtimeMs) < (24 * 60 * 60 * 1000)) {
447
+ console.error(`[database] Empty live database detected alongside a recent healthy backup (${latestHealthyBackup.count} memories). Restoring ${latestHealthyBackup.path}`);
448
+ database.close();
449
+ restoreBackupAsLive(expandedPath, latestHealthyBackup.path, 'empty-live');
450
+ database = new Database(expandedPath);
451
+ }
259
452
  db = database;
260
453
  // Enable WAL mode for better concurrency
261
454
  db.pragma('journal_mode = WAL');
@@ -265,15 +458,6 @@ export function initDatabase(dbPath) {
265
458
  db.pragma('busy_timeout = 10000');
266
459
  // Auto-checkpoint every 100 pages (~400KB) to prevent WAL bloat
267
460
  db.pragma('wal_autocheckpoint = 100');
268
- // Create lock file to help detect concurrent instances
269
- lockFilePath = expandedPath + '.lock';
270
- const pid = process.pid;
271
- try {
272
- writeFileSync(lockFilePath, `${pid}\n${new Date().toISOString()}`);
273
- }
274
- catch {
275
- // Non-fatal - lock file is advisory
276
- }
277
461
  // Register cleanup handlers for graceful shutdown
278
462
  registerShutdownHandlers();
279
463
  // Run migrations FIRST for existing databases
@@ -633,16 +817,24 @@ export function closeDatabase() {
633
817
  db.close();
634
818
  db = null;
635
819
  currentDbPath = null;
636
- // Remove lock file
637
- if (lockFilePath && existsSync(lockFilePath)) {
638
- try {
639
- unlinkSync(lockFilePath);
640
- }
641
- catch {
642
- // Non-fatal
643
- }
644
- lockFilePath = null;
820
+ }
821
+ if (lockFileFd !== null) {
822
+ try {
823
+ closeSync(lockFileFd);
824
+ }
825
+ catch {
826
+ // Best-effort close.
827
+ }
828
+ lockFileFd = null;
829
+ }
830
+ if (lockFilePath && existsSync(lockFilePath)) {
831
+ try {
832
+ unlinkSync(lockFilePath);
833
+ }
834
+ catch {
835
+ // Non-fatal
645
836
  }
837
+ lockFilePath = null;
646
838
  }
647
839
  }
648
840
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shieldcortex",
3
- "version": "3.4.12",
3
+ "version": "3.4.14",
4
4
  "description": "Trustworthy memory and security for AI agents. Recall debugging, review queue, OpenClaw session capture, and memory poisoning defence for Claude Code, Codex, OpenClaw, LangChain, and MCP agents.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",