@jagilber-org/index-server 1.22.1 → 1.26.4

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 (190) hide show
  1. package/CHANGELOG.md +91 -2
  2. package/CODE_OF_CONDUCT.md +2 -0
  3. package/CONTRIBUTING.md +32 -2
  4. package/README.md +82 -19
  5. package/SECURITY.md +17 -5
  6. package/dist/config/dashboardConfig.d.ts +3 -0
  7. package/dist/config/dashboardConfig.js +3 -0
  8. package/dist/config/defaultValues.d.ts +1 -1
  9. package/dist/config/defaultValues.js +1 -1
  10. package/dist/config/featureConfig.d.ts +2 -0
  11. package/dist/config/featureConfig.js +6 -1
  12. package/dist/config/runtimeConfig.d.ts +1 -1
  13. package/dist/config/runtimeConfig.js +8 -9
  14. package/dist/dashboard/client/admin.html +170 -53
  15. package/dist/dashboard/client/css/admin.css +132 -0
  16. package/dist/dashboard/client/js/admin.auth.js +25 -11
  17. package/dist/dashboard/client/js/admin.config.js +1 -1
  18. package/dist/dashboard/client/js/admin.feedback.js +328 -0
  19. package/dist/dashboard/client/js/admin.graph.js +120 -18
  20. package/dist/dashboard/client/js/admin.instructions.js +27 -13
  21. package/dist/dashboard/client/js/admin.logs.js +1 -5
  22. package/dist/dashboard/client/js/admin.maintenance.js +53 -8
  23. package/dist/dashboard/client/js/admin.messaging.js +1 -4
  24. package/dist/dashboard/client/js/admin.overview.js +5 -1
  25. package/dist/dashboard/client/js/admin.sessions.js +1 -1
  26. package/dist/dashboard/client/js/admin.utils.js +43 -1
  27. package/dist/dashboard/client/js/mermaid.min.js +813 -537
  28. package/dist/dashboard/export/DataExporter.js +2 -1
  29. package/dist/dashboard/server/AdminPanel.d.ts +3 -0
  30. package/dist/dashboard/server/AdminPanel.js +132 -35
  31. package/dist/dashboard/server/ApiRoutes.js +40 -9
  32. package/dist/dashboard/server/DashboardServer.js +1 -1
  33. package/dist/dashboard/server/FileMetricsStorage.d.ts +19 -0
  34. package/dist/dashboard/server/FileMetricsStorage.js +52 -5
  35. package/dist/dashboard/server/HttpTransport.js +6 -0
  36. package/dist/dashboard/server/InstanceManager.js +7 -2
  37. package/dist/dashboard/server/KnowledgeStore.js +7 -2
  38. package/dist/dashboard/server/MetricsCollector.d.ts +16 -0
  39. package/dist/dashboard/server/MetricsCollector.js +113 -17
  40. package/dist/dashboard/server/legacyDashboardHtml.js +7 -2
  41. package/dist/dashboard/server/middleware/ensureLoadedMiddleware.d.ts +1 -1
  42. package/dist/dashboard/server/middleware/ensureLoadedMiddleware.js +8 -3
  43. package/dist/dashboard/server/routes/admin.feedback.routes.d.ts +15 -0
  44. package/dist/dashboard/server/routes/admin.feedback.routes.js +188 -0
  45. package/dist/dashboard/server/routes/admin.routes.js +35 -27
  46. package/dist/dashboard/server/routes/alerts.routes.js +4 -3
  47. package/dist/dashboard/server/routes/api.feedback.routes.js +2 -1
  48. package/dist/dashboard/server/routes/api.usage.routes.js +8 -7
  49. package/dist/dashboard/server/routes/embeddings.routes.d.ts +2 -1
  50. package/dist/dashboard/server/routes/embeddings.routes.js +18 -9
  51. package/dist/dashboard/server/routes/graph.routes.js +10 -13
  52. package/dist/dashboard/server/routes/index.d.ts +1 -0
  53. package/dist/dashboard/server/routes/index.js +74 -39
  54. package/dist/dashboard/server/routes/instances.routes.js +2 -1
  55. package/dist/dashboard/server/routes/instructions.routes.js +46 -27
  56. package/dist/dashboard/server/routes/knowledge.routes.js +4 -3
  57. package/dist/dashboard/server/routes/logs.routes.js +5 -4
  58. package/dist/dashboard/server/routes/messaging.routes.js +15 -14
  59. package/dist/dashboard/server/routes/metrics.routes.js +14 -13
  60. package/dist/dashboard/server/routes/scripts.routes.js +6 -3
  61. package/dist/dashboard/server/routes/status.routes.js +5 -4
  62. package/dist/dashboard/server/routes/synthetic.routes.js +3 -2
  63. package/dist/dashboard/server/routes/usage.routes.js +2 -1
  64. package/dist/dashboard/server/utils/escapeHtml.d.ts +1 -0
  65. package/dist/dashboard/server/utils/escapeHtml.js +11 -0
  66. package/dist/dashboard/server/utils/pathContainment.d.ts +1 -0
  67. package/dist/dashboard/server/utils/pathContainment.js +15 -0
  68. package/dist/dashboard/server/wsInit.js +2 -2
  69. package/dist/lib/mcpStdioLogging.d.ts +165 -0
  70. package/dist/lib/mcpStdioLogging.js +287 -0
  71. package/dist/schemas/index.d.ts +37 -2
  72. package/dist/schemas/index.js +27 -3
  73. package/dist/server/backgroundServicesStartup.d.ts +7 -1
  74. package/dist/server/backgroundServicesStartup.js +25 -8
  75. package/dist/server/certInit.d.ts +97 -0
  76. package/dist/server/certInit.js +359 -0
  77. package/dist/server/certInit.types.d.ts +92 -0
  78. package/dist/server/certInit.types.js +34 -0
  79. package/dist/server/handshake/fallbackFrames.d.ts +31 -0
  80. package/dist/server/handshake/fallbackFrames.js +38 -0
  81. package/dist/server/handshake/initializeDetector.d.ts +31 -0
  82. package/dist/server/handshake/initializeDetector.js +88 -0
  83. package/dist/server/handshake/protocol.d.ts +15 -0
  84. package/dist/server/handshake/protocol.js +37 -0
  85. package/dist/server/handshake/readyEmitter.d.ts +6 -0
  86. package/dist/server/handshake/readyEmitter.js +88 -0
  87. package/dist/server/handshake/safetyFallbacks.d.ts +1 -0
  88. package/dist/server/handshake/safetyFallbacks.js +134 -0
  89. package/dist/server/handshake/stdinSniffer.d.ts +1 -0
  90. package/dist/server/handshake/stdinSniffer.js +260 -0
  91. package/dist/server/handshake/tracing.d.ts +16 -0
  92. package/dist/server/handshake/tracing.js +95 -0
  93. package/dist/server/handshakeManager.d.ts +23 -23
  94. package/dist/server/handshakeManager.js +36 -466
  95. package/dist/server/index-server.d.ts +23 -0
  96. package/dist/server/index-server.js +194 -9
  97. package/dist/server/mcpReadOnlySurfaces.d.ts +44 -0
  98. package/dist/server/mcpReadOnlySurfaces.js +297 -0
  99. package/dist/server/sdkServer.js +69 -7
  100. package/dist/server/transport.d.ts +5 -6
  101. package/dist/server/transport.js +46 -64
  102. package/dist/server/transportFactory.d.ts +3 -9
  103. package/dist/server/transportFactory.js +18 -380
  104. package/dist/services/atomicFs.d.ts +3 -0
  105. package/dist/services/atomicFs.js +171 -13
  106. package/dist/services/auditLog.d.ts +17 -2
  107. package/dist/services/auditLog.js +75 -14
  108. package/dist/services/bootstrapGating.js +1 -1
  109. package/dist/services/categoryRules.d.ts +10 -0
  110. package/dist/services/categoryRules.js +17 -0
  111. package/dist/services/classificationService.js +7 -5
  112. package/dist/services/embeddingService.d.ts +27 -11
  113. package/dist/services/embeddingService.js +51 -14
  114. package/dist/services/feedbackStorage.d.ts +39 -0
  115. package/dist/services/feedbackStorage.js +88 -0
  116. package/dist/services/handlers/instructions.add.js +429 -317
  117. package/dist/services/handlers/instructions.groom.js +128 -31
  118. package/dist/services/handlers/instructions.import.js +56 -23
  119. package/dist/services/handlers/instructions.patch.js +43 -32
  120. package/dist/services/handlers/instructions.query.js +20 -29
  121. package/dist/services/handlers/instructions.shared.d.ts +54 -0
  122. package/dist/services/handlers/instructions.shared.js +126 -1
  123. package/dist/services/handlers.activation.js +83 -81
  124. package/dist/services/handlers.dashboardConfig.d.ts +2 -2
  125. package/dist/services/handlers.dashboardConfig.js +1 -2
  126. package/dist/services/handlers.diagnostics.js +75 -54
  127. package/dist/services/handlers.feedback.d.ts +4 -11
  128. package/dist/services/handlers.feedback.js +11 -333
  129. package/dist/services/handlers.gates.js +69 -37
  130. package/dist/services/handlers.graph.js +2 -2
  131. package/dist/services/handlers.help.js +2 -2
  132. package/dist/services/handlers.instructionSchema.js +4 -2
  133. package/dist/services/handlers.integrity.js +42 -22
  134. package/dist/services/handlers.messaging.js +1 -1
  135. package/dist/services/handlers.metrics.js +51 -6
  136. package/dist/services/handlers.prompt.js +10 -2
  137. package/dist/services/handlers.search.js +94 -44
  138. package/dist/services/handlers.trace.js +1 -1
  139. package/dist/services/handlers.usage.js +38 -7
  140. package/dist/services/indexContext.d.ts +21 -1
  141. package/dist/services/indexContext.js +263 -78
  142. package/dist/services/indexLoader.d.ts +1 -0
  143. package/dist/services/indexLoader.js +28 -8
  144. package/dist/services/instructionRecordValidation.d.ts +39 -0
  145. package/dist/services/instructionRecordValidation.js +388 -0
  146. package/dist/services/instructions.dispatcher.js +4 -4
  147. package/dist/services/loaderSchemaValidator.d.ts +15 -0
  148. package/dist/services/loaderSchemaValidator.js +69 -0
  149. package/dist/services/logger.js +11 -2
  150. package/dist/services/mcpLogBridge.d.ts +49 -0
  151. package/dist/services/mcpLogBridge.js +83 -0
  152. package/dist/services/ownershipService.js +18 -8
  153. package/dist/services/performanceBaseline.js +23 -22
  154. package/dist/services/promptReviewService.d.ts +3 -1
  155. package/dist/services/promptReviewService.js +41 -13
  156. package/dist/services/regexSafety.d.ts +6 -0
  157. package/dist/services/regexSafety.js +46 -0
  158. package/dist/services/seedBootstrap.js +1 -1
  159. package/dist/services/storage/factory.d.ts +14 -1
  160. package/dist/services/storage/factory.js +61 -1
  161. package/dist/services/storage/jsonEmbeddingStore.d.ts +15 -0
  162. package/dist/services/storage/jsonEmbeddingStore.js +83 -0
  163. package/dist/services/storage/jsonFileStore.d.ts +3 -1
  164. package/dist/services/storage/jsonFileStore.js +8 -6
  165. package/dist/services/storage/migrationEngine.d.ts +13 -0
  166. package/dist/services/storage/migrationEngine.js +31 -0
  167. package/dist/services/storage/sqliteEmbeddingStore.d.ts +30 -0
  168. package/dist/services/storage/sqliteEmbeddingStore.js +222 -0
  169. package/dist/services/storage/sqliteStore.d.ts +3 -1
  170. package/dist/services/storage/sqliteStore.js +2 -2
  171. package/dist/services/storage/types.d.ts +48 -1
  172. package/dist/services/toolRegistry.js +77 -67
  173. package/dist/services/toolRegistry.zod.js +89 -86
  174. package/dist/services/tracing.js +5 -4
  175. package/dist/utils/envUtils.d.ts +4 -0
  176. package/dist/utils/envUtils.js +7 -0
  177. package/dist/utils/memoryMonitor.js +11 -10
  178. package/package.json +12 -4
  179. package/schemas/instruction.schema.json +38 -1
  180. package/scripts/copy-dashboard-assets.mjs +1 -1
  181. package/scripts/dist/README.md +1 -1
  182. package/scripts/generate-certs.mjs +201 -0
  183. package/scripts/setup-wizard.mjs +781 -0
  184. package/server.json +20 -0
  185. package/dist/externalClientLib.d.ts +0 -1
  186. package/dist/externalClientLib.js +0 -2
  187. package/dist/portableClientWrapper.d.ts +0 -1
  188. package/dist/portableClientWrapper.js +0 -2
  189. package/dist/services/indexingService.d.ts +0 -1
  190. package/dist/services/indexingService.js +0 -2
@@ -14,6 +14,7 @@ exports.DataExporter = void 0;
14
14
  const jsonExporter_js_1 = require("./exporters/jsonExporter.js");
15
15
  const csvExporter_js_1 = require("./exporters/csvExporter.js");
16
16
  const xmlExporter_js_1 = require("./exporters/xmlExporter.js");
17
+ const logger_js_1 = require("../../services/logger.js");
17
18
  class DataExporter {
18
19
  exportConfigs = new Map();
19
20
  exportJobs = new Map();
@@ -613,7 +614,7 @@ class DataExporter {
613
614
  callback(job);
614
615
  }
615
616
  catch (error) {
616
- console.error('Error in export job callback:', error);
617
+ (0, logger_js_1.logError)('Error in export job callback:', error);
617
618
  }
618
619
  });
619
620
  }
@@ -101,6 +101,7 @@ export declare class AdminPanel {
101
101
  instructionCount: number;
102
102
  schemaVersion?: string;
103
103
  sizeBytes: number;
104
+ warnings?: string[];
104
105
  }[];
105
106
  restoreBackup(backupId: string): {
106
107
  success: boolean;
@@ -118,6 +119,7 @@ export declare class AdminPanel {
118
119
  success: boolean;
119
120
  message: string;
120
121
  pruned?: number;
122
+ errors?: string[];
121
123
  };
122
124
  /** Export a backup — returns the zip file path for streaming, or falls back to JSON bundle for legacy dirs */
123
125
  exportBackup(backupId: string): {
@@ -128,6 +130,7 @@ export declare class AdminPanel {
128
130
  manifest: Record<string, unknown>;
129
131
  files: Record<string, unknown>;
130
132
  };
133
+ warnings?: string[];
131
134
  };
132
135
  /** Import a backup from a JSON bundle uploaded by the client — creates a zip */
133
136
  importBackup(bundle: {
@@ -24,6 +24,7 @@ const indexContext_1 = require("../../services/indexContext");
24
24
  const AdminPanelConfig_1 = require("./AdminPanelConfig");
25
25
  const AdminPanelState_1 = require("./AdminPanelState");
26
26
  const backupZip_1 = require("../../services/backupZip");
27
+ const auditLog_1 = require("../../services/auditLog");
27
28
  const adm_zip_1 = __importDefault(require("adm-zip"));
28
29
  class AdminPanel {
29
30
  panelConfig;
@@ -154,9 +155,11 @@ class AdminPanel {
154
155
  return { success: true, message: 'System backup completed successfully', backupId, files: fileCount };
155
156
  }
156
157
  catch (error) {
158
+ const errMsg = error instanceof Error ? error.message : String(error);
159
+ (0, auditLog_1.logAudit)('admin/backup/perform_failed', undefined, { error: errMsg }, 'mutation');
157
160
  return {
158
161
  success: false,
159
- message: `Backup failed: ${error instanceof Error ? error.message : String(error)}`
162
+ message: `Backup failed: ${errMsg}`
160
163
  };
161
164
  }
162
165
  }
@@ -192,6 +195,7 @@ class AdminPanel {
192
195
  let createdAt = new Date(stat.mtime).toISOString();
193
196
  let instructionCount = 0;
194
197
  let schemaVersion;
198
+ const entryWarnings = [];
195
199
  if (fs_1.default.existsSync(manifestPath)) {
196
200
  try {
197
201
  const mf = JSON.parse(fs_1.default.readFileSync(manifestPath, 'utf-8'));
@@ -199,7 +203,13 @@ class AdminPanel {
199
203
  instructionCount = mf.instructionCount || 0;
200
204
  schemaVersion = mf.schemaVersion;
201
205
  }
202
- catch { /* ignore */ }
206
+ catch (err) {
207
+ const errMsg = err instanceof Error ? err.message : String(err);
208
+ const msg = `Failed to parse manifest for backup '${entry}': ${errMsg}`;
209
+ process.stderr.write(`[admin] ${msg}\n`);
210
+ entryWarnings.push(msg);
211
+ (0, auditLog_1.logAudit)('admin/backup/list_warning', [entry], { error: errMsg, phase: 'manifest_parse' }, 'read');
212
+ }
203
213
  }
204
214
  else {
205
215
  instructionCount = fs_1.default.readdirSync(full).filter(f => f.toLowerCase().endsWith('.json')).length;
@@ -212,10 +222,18 @@ class AdminPanel {
212
222
  return sum;
213
223
  }
214
224
  }, 0);
215
- results.push({ id: entry, createdAt, instructionCount, schemaVersion, sizeBytes });
225
+ const result = { id: entry, createdAt, instructionCount, schemaVersion, sizeBytes };
226
+ if (entryWarnings.length > 0)
227
+ result.warnings = entryWarnings;
228
+ results.push(result);
216
229
  }
217
230
  }
218
- catch { /* ignore individual entry errors */ }
231
+ catch (err) {
232
+ const errMsg = err instanceof Error ? err.message : String(err);
233
+ const msg = `Failed to read backup entry '${entry}': ${errMsg}`;
234
+ process.stderr.write(`[admin] ${msg}\n`);
235
+ (0, auditLog_1.logAudit)('admin/backup/list_warning', [entry], { error: errMsg, phase: 'read_entry' }, 'read');
236
+ }
219
237
  }
220
238
  results.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
221
239
  return results;
@@ -228,8 +246,11 @@ class AdminPanel {
228
246
  const backupDir = path_1.default.join(backupRoot, safeId);
229
247
  const isZip = fs_1.default.existsSync(zipPath);
230
248
  const isDir = fs_1.default.existsSync(backupDir) && fs_1.default.statSync(backupDir).isDirectory();
231
- if (!isZip && !isDir)
232
- return { success: false, message: `Backup not found: ${safeId}` };
249
+ if (!isZip && !isDir) {
250
+ const msg = `Backup not found: ${safeId}`;
251
+ (0, auditLog_1.logAudit)('admin/backup/restore_failed', [safeId], { error: msg }, 'mutation');
252
+ return { success: false, message: msg };
253
+ }
233
254
  const instructionsDir = this.instructionsRoot;
234
255
  if (!fs_1.default.existsSync(instructionsDir))
235
256
  fs_1.default.mkdirSync(instructionsDir, { recursive: true });
@@ -265,7 +286,9 @@ class AdminPanel {
265
286
  return { success: true, message: `Backup ${safeId} restored`, restored };
266
287
  }
267
288
  catch (error) {
268
- return { success: false, message: `Restore failed: ${error instanceof Error ? error.message : String(error)}` };
289
+ const errMsg = error instanceof Error ? error.message : String(error);
290
+ (0, auditLog_1.logAudit)('admin/backup/restore_failed', backupId ? [String(backupId)] : undefined, { error: errMsg }, 'mutation');
291
+ return { success: false, message: `Restore failed: ${errMsg}` };
269
292
  }
270
293
  }
271
294
  /** Delete a backup zip or directory (safety checks on name) */
@@ -278,10 +301,15 @@ class AdminPanel {
278
301
  const dirPath = path_1.default.join(backupRoot, safeId);
279
302
  const hasZip = fs_1.default.existsSync(zipPath);
280
303
  const hasDir = fs_1.default.existsSync(dirPath) && fs_1.default.statSync(dirPath).isDirectory();
281
- if (!hasZip && !hasDir)
282
- return { success: false, message: `Backup not found: ${safeId}` };
304
+ if (!hasZip && !hasDir) {
305
+ const msg = `Backup not found: ${safeId}`;
306
+ (0, auditLog_1.logAudit)('admin/backup/delete_failed', [safeId], { error: msg }, 'mutation');
307
+ return { success: false, message: msg };
308
+ }
283
309
  if (!/^backup_|^instructions-|^pre_restore_|^auto-backup-/.test(safeId)) {
284
- return { success: false, message: 'Refusing to delete unexpected backup name' };
310
+ const msg = 'Refusing to delete unexpected backup name';
311
+ (0, auditLog_1.logAudit)('admin/backup/delete_failed', [safeId], { error: msg }, 'mutation');
312
+ return { success: false, message: msg };
285
313
  }
286
314
  if (hasZip)
287
315
  fs_1.default.unlinkSync(zipPath);
@@ -291,17 +319,23 @@ class AdminPanel {
291
319
  return { success: true, message: `Backup ${safeId} deleted`, removed: true };
292
320
  }
293
321
  catch (error) {
294
- return { success: false, message: `Delete failed: ${error instanceof Error ? error.message : String(error)}` };
322
+ const errMsg = error instanceof Error ? error.message : String(error);
323
+ (0, auditLog_1.logAudit)('admin/backup/delete_failed', backupId ? [String(backupId)] : undefined, { error: errMsg }, 'mutation');
324
+ return { success: false, message: `Delete failed: ${errMsg}` };
295
325
  }
296
326
  }
297
327
  /** Prune backups keeping newest N (by createdAt / mtime). Returns count pruned. */
298
328
  pruneBackups(retain) {
299
329
  try {
300
- if (retain < 0)
301
- return { success: false, message: 'retain must be >= 0' };
330
+ if (retain < 0) {
331
+ const msg = 'retain must be >= 0';
332
+ (0, auditLog_1.logAudit)('admin/backup/prune_failed', undefined, { error: msg, retain }, 'mutation');
333
+ return { success: false, message: msg };
334
+ }
302
335
  const backupRoot = this.backupRoot;
303
336
  if (!fs_1.default.existsSync(backupRoot))
304
337
  return { success: true, message: 'No backups to prune', pruned: 0 };
338
+ const pruneErrors = [];
305
339
  const entries = fs_1.default.readdirSync(backupRoot)
306
340
  .map(name => {
307
341
  const full = path_1.default.join(backupRoot, name);
@@ -341,10 +375,21 @@ class AdminPanel {
341
375
  fs_1.default.rmSync(d.full, { recursive: true, force: true });
342
376
  prunedAll++;
343
377
  }
344
- catch { /* ignore */ }
378
+ catch (err) {
379
+ const errMsg = err instanceof Error ? err.message : String(err);
380
+ const msg = `Failed to delete backup '${d.id}': ${errMsg}`;
381
+ process.stderr.write(`[admin] ${msg}\n`);
382
+ pruneErrors.push(msg);
383
+ (0, auditLog_1.logAudit)('admin/backup/prune_warning', [d.id], { error: errMsg, phase: 'delete_all' }, 'mutation');
384
+ }
345
385
  }
346
386
  process.stderr.write(`[admin] Pruned all backups (${prunedAll})\n`);
347
- return { success: true, message: `Pruned ${prunedAll} backups`, pruned: prunedAll };
387
+ const result = { success: true, message: `Pruned ${prunedAll} backups`, pruned: prunedAll };
388
+ if (pruneErrors.length > 0) {
389
+ result.message += ` (${pruneErrors.length} error(s))`;
390
+ result.errors = pruneErrors;
391
+ }
392
+ return result;
348
393
  }
349
394
  const toDelete = entries.slice(retain);
350
395
  let pruned = 0;
@@ -356,13 +401,26 @@ class AdminPanel {
356
401
  fs_1.default.rmSync(d.full, { recursive: true, force: true });
357
402
  pruned++;
358
403
  }
359
- catch { /* ignore */ }
404
+ catch (err) {
405
+ const errMsg = err instanceof Error ? err.message : String(err);
406
+ const msg = `Failed to delete backup '${d.id}': ${errMsg}`;
407
+ process.stderr.write(`[admin] ${msg}\n`);
408
+ pruneErrors.push(msg);
409
+ (0, auditLog_1.logAudit)('admin/backup/prune_warning', [d.id], { error: errMsg, phase: 'delete_retain' }, 'mutation');
410
+ }
360
411
  }
361
412
  process.stderr.write(`[admin] Pruned ${pruned} backup(s); retained ${entries.length - pruned}\n`);
362
- return { success: true, message: `Pruned ${pruned} backups (retained ${entries.length - pruned})`, pruned };
413
+ const result = { success: true, message: `Pruned ${pruned} backups (retained ${entries.length - pruned})`, pruned };
414
+ if (pruneErrors.length > 0) {
415
+ result.message += ` (${pruneErrors.length} error(s))`;
416
+ result.errors = pruneErrors;
417
+ }
418
+ return result;
363
419
  }
364
420
  catch (error) {
365
- return { success: false, message: `Prune failed: ${error instanceof Error ? error.message : String(error)}` };
421
+ const errMsg = error instanceof Error ? error.message : String(error);
422
+ (0, auditLog_1.logAudit)('admin/backup/prune_failed', undefined, { error: errMsg, retain }, 'mutation');
423
+ return { success: false, message: `Prune failed: ${errMsg}` };
366
424
  }
367
425
  }
368
426
  /** Export a backup — returns the zip file path for streaming, or falls back to JSON bundle for legacy dirs */
@@ -375,15 +433,25 @@ class AdminPanel {
375
433
  }
376
434
  // Legacy directory fallback
377
435
  const backupDir = path_1.default.join(this.backupRoot, safeId);
378
- if (!fs_1.default.existsSync(backupDir) || !fs_1.default.statSync(backupDir).isDirectory())
379
- return { success: false, message: `Backup not found: ${safeId}` };
436
+ if (!fs_1.default.existsSync(backupDir) || !fs_1.default.statSync(backupDir).isDirectory()) {
437
+ const msg = `Backup not found: ${safeId}`;
438
+ (0, auditLog_1.logAudit)('admin/backup/export_failed', [safeId], { error: msg }, 'read');
439
+ return { success: false, message: msg };
440
+ }
380
441
  let manifest = {};
442
+ const exportWarnings = [];
381
443
  const manifestPath = path_1.default.join(backupDir, 'manifest.json');
382
444
  if (fs_1.default.existsSync(manifestPath)) {
383
445
  try {
384
446
  manifest = JSON.parse(fs_1.default.readFileSync(manifestPath, 'utf-8'));
385
447
  }
386
- catch { /* ignore */ }
448
+ catch (err) {
449
+ const errMsg = err instanceof Error ? err.message : String(err);
450
+ const msg = `Failed to parse manifest.json for backup '${safeId}': ${errMsg}`;
451
+ process.stderr.write(`[admin] ${msg}\n`);
452
+ exportWarnings.push(msg);
453
+ (0, auditLog_1.logAudit)('admin/backup/export_warning', [safeId], { error: errMsg, phase: 'manifest_parse' }, 'read');
454
+ }
387
455
  }
388
456
  const files = {};
389
457
  for (const f of fs_1.default.readdirSync(backupDir)) {
@@ -391,20 +459,35 @@ class AdminPanel {
391
459
  try {
392
460
  files[f] = JSON.parse(fs_1.default.readFileSync(path_1.default.join(backupDir, f), 'utf-8'));
393
461
  }
394
- catch { /* skip corrupt */ }
462
+ catch (err) {
463
+ const errMsg = err instanceof Error ? err.message : String(err);
464
+ const msg = `Skipped corrupt file '${f}' in backup '${safeId}': ${errMsg}`;
465
+ process.stderr.write(`[admin] ${msg}\n`);
466
+ exportWarnings.push(msg);
467
+ (0, auditLog_1.logAudit)('admin/backup/export_warning', [safeId], { error: errMsg, phase: 'file_parse', file: f }, 'read');
468
+ }
395
469
  }
396
470
  }
397
- return { success: true, message: 'Export ready', bundle: { manifest, files } };
471
+ const result = { success: true, message: 'Export ready', bundle: { manifest, files } };
472
+ if (exportWarnings.length > 0) {
473
+ result.warnings = exportWarnings;
474
+ result.message = `Export ready (${exportWarnings.length} warning(s))`;
475
+ }
476
+ return result;
398
477
  }
399
478
  catch (error) {
400
- return { success: false, message: `Export failed: ${error instanceof Error ? error.message : String(error)}` };
479
+ const errMsg = error instanceof Error ? error.message : String(error);
480
+ (0, auditLog_1.logAudit)('admin/backup/export_failed', backupId ? [String(backupId)] : undefined, { error: errMsg }, 'read');
481
+ return { success: false, message: `Export failed: ${errMsg}` };
401
482
  }
402
483
  }
403
484
  /** Import a backup from a JSON bundle uploaded by the client — creates a zip */
404
485
  importBackup(bundle) {
405
486
  try {
406
487
  if (!bundle || typeof bundle !== 'object' || !bundle.files || typeof bundle.files !== 'object') {
407
- return { success: false, message: 'Invalid bundle: must contain a "files" object' };
488
+ const msg = 'Invalid bundle: must contain a "files" object';
489
+ (0, auditLog_1.logAudit)('admin/backup/import_failed', undefined, { error: msg }, 'mutation');
490
+ return { success: false, message: msg };
408
491
  }
409
492
  const now = new Date();
410
493
  const baseTs = now.toISOString().replace(/[-:]/g, '').replace(/\..+/, '');
@@ -437,21 +520,27 @@ class AdminPanel {
437
520
  return { success: true, message: `Imported ${written} files as ${backupId}`, backupId, files: written };
438
521
  }
439
522
  catch (error) {
440
- return { success: false, message: `Import failed: ${error instanceof Error ? error.message : String(error)}` };
523
+ const errMsg = error instanceof Error ? error.message : String(error);
524
+ (0, auditLog_1.logAudit)('admin/backup/import_failed', undefined, { error: errMsg, mode: 'json' }, 'mutation');
525
+ return { success: false, message: `Import failed: ${errMsg}` };
441
526
  }
442
527
  }
443
528
  /** Import a zip backup uploaded by the client without rewriting its contents. */
444
529
  importZipBackup(zipBuffer, sourceName) {
445
530
  try {
446
531
  if (!Buffer.isBuffer(zipBuffer) || zipBuffer.length === 0) {
447
- return { success: false, message: 'Invalid zip backup: upload was empty' };
532
+ const msg = 'Invalid zip backup: upload was empty';
533
+ (0, auditLog_1.logAudit)('admin/backup/import_failed', undefined, { error: msg, mode: 'zip' }, 'mutation');
534
+ return { success: false, message: msg };
448
535
  }
449
536
  const zip = new adm_zip_1.default(zipBuffer);
450
537
  const instructionFiles = zip.getEntries()
451
538
  .map(entry => path_1.default.basename(entry.entryName))
452
539
  .filter(name => name.toLowerCase().endsWith('.json') && name === path_1.default.basename(name) && name !== 'manifest.json');
453
540
  if (!instructionFiles.length) {
454
- return { success: false, message: 'Invalid zip backup: contains no instruction files' };
541
+ const msg = 'Invalid zip backup: contains no instruction files';
542
+ (0, auditLog_1.logAudit)('admin/backup/import_failed', undefined, { error: msg, mode: 'zip' }, 'mutation');
543
+ return { success: false, message: msg };
455
544
  }
456
545
  const now = new Date();
457
546
  const baseTs = now.toISOString().replace(/[-:]/g, '').replace(/\..+/, '');
@@ -460,13 +549,15 @@ class AdminPanel {
460
549
  const zipPath = path_1.default.join(this.backupRoot, `${backupId}.zip`);
461
550
  if (!fs_1.default.existsSync(this.backupRoot))
462
551
  fs_1.default.mkdirSync(this.backupRoot, { recursive: true });
463
- fs_1.default.writeFileSync(zipPath, zipBuffer);
552
+ fs_1.default.writeFileSync(zipPath, zipBuffer); // lgtm[js/http-to-file-access] — zipPath is generated under controlled backupRoot; admin endpoint behind dashboardAdminAuth
464
553
  const safeSourceName = sourceName ? path_1.default.basename(sourceName) : undefined;
465
554
  process.stderr.write(`[admin] Imported zip backup from file: ${backupId}.zip (${instructionFiles.length} files${safeSourceName ? `, source=${safeSourceName}` : ''})\n`);
466
555
  return { success: true, message: `Imported ${instructionFiles.length} files as ${backupId}`, backupId, files: instructionFiles.length };
467
556
  }
468
557
  catch (error) {
469
- return { success: false, message: `Import failed: ${error instanceof Error ? error.message : String(error)}` };
558
+ const errMsg = error instanceof Error ? error.message : String(error);
559
+ (0, auditLog_1.logAudit)('admin/backup/import_failed', undefined, { error: errMsg, mode: 'zip' }, 'mutation');
560
+ return { success: false, message: `Import failed: ${errMsg}` };
470
561
  }
471
562
  }
472
563
  /**
@@ -582,8 +673,8 @@ class AdminPanel {
582
673
  skipped = Math.max(0, scanned - accepted);
583
674
  }
584
675
  }
585
- catch {
586
- /* ignore */
676
+ catch (err) {
677
+ process.stderr.write(`[admin] getAdminStats: failed to read index state: ${err instanceof Error ? err.message : String(err)}\n`);
587
678
  }
588
679
  // Count instructions from the store (raw count uses store, with disk fallback for transparency)
589
680
  const indexDir = this.instructionsRoot;
@@ -598,7 +689,9 @@ class AdminPanel {
598
689
  rawFileCount = fs_1.default.readdirSync(indexDir).filter(f => f.toLowerCase().endsWith('.json')).length;
599
690
  }
600
691
  }
601
- catch { /* ignore */ }
692
+ catch (err) {
693
+ process.stderr.write(`[admin] getAdminStats: failed to count instruction files on disk: ${err instanceof Error ? err.message : String(err)}\n`);
694
+ }
602
695
  }
603
696
  // Recompute schema version snapshot only when any of these counts change
604
697
  const cacheNeedsUpdate = !this.indexStatsCache || this.indexStatsCache.acceptedInstructions !== accepted || this.indexStatsCache.rawFileCount !== rawFileCount || this.indexStatsCache.skippedInstructions !== skipped;
@@ -623,11 +716,15 @@ class AdminPanel {
623
716
  if (typeof json.schemaVersion === 'string')
624
717
  schemaVersions.add(json.schemaVersion);
625
718
  }
626
- catch { /* ignore parse */ }
719
+ catch (err) {
720
+ process.stderr.write(`[admin] getAdminStats: failed to parse schema version from '${f}': ${err instanceof Error ? err.message : String(err)}\n`);
721
+ }
627
722
  }
628
723
  }
629
724
  }
630
- catch { /* ignore */ }
725
+ catch (err) {
726
+ process.stderr.write(`[admin] getAdminStats: failed to read instruction files for schema version: ${err instanceof Error ? err.message : String(err)}\n`);
727
+ }
631
728
  }
632
729
  const schemaVersion = schemaVersions.size === 0 ? 'unknown' : (schemaVersions.size === 1 ? Array.from(schemaVersions)[0] : `mixed(${Array.from(schemaVersions).join(',')})`);
633
730
  this.indexStatsCache = {
@@ -39,22 +39,21 @@ var __importStar = (this && this.__importStar) || (function () {
39
39
  return result;
40
40
  };
41
41
  })();
42
- var __importDefault = (this && this.__importDefault) || function (mod) {
43
- return (mod && mod.__esModule) ? mod : { "default": mod };
44
- };
45
42
  Object.defineProperty(exports, "__esModule", { value: true });
46
43
  exports.createApiRoutes = createApiRoutes;
47
44
  const express_1 = __importStar(require("express"));
48
- const express_rate_limit_1 = __importDefault(require("express-rate-limit"));
45
+ const express_rate_limit_1 = __importStar(require("express-rate-limit"));
49
46
  const MetricsCollector_js_1 = require("./MetricsCollector.js");
50
47
  const auditLog_1 = require("../../services/auditLog");
51
48
  const runtimeConfig_js_1 = require("../../config/runtimeConfig.js");
52
49
  const index_js_1 = require("./routes/index.js");
53
50
  const ensureLoadedMiddleware_js_1 = require("./middleware/ensureLoadedMiddleware.js");
51
+ const logger_js_1 = require("../../services/logger.js");
54
52
  function createApiRoutes(options = {}) {
55
53
  const router = (0, express_1.Router)();
56
54
  const metricsCollector = (0, MetricsCollector_js_1.getMetricsCollector)();
57
- const rateLimitOpts = options.rateLimit ?? { windowMs: 60_000, max: 100 };
55
+ const httpCfg = (0, runtimeConfig_js_1.getRuntimeConfig)().dashboard.http;
56
+ const rateLimitOpts = options.rateLimit ?? { windowMs: httpCfg.rateLimitWindowMs, max: httpCfg.rateLimitMax };
58
57
  // CORS middleware (if enabled)
59
58
  // Security: only allow loopback origins (localhost, 127.0.0.1, [::1]) to prevent
60
59
  // cross-origin attacks. No wildcard (*) origins; credentials are not exposed.
@@ -63,7 +62,7 @@ function createApiRoutes(options = {}) {
63
62
  const origin = req.headers.origin;
64
63
  // nosemgrep: javascript.express.security.cors-misconfiguration.cors-misconfiguration -- origin is validated against loopback-only regex; not user-controlled
65
64
  if (origin && /^https?:\/\/(localhost|127\.0\.0\.1|\[::1\])(:\d+)?$/.test(origin)) {
66
- res.header('Access-Control-Allow-Origin', origin);
65
+ res.header('Access-Control-Allow-Origin', origin); // lgtm[js/cors-misconfiguration] — origin validated against loopback-only regex above
67
66
  }
68
67
  res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
69
68
  res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
@@ -72,8 +71,9 @@ function createApiRoutes(options = {}) {
72
71
  }
73
72
  // JSON middleware
74
73
  router.use(express_1.default.json());
75
- const rateLimitEnabled = (0, runtimeConfig_js_1.getRuntimeConfig)().dashboard.http.rateLimitEnabled;
74
+ const rateLimitEnabled = httpCfg.rateLimitEnabled;
76
75
  if (rateLimitEnabled && rateLimitOpts.max > 0 && rateLimitOpts.windowMs > 0) {
76
+ // Global rate limit for all routes (reads + mutations)
77
77
  router.use((0, express_rate_limit_1.default)({
78
78
  windowMs: rateLimitOpts.windowMs,
79
79
  max: rateLimitOpts.max,
@@ -81,13 +81,43 @@ function createApiRoutes(options = {}) {
81
81
  legacyHeaders: true,
82
82
  validate: { ip: false },
83
83
  skip: (req) => req.method === 'OPTIONS',
84
- keyGenerator: (req) => req.ip || req.socket.remoteAddress || 'unknown',
84
+ keyGenerator: (req) => {
85
+ const clientIp = req.ip || req.socket.remoteAddress;
86
+ return clientIp ? (0, express_rate_limit_1.ipKeyGenerator)(clientIp) : 'unknown';
87
+ },
85
88
  handler: (_req, res) => {
86
89
  const retryAfter = Number(res.getHeader('Retry-After') || Math.ceil(rateLimitOpts.windowMs / 1000));
87
90
  res.status(429).json({
88
91
  error: 'Too Many Requests',
89
92
  message: `Rate limit exceeded. Try again in ${retryAfter} second(s).`,
90
93
  retryAfterSeconds: retryAfter,
94
+ tier: 'global',
95
+ timestamp: Date.now(),
96
+ });
97
+ },
98
+ }));
99
+ // Stricter rate limit for mutation endpoints (POST/PUT/PATCH/DELETE).
100
+ // Defaults to runtimeConfig httpCfg.rateLimitMutationMax (env-configurable),
101
+ // bounded by the global per-window cap so mutation-tier never exceeds global.
102
+ const mutationMax = Math.max(1, Math.min(httpCfg.rateLimitMutationMax, rateLimitOpts.max));
103
+ router.use((0, express_rate_limit_1.default)({
104
+ windowMs: rateLimitOpts.windowMs,
105
+ max: mutationMax,
106
+ standardHeaders: true,
107
+ legacyHeaders: false,
108
+ validate: { ip: false },
109
+ skip: (req) => req.method === 'OPTIONS' || req.method === 'GET' || req.method === 'HEAD',
110
+ keyGenerator: (req) => {
111
+ const clientIp = req.ip || req.socket.remoteAddress;
112
+ return `mutation:${clientIp ? (0, express_rate_limit_1.ipKeyGenerator)(clientIp) : 'unknown'}`;
113
+ },
114
+ handler: (_req, res) => {
115
+ const retryAfter = Number(res.getHeader('Retry-After') || Math.ceil(rateLimitOpts.windowMs / 1000));
116
+ res.status(429).json({
117
+ error: 'Too Many Requests',
118
+ message: `Mutation rate limit exceeded. Try again in ${retryAfter} second(s).`,
119
+ retryAfterSeconds: retryAfter,
120
+ tier: 'mutation',
91
121
  timestamp: Date.now(),
92
122
  });
93
123
  },
@@ -169,9 +199,10 @@ function createApiRoutes(options = {}) {
169
199
  router.use((0, index_js_1.createScriptsRoutes)());
170
200
  router.use((0, index_js_1.createMessagingRoutes)());
171
201
  router.use((0, index_js_1.createSqliteRoutes)());
202
+ router.use((0, index_js_1.createAdminFeedbackRoutes)());
172
203
  // Error handling middleware
173
204
  router.use((error, _req, res, _next) => {
174
- console.error('[API] Unhandled error:', error);
205
+ (0, logger_js_1.logError)('[API] Unhandled error:', error);
175
206
  const exposeDetails = (0, runtimeConfig_js_1.getRuntimeConfig)().dashboard.http.verboseLogging;
176
207
  res.status(500).json({
177
208
  error: 'Internal server error',
@@ -118,7 +118,7 @@ class DashboardServer {
118
118
  const origin = req.headers.origin;
119
119
  // nosemgrep: javascript.express.security.cors-misconfiguration.cors-misconfiguration -- origin is validated against loopback-only regex; not user-controlled
120
120
  if (origin && /^https?:\/\/(localhost|127\.0\.0\.1|\[::1\])(:\d+)?$/.test(origin)) {
121
- res.header('Access-Control-Allow-Origin', origin);
121
+ res.header('Access-Control-Allow-Origin', origin); // lgtm[js/cors-misconfiguration] — origin validated against loopback-only regex above
122
122
  }
123
123
  res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
124
124
  res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization');
@@ -7,6 +7,7 @@ export declare class FileMetricsStorage {
7
7
  private storageDir;
8
8
  private maxFiles;
9
9
  private retentionMinutes;
10
+ private cleanupHealth;
10
11
  constructor(options?: {
11
12
  storageDir?: string;
12
13
  maxFiles?: number;
@@ -32,15 +33,33 @@ export declare class FileMetricsStorage {
32
33
  totalSizeKB: number;
33
34
  oldestTimestamp?: number;
34
35
  newestTimestamp?: number;
36
+ cleanup: {
37
+ degraded: boolean;
38
+ deletionFailures: number;
39
+ lastDeletionError?: string;
40
+ lastDeletionFailureAt?: number;
41
+ lastDeletionRecoveredAt?: number;
42
+ };
35
43
  }>;
36
44
  /**
37
45
  * Clear all stored metrics
38
46
  */
39
47
  clearAll(): Promise<void>;
48
+ getCleanupHealth(): {
49
+ degraded: boolean;
50
+ deletionFailures: number;
51
+ lastDeletionError?: string;
52
+ lastDeletionFailureAt?: number;
53
+ lastDeletionRecoveredAt?: number;
54
+ };
40
55
  private ensureStorageDir;
41
56
  private getSnapshotFiles;
42
57
  private extractTimestampFromFilename;
43
58
  private cleanupOldFiles;
59
+ private formatCleanupError;
60
+ private recordDeletionFailure;
61
+ private recordDeletionRecovery;
62
+ private deleteSnapshotFile;
44
63
  }
45
64
  export declare function getFileMetricsStorage(options?: {
46
65
  storageDir?: string;
@@ -16,6 +16,13 @@ class FileMetricsStorage {
16
16
  storageDir;
17
17
  maxFiles;
18
18
  retentionMinutes;
19
+ cleanupHealth = {
20
+ degraded: false,
21
+ deletionFailures: 0,
22
+ lastDeletionError: undefined,
23
+ lastDeletionFailureAt: undefined,
24
+ lastDeletionRecoveredAt: undefined,
25
+ };
19
26
  constructor(options = {}) {
20
27
  this.storageDir = options.storageDir || path_1.default.join(process.cwd(), 'metrics');
21
28
  this.maxFiles = options.maxFiles || 720; // 12 hours at 1-minute intervals
@@ -96,7 +103,7 @@ class FileMetricsStorage {
96
103
  try {
97
104
  const files = await this.getSnapshotFiles();
98
105
  if (files.length === 0) {
99
- return { fileCount: 0, totalSizeKB: 0 };
106
+ return { fileCount: 0, totalSizeKB: 0, cleanup: this.getCleanupHealth() };
100
107
  }
101
108
  let totalSize = 0;
102
109
  for (const file of files) {
@@ -114,11 +121,12 @@ class FileMetricsStorage {
114
121
  totalSizeKB: Math.round(totalSize / 1024),
115
122
  oldestTimestamp: timestamps.length > 0 ? Math.min(...timestamps) : undefined,
116
123
  newestTimestamp: timestamps.length > 0 ? Math.max(...timestamps) : undefined,
124
+ cleanup: this.getCleanupHealth(),
117
125
  };
118
126
  }
119
127
  catch (error) {
120
128
  (0, logger_js_1.logError)('[FileMetricsStorage] Failed to get storage stats', error);
121
- return { fileCount: 0, totalSizeKB: 0 };
129
+ return { fileCount: 0, totalSizeKB: 0, cleanup: this.getCleanupHealth() };
122
130
  }
123
131
  }
124
132
  /**
@@ -127,12 +135,19 @@ class FileMetricsStorage {
127
135
  async clearAll() {
128
136
  try {
129
137
  const files = await this.getSnapshotFiles();
130
- await Promise.all(files.map(file => fs_1.default.promises.unlink(path_1.default.join(this.storageDir, file)).catch(() => { })));
138
+ const results = await Promise.all(files.map(file => this.deleteSnapshotFile(file)));
139
+ const failedCount = results.filter(result => !result).length;
140
+ if (failedCount > 0) {
141
+ (0, logger_js_1.logWarn)('[FileMetricsStorage] Failed to delete one or more metrics files during clear', { failedCount });
142
+ }
131
143
  }
132
144
  catch (error) {
133
145
  (0, logger_js_1.logError)('[FileMetricsStorage] Failed to clear metrics storage', error);
134
146
  }
135
147
  }
148
+ getCleanupHealth() {
149
+ return { ...this.cleanupHealth };
150
+ }
136
151
  async ensureStorageDir() {
137
152
  try {
138
153
  await fs_1.default.promises.mkdir(this.storageDir, { recursive: true });
@@ -174,14 +189,46 @@ class FileMetricsStorage {
174
189
  filesToDelete = [...new Set([...expiredFiles, ...oldestFiles])];
175
190
  }
176
191
  if (filesToDelete.length > 0) {
177
- await Promise.all(filesToDelete.map(file => fs_1.default.promises.unlink(path_1.default.join(this.storageDir, file)).catch(() => { })));
178
- (0, logger_js_1.logInfo)('[FileMetricsStorage] Cleaned up old metrics files', { count: filesToDelete.length });
192
+ const results = await Promise.all(filesToDelete.map(file => this.deleteSnapshotFile(file)));
193
+ const deletedCount = results.filter(Boolean).length;
194
+ const failedCount = filesToDelete.length - deletedCount;
195
+ (0, logger_js_1.logInfo)('[FileMetricsStorage] Cleaned up old metrics files', { count: deletedCount, failedCount });
179
196
  }
180
197
  }
181
198
  catch (error) {
182
199
  (0, logger_js_1.logError)('[FileMetricsStorage] Failed to cleanup old metrics files', error);
183
200
  }
184
201
  }
202
+ formatCleanupError(error) {
203
+ return error instanceof Error ? (error.stack ?? error.message) : String(error);
204
+ }
205
+ recordDeletionFailure(file, error) {
206
+ this.cleanupHealth.degraded = true;
207
+ this.cleanupHealth.deletionFailures += 1;
208
+ this.cleanupHealth.lastDeletionError = this.formatCleanupError(error);
209
+ this.cleanupHealth.lastDeletionFailureAt = Date.now();
210
+ this.cleanupHealth.lastDeletionRecoveredAt = undefined;
211
+ (0, logger_js_1.logWarn)(`[FileMetricsStorage] Failed to delete metrics file ${file}`, error);
212
+ }
213
+ recordDeletionRecovery(file) {
214
+ if (!this.cleanupHealth.degraded)
215
+ return;
216
+ this.cleanupHealth.degraded = false;
217
+ this.cleanupHealth.lastDeletionError = undefined;
218
+ this.cleanupHealth.lastDeletionRecoveredAt = Date.now();
219
+ (0, logger_js_1.logInfo)('[FileMetricsStorage] Metrics file cleanup recovered', { file, deletionFailures: this.cleanupHealth.deletionFailures });
220
+ }
221
+ async deleteSnapshotFile(file) {
222
+ try {
223
+ await fs_1.default.promises.unlink(path_1.default.join(this.storageDir, file));
224
+ this.recordDeletionRecovery(file);
225
+ return true;
226
+ }
227
+ catch (error) {
228
+ this.recordDeletionFailure(file, error);
229
+ return false;
230
+ }
231
+ }
185
232
  }
186
233
  exports.FileMetricsStorage = FileMetricsStorage;
187
234
  /**