s3db.js 13.6.1 → 14.0.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.
Files changed (189) hide show
  1. package/README.md +56 -15
  2. package/dist/s3db.cjs +72446 -39022
  3. package/dist/s3db.cjs.map +1 -1
  4. package/dist/s3db.es.js +72172 -38790
  5. package/dist/s3db.es.js.map +1 -1
  6. package/mcp/lib/base-handler.js +157 -0
  7. package/mcp/lib/handlers/connection-handler.js +280 -0
  8. package/mcp/lib/handlers/query-handler.js +533 -0
  9. package/mcp/lib/handlers/resource-handler.js +428 -0
  10. package/mcp/lib/tool-registry.js +336 -0
  11. package/mcp/lib/tools/connection-tools.js +161 -0
  12. package/mcp/lib/tools/query-tools.js +267 -0
  13. package/mcp/lib/tools/resource-tools.js +404 -0
  14. package/package.json +85 -50
  15. package/src/clients/memory-client.class.js +346 -191
  16. package/src/clients/memory-storage.class.js +300 -84
  17. package/src/clients/s3-client.class.js +7 -6
  18. package/src/concerns/geo-encoding.js +19 -2
  19. package/src/concerns/ip.js +59 -9
  20. package/src/concerns/money.js +8 -1
  21. package/src/concerns/password-hashing.js +49 -8
  22. package/src/concerns/plugin-storage.js +186 -18
  23. package/src/concerns/storage-drivers/filesystem-driver.js +284 -0
  24. package/src/database.class.js +139 -29
  25. package/src/errors.js +332 -42
  26. package/src/plugins/api/auth/oidc-auth.js +66 -17
  27. package/src/plugins/api/auth/strategies/base-strategy.class.js +74 -0
  28. package/src/plugins/api/auth/strategies/factory.class.js +63 -0
  29. package/src/plugins/api/auth/strategies/global-strategy.class.js +44 -0
  30. package/src/plugins/api/auth/strategies/path-based-strategy.class.js +83 -0
  31. package/src/plugins/api/auth/strategies/path-rules-strategy.class.js +118 -0
  32. package/src/plugins/api/concerns/failban-manager.js +106 -57
  33. package/src/plugins/api/concerns/route-context.js +601 -0
  34. package/src/plugins/api/index.js +168 -40
  35. package/src/plugins/api/routes/auth-routes.js +198 -30
  36. package/src/plugins/api/routes/resource-routes.js +19 -4
  37. package/src/plugins/api/server/health-manager.class.js +163 -0
  38. package/src/plugins/api/server/middleware-chain.class.js +310 -0
  39. package/src/plugins/api/server/router.class.js +472 -0
  40. package/src/plugins/api/server.js +280 -1303
  41. package/src/plugins/api/utils/custom-routes.js +17 -5
  42. package/src/plugins/api/utils/guards.js +76 -17
  43. package/src/plugins/api/utils/openapi-generator-cached.class.js +133 -0
  44. package/src/plugins/api/utils/openapi-generator.js +7 -6
  45. package/src/plugins/audit.plugin.js +30 -8
  46. package/src/plugins/backup.plugin.js +110 -14
  47. package/src/plugins/cache/cache.class.js +22 -5
  48. package/src/plugins/cache/filesystem-cache.class.js +116 -19
  49. package/src/plugins/cache/memory-cache.class.js +211 -57
  50. package/src/plugins/cache/multi-tier-cache.class.js +371 -0
  51. package/src/plugins/cache/partition-aware-filesystem-cache.class.js +168 -47
  52. package/src/plugins/cache/redis-cache.class.js +552 -0
  53. package/src/plugins/cache/s3-cache.class.js +17 -8
  54. package/src/plugins/cache.plugin.js +176 -61
  55. package/src/plugins/cloud-inventory/drivers/alibaba-driver.js +8 -1
  56. package/src/plugins/cloud-inventory/drivers/aws-driver.js +60 -29
  57. package/src/plugins/cloud-inventory/drivers/azure-driver.js +8 -1
  58. package/src/plugins/cloud-inventory/drivers/base-driver.js +16 -2
  59. package/src/plugins/cloud-inventory/drivers/cloudflare-driver.js +8 -1
  60. package/src/plugins/cloud-inventory/drivers/digitalocean-driver.js +8 -1
  61. package/src/plugins/cloud-inventory/drivers/hetzner-driver.js +8 -1
  62. package/src/plugins/cloud-inventory/drivers/linode-driver.js +8 -1
  63. package/src/plugins/cloud-inventory/drivers/mongodb-atlas-driver.js +8 -1
  64. package/src/plugins/cloud-inventory/drivers/vultr-driver.js +8 -1
  65. package/src/plugins/cloud-inventory/index.js +29 -8
  66. package/src/plugins/cloud-inventory/registry.js +64 -42
  67. package/src/plugins/cloud-inventory.plugin.js +240 -138
  68. package/src/plugins/concerns/plugin-dependencies.js +54 -0
  69. package/src/plugins/concerns/resource-names.js +100 -0
  70. package/src/plugins/consumers/index.js +10 -2
  71. package/src/plugins/consumers/sqs-consumer.js +12 -2
  72. package/src/plugins/cookie-farm-suite.plugin.js +278 -0
  73. package/src/plugins/cookie-farm.errors.js +73 -0
  74. package/src/plugins/cookie-farm.plugin.js +869 -0
  75. package/src/plugins/costs.plugin.js +7 -1
  76. package/src/plugins/eventual-consistency/analytics.js +94 -19
  77. package/src/plugins/eventual-consistency/config.js +15 -7
  78. package/src/plugins/eventual-consistency/consolidation.js +29 -11
  79. package/src/plugins/eventual-consistency/garbage-collection.js +3 -1
  80. package/src/plugins/eventual-consistency/helpers.js +39 -14
  81. package/src/plugins/eventual-consistency/install.js +21 -2
  82. package/src/plugins/eventual-consistency/utils.js +32 -10
  83. package/src/plugins/fulltext.plugin.js +38 -11
  84. package/src/plugins/geo.plugin.js +61 -9
  85. package/src/plugins/identity/concerns/config.js +61 -0
  86. package/src/plugins/identity/concerns/mfa-manager.js +15 -2
  87. package/src/plugins/identity/concerns/rate-limit.js +124 -0
  88. package/src/plugins/identity/concerns/resource-schemas.js +9 -1
  89. package/src/plugins/identity/concerns/token-generator.js +29 -4
  90. package/src/plugins/identity/drivers/auth-driver.interface.js +76 -0
  91. package/src/plugins/identity/drivers/client-credentials-driver.js +127 -0
  92. package/src/plugins/identity/drivers/index.js +18 -0
  93. package/src/plugins/identity/drivers/password-driver.js +122 -0
  94. package/src/plugins/identity/email-service.js +17 -2
  95. package/src/plugins/identity/index.js +413 -69
  96. package/src/plugins/identity/oauth2-server.js +413 -30
  97. package/src/plugins/identity/oidc-discovery.js +16 -8
  98. package/src/plugins/identity/rsa-keys.js +115 -35
  99. package/src/plugins/identity/server.js +166 -45
  100. package/src/plugins/identity/session-manager.js +53 -7
  101. package/src/plugins/identity/ui/pages/mfa-verification.js +17 -15
  102. package/src/plugins/identity/ui/routes.js +363 -255
  103. package/src/plugins/importer/index.js +153 -20
  104. package/src/plugins/index.js +9 -2
  105. package/src/plugins/kubernetes-inventory/index.js +6 -0
  106. package/src/plugins/kubernetes-inventory/k8s-driver.js +867 -0
  107. package/src/plugins/kubernetes-inventory/resource-types.js +274 -0
  108. package/src/plugins/kubernetes-inventory.plugin.js +980 -0
  109. package/src/plugins/metrics.plugin.js +64 -16
  110. package/src/plugins/ml/base-model.class.js +25 -15
  111. package/src/plugins/ml/regression-model.class.js +1 -1
  112. package/src/plugins/ml.errors.js +57 -25
  113. package/src/plugins/ml.plugin.js +28 -4
  114. package/src/plugins/namespace.js +210 -0
  115. package/src/plugins/plugin.class.js +180 -8
  116. package/src/plugins/puppeteer/console-monitor.js +729 -0
  117. package/src/plugins/puppeteer/cookie-manager.js +492 -0
  118. package/src/plugins/puppeteer/network-monitor.js +816 -0
  119. package/src/plugins/puppeteer/performance-manager.js +746 -0
  120. package/src/plugins/puppeteer/proxy-manager.js +478 -0
  121. package/src/plugins/puppeteer/stealth-manager.js +556 -0
  122. package/src/plugins/puppeteer.errors.js +81 -0
  123. package/src/plugins/puppeteer.plugin.js +1327 -0
  124. package/src/plugins/queue-consumer.plugin.js +69 -14
  125. package/src/plugins/recon/behaviors/uptime-behavior.js +691 -0
  126. package/src/plugins/recon/concerns/command-runner.js +148 -0
  127. package/src/plugins/recon/concerns/diff-detector.js +372 -0
  128. package/src/plugins/recon/concerns/fingerprint-builder.js +307 -0
  129. package/src/plugins/recon/concerns/process-manager.js +338 -0
  130. package/src/plugins/recon/concerns/report-generator.js +478 -0
  131. package/src/plugins/recon/concerns/security-analyzer.js +571 -0
  132. package/src/plugins/recon/concerns/target-normalizer.js +68 -0
  133. package/src/plugins/recon/config/defaults.js +321 -0
  134. package/src/plugins/recon/config/resources.js +370 -0
  135. package/src/plugins/recon/index.js +778 -0
  136. package/src/plugins/recon/managers/dependency-manager.js +174 -0
  137. package/src/plugins/recon/managers/scheduler-manager.js +179 -0
  138. package/src/plugins/recon/managers/storage-manager.js +745 -0
  139. package/src/plugins/recon/managers/target-manager.js +274 -0
  140. package/src/plugins/recon/stages/asn-stage.js +314 -0
  141. package/src/plugins/recon/stages/certificate-stage.js +84 -0
  142. package/src/plugins/recon/stages/dns-stage.js +107 -0
  143. package/src/plugins/recon/stages/dnsdumpster-stage.js +362 -0
  144. package/src/plugins/recon/stages/fingerprint-stage.js +71 -0
  145. package/src/plugins/recon/stages/google-dorks-stage.js +440 -0
  146. package/src/plugins/recon/stages/http-stage.js +89 -0
  147. package/src/plugins/recon/stages/latency-stage.js +148 -0
  148. package/src/plugins/recon/stages/massdns-stage.js +302 -0
  149. package/src/plugins/recon/stages/osint-stage.js +1373 -0
  150. package/src/plugins/recon/stages/ports-stage.js +169 -0
  151. package/src/plugins/recon/stages/screenshot-stage.js +94 -0
  152. package/src/plugins/recon/stages/secrets-stage.js +514 -0
  153. package/src/plugins/recon/stages/subdomains-stage.js +295 -0
  154. package/src/plugins/recon/stages/tls-audit-stage.js +78 -0
  155. package/src/plugins/recon/stages/vulnerability-stage.js +78 -0
  156. package/src/plugins/recon/stages/web-discovery-stage.js +113 -0
  157. package/src/plugins/recon/stages/whois-stage.js +349 -0
  158. package/src/plugins/recon.plugin.js +75 -0
  159. package/src/plugins/recon.plugin.js.backup +2635 -0
  160. package/src/plugins/relation.errors.js +87 -14
  161. package/src/plugins/replicator.plugin.js +514 -137
  162. package/src/plugins/replicators/base-replicator.class.js +89 -1
  163. package/src/plugins/replicators/bigquery-replicator.class.js +66 -22
  164. package/src/plugins/replicators/dynamodb-replicator.class.js +22 -15
  165. package/src/plugins/replicators/mongodb-replicator.class.js +22 -15
  166. package/src/plugins/replicators/mysql-replicator.class.js +52 -17
  167. package/src/plugins/replicators/planetscale-replicator.class.js +30 -4
  168. package/src/plugins/replicators/postgres-replicator.class.js +62 -27
  169. package/src/plugins/replicators/s3db-replicator.class.js +25 -18
  170. package/src/plugins/replicators/schema-sync.helper.js +3 -3
  171. package/src/plugins/replicators/sqs-replicator.class.js +8 -2
  172. package/src/plugins/replicators/turso-replicator.class.js +23 -3
  173. package/src/plugins/replicators/webhook-replicator.class.js +42 -4
  174. package/src/plugins/s3-queue.plugin.js +464 -65
  175. package/src/plugins/scheduler.plugin.js +20 -6
  176. package/src/plugins/state-machine.plugin.js +40 -9
  177. package/src/plugins/tfstate/base-driver.js +28 -4
  178. package/src/plugins/tfstate/errors.js +65 -10
  179. package/src/plugins/tfstate/filesystem-driver.js +52 -8
  180. package/src/plugins/tfstate/index.js +163 -90
  181. package/src/plugins/tfstate/s3-driver.js +64 -6
  182. package/src/plugins/ttl.plugin.js +72 -17
  183. package/src/plugins/vector/distances.js +18 -12
  184. package/src/plugins/vector/kmeans.js +26 -4
  185. package/src/resource.class.js +115 -19
  186. package/src/testing/factory.class.js +20 -3
  187. package/src/testing/seeder.class.js +7 -1
  188. package/src/clients/memory-client.md +0 -917
  189. package/src/plugins/cloud-inventory/drivers/mock-drivers.js +0 -449
@@ -5,6 +5,7 @@ export class Cache extends EventEmitter {
5
5
  constructor(config = {}) {
6
6
  super();
7
7
  this.config = config;
8
+ this._fallbackStore = new Map();
8
9
  }
9
10
  // to implement:
10
11
  async _set (key, data) {}
@@ -27,22 +28,25 @@ export class Cache extends EventEmitter {
27
28
  // generic class methods
28
29
  async set(key, data) {
29
30
  this.validateKey(key);
31
+ this._fallbackStore.set(key, data);
30
32
  await this._set(key, data);
31
- this.emit("set", data);
33
+ this.emit("set", { key, value: data });
32
34
  return data
33
35
  }
34
36
 
35
37
  async get(key) {
36
38
  this.validateKey(key);
37
39
  const data = await this._get(key);
38
- this.emit("fetched", data);
39
- return data;
40
+ const value = data !== undefined ? data : this._fallbackStore.get(key);
41
+ this.emit("fetched", { key, value });
42
+ return value;
40
43
  }
41
44
 
42
45
  async del(key) {
43
46
  this.validateKey(key);
44
47
  const data = await this._del(key);
45
- this.emit("deleted", data);
48
+ this._fallbackStore.delete(key);
49
+ this.emit("deleted", { key, value: data });
46
50
  return data;
47
51
  }
48
52
 
@@ -52,9 +56,22 @@ export class Cache extends EventEmitter {
52
56
 
53
57
  async clear(prefix) {
54
58
  const data = await this._clear(prefix);
55
- this.emit("clear", data);
59
+ if (!prefix) {
60
+ this._fallbackStore.clear();
61
+ } else {
62
+ for (const key of this._fallbackStore.keys()) {
63
+ if (key.startsWith(prefix)) {
64
+ this._fallbackStore.delete(key);
65
+ }
66
+ }
67
+ }
68
+ this.emit("clear", { prefix, value: data });
56
69
  return data;
57
70
  }
71
+
72
+ stats() {
73
+ return typeof this.getStats === 'function' ? this.getStats() : {};
74
+ }
58
75
  }
59
76
 
60
77
  export default Cache
@@ -84,6 +84,7 @@ import path from 'path';
84
84
  import zlib from 'node:zlib';
85
85
  import { Cache } from './cache.class.js';
86
86
  import tryFn from '../../concerns/try-fn.js';
87
+ import { CacheError } from '../cache.errors.js';
87
88
 
88
89
  export class FilesystemCache extends Cache {
89
90
  constructor({
@@ -112,7 +113,13 @@ export class FilesystemCache extends Cache {
112
113
  super(config);
113
114
 
114
115
  if (!directory) {
115
- throw new Error('FilesystemCache: directory parameter is required');
116
+ throw new CacheError('FilesystemCache requires a directory', {
117
+ driver: 'filesystem',
118
+ operation: 'constructor',
119
+ statusCode: 400,
120
+ retriable: false,
121
+ suggestion: 'Pass { directory: "./cache" } or configure a valid cache directory before enabling FilesystemCache.'
122
+ });
116
123
  }
117
124
 
118
125
  this.directory = path.resolve(directory);
@@ -147,14 +154,34 @@ export class FilesystemCache extends Cache {
147
154
 
148
155
  this.locks = new Map(); // For file locking
149
156
  this.cleanupTimer = null;
150
-
151
- this._init();
157
+
158
+ // Store _init promise to allow tests to handle initialization errors
159
+ this._initPromise = this._init().catch(err => {
160
+ this._initError = err;
161
+ // Silently capture initialization error - will be thrown on first operation
162
+ });
152
163
  }
153
164
 
154
165
  async _init() {
155
166
  // Create cache directory if needed
156
167
  if (this.createDirectory) {
157
168
  await this._ensureDirectory(this.directory);
169
+ } else {
170
+ const [exists] = await tryFn(async () => {
171
+ const stats = await stat(this.directory);
172
+ return stats.isDirectory();
173
+ });
174
+
175
+ if (!exists) {
176
+ throw new CacheError(`Cache directory "${this.directory}" does not exist and createDirectory is disabled`, {
177
+ driver: 'filesystem',
178
+ operation: 'init',
179
+ statusCode: 500,
180
+ retriable: false,
181
+ suggestion: 'Create the cache directory manually or enable createDirectory in the FilesystemCache configuration.',
182
+ directory: this.directory
183
+ });
184
+ }
158
185
  }
159
186
 
160
187
  // Start cleanup timer if enabled
@@ -168,12 +195,39 @@ export class FilesystemCache extends Cache {
168
195
  }
169
196
 
170
197
  async _ensureDirectory(dir) {
198
+ if (!this.createDirectory) {
199
+ const [exists] = await tryFn(async () => {
200
+ const stats = await stat(dir);
201
+ return stats.isDirectory();
202
+ });
203
+
204
+ if (!exists) {
205
+ throw new CacheError(`Cache directory "${dir}" is missing (createDirectory disabled)`, {
206
+ driver: 'filesystem',
207
+ operation: 'ensureDirectory',
208
+ statusCode: 500,
209
+ retriable: false,
210
+ suggestion: 'Create the directory before writing cache entries or enable createDirectory.',
211
+ directory: dir
212
+ });
213
+ }
214
+ return;
215
+ }
216
+
171
217
  const [ok, err] = await tryFn(async () => {
172
218
  await mkdir(dir, { recursive: true });
173
219
  });
174
220
 
175
221
  if (!ok && err.code !== 'EEXIST') {
176
- throw new Error(`Failed to create cache directory: ${err.message}`);
222
+ throw new CacheError(`Failed to create cache directory: ${err.message}`, {
223
+ driver: 'filesystem',
224
+ operation: 'ensureDirectory',
225
+ statusCode: 500,
226
+ retriable: false,
227
+ suggestion: 'Check filesystem permissions and ensure the process can create directories.',
228
+ directory: dir,
229
+ original: err
230
+ });
177
231
  }
178
232
  }
179
233
 
@@ -190,43 +244,56 @@ export class FilesystemCache extends Cache {
190
244
 
191
245
  async _set(key, data) {
192
246
  const filePath = this._getFilePath(key);
193
-
247
+
194
248
  try {
195
249
  // Prepare data
196
250
  let serialized = JSON.stringify(data);
197
251
  const originalSize = Buffer.byteLength(serialized, this.encoding);
198
-
252
+
199
253
  // Check size limit
200
254
  if (originalSize > this.maxFileSize) {
201
- throw new Error(`Cache data exceeds maximum file size: ${originalSize} > ${this.maxFileSize}`);
255
+ throw new CacheError('Cache data exceeds maximum file size', {
256
+ driver: 'filesystem',
257
+ operation: 'set',
258
+ statusCode: 413,
259
+ retriable: false,
260
+ suggestion: 'Increase maxFileSize or reduce the cached payload size.',
261
+ key,
262
+ size: originalSize,
263
+ maxFileSize: this.maxFileSize
264
+ });
202
265
  }
203
-
266
+
204
267
  let compressed = false;
205
268
  let finalData = serialized;
206
-
269
+
207
270
  // Compress if enabled and over threshold
208
271
  if (this.enableCompression && originalSize >= this.compressionThreshold) {
209
272
  const compressedBuffer = zlib.gzipSync(Buffer.from(serialized, this.encoding));
210
273
  finalData = compressedBuffer.toString('base64');
211
274
  compressed = true;
212
275
  }
213
-
276
+
277
+ // Ensure directory exists before writing
278
+ const dir = path.dirname(filePath);
279
+ await this._ensureDirectory(dir);
280
+
214
281
  // Create backup if enabled
215
282
  if (this.enableBackup && await this._fileExists(filePath)) {
216
283
  const backupPath = filePath + this.backupSuffix;
217
284
  await this._copyFile(filePath, backupPath);
218
285
  }
219
-
286
+
220
287
  // Acquire lock if enabled
221
288
  if (this.enableLocking) {
222
289
  await this._acquireLock(filePath);
223
290
  }
224
-
291
+
225
292
  try {
226
293
  // Write data
227
- await writeFile(filePath, finalData, {
294
+ await writeFile(filePath, finalData, {
228
295
  encoding: compressed ? 'utf8' : this.encoding,
229
- mode: this.fileMode
296
+ mode: this.fileMode
230
297
  });
231
298
 
232
299
  // Write metadata if enabled
@@ -270,7 +337,15 @@ export class FilesystemCache extends Cache {
270
337
  if (this.enableStats) {
271
338
  this.stats.errors++;
272
339
  }
273
- throw new Error(`Failed to set cache key '${key}': ${error.message}`);
340
+ throw new CacheError(`Failed to set cache key '${key}': ${error.message}`, {
341
+ driver: 'filesystem',
342
+ operation: 'set',
343
+ statusCode: 500,
344
+ retriable: false,
345
+ suggestion: 'Verify filesystem permissions and available disk space.',
346
+ key,
347
+ original: error
348
+ });
274
349
  }
275
350
  }
276
351
 
@@ -422,7 +497,15 @@ export class FilesystemCache extends Cache {
422
497
  if (this.enableStats) {
423
498
  this.stats.errors++;
424
499
  }
425
- throw new Error(`Failed to delete cache key '${key}': ${error.message}`);
500
+ throw new CacheError(`Failed to delete cache key '${key}': ${error.message}`, {
501
+ driver: 'filesystem',
502
+ operation: 'delete',
503
+ statusCode: 500,
504
+ retriable: false,
505
+ suggestion: 'Ensure cache files are writable and not locked by another process.',
506
+ key,
507
+ original: error
508
+ });
426
509
  }
427
510
  }
428
511
 
@@ -522,7 +605,14 @@ export class FilesystemCache extends Cache {
522
605
  if (this.enableStats) {
523
606
  this.stats.errors++;
524
607
  }
525
- throw new Error(`Failed to clear cache: ${error.message}`);
608
+ throw new CacheError(`Failed to clear cache: ${error.message}`, {
609
+ driver: 'filesystem',
610
+ operation: 'clear',
611
+ statusCode: 500,
612
+ retriable: false,
613
+ suggestion: 'Verify the cache directory is accessible and not in use by another process.',
614
+ original: error
615
+ });
526
616
  }
527
617
  }
528
618
 
@@ -633,7 +723,14 @@ export class FilesystemCache extends Cache {
633
723
 
634
724
  while (this.locks.has(lockKey)) {
635
725
  if (Date.now() - startTime > this.lockTimeout) {
636
- throw new Error(`Lock timeout for file: ${filePath}`);
726
+ throw new CacheError(`Lock timeout for file: ${filePath}`, {
727
+ driver: 'filesystem',
728
+ operation: 'acquireLock',
729
+ statusCode: 408,
730
+ retriable: true,
731
+ suggestion: 'Increase lockTimeout or investigate long-running cache writes holding the lock.',
732
+ key: lockKey
733
+ });
637
734
  }
638
735
  await new Promise(resolve => setTimeout(resolve, 10));
639
736
  }
@@ -689,4 +786,4 @@ export class FilesystemCache extends Cache {
689
786
  }
690
787
  }
691
788
 
692
- export default FilesystemCache;
789
+ export default FilesystemCache;
@@ -137,10 +137,31 @@
137
137
  import zlib from 'node:zlib';
138
138
  import os from 'node:os';
139
139
  import { Cache } from "./cache.class.js"
140
+ import { CacheError } from '../cache.errors.js';
140
141
 
141
142
  export class MemoryCache extends Cache {
142
143
  constructor(config = {}) {
143
144
  super(config);
145
+ this.caseSensitive = config.caseSensitive !== undefined ? config.caseSensitive : true;
146
+ this.serializer = typeof config.serializer === 'function' ? config.serializer : JSON.stringify;
147
+
148
+ // Default deserializer with Date reconstruction
149
+ const defaultDeserializer = (str) => {
150
+ return JSON.parse(str, (key, value) => {
151
+ // Reconstruct Date objects from ISO strings
152
+ if (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/.test(value)) {
153
+ return new Date(value);
154
+ }
155
+ return value;
156
+ });
157
+ };
158
+
159
+ this.deserializer = typeof config.deserializer === 'function' ? config.deserializer : defaultDeserializer;
160
+ this.enableStats = config.enableStats === true;
161
+ this.evictionPolicy = (config.evictionPolicy || 'fifo').toLowerCase();
162
+ if (!['lru', 'fifo'].includes(this.evictionPolicy)) {
163
+ this.evictionPolicy = 'fifo';
164
+ }
144
165
  this.cache = {};
145
166
  this.meta = {};
146
167
  this.maxSize = config.maxSize !== undefined ? config.maxSize : 1000;
@@ -148,19 +169,26 @@ export class MemoryCache extends Cache {
148
169
  // Validate that only one memory limit option is used
149
170
  if (config.maxMemoryBytes && config.maxMemoryBytes > 0 &&
150
171
  config.maxMemoryPercent && config.maxMemoryPercent > 0) {
151
- throw new Error(
152
- '[MemoryCache] Cannot use both maxMemoryBytes and maxMemoryPercent. ' +
153
- 'Choose one: maxMemoryBytes (absolute) or maxMemoryPercent (0...1 fraction).'
154
- );
172
+ throw new CacheError('[MemoryCache] Cannot use both maxMemoryBytes and maxMemoryPercent', {
173
+ driver: 'memory',
174
+ operation: 'constructor',
175
+ statusCode: 400,
176
+ retriable: false,
177
+ suggestion: 'Choose either maxMemoryBytes or maxMemoryPercent to limit memory usage.'
178
+ });
155
179
  }
156
180
 
157
181
  // Calculate maxMemoryBytes from percentage if provided
158
182
  if (config.maxMemoryPercent && config.maxMemoryPercent > 0) {
159
183
  if (config.maxMemoryPercent > 1) {
160
- throw new Error(
161
- '[MemoryCache] maxMemoryPercent must be between 0 and 1 (e.g., 0.1 for 10%). ' +
162
- `Received: ${config.maxMemoryPercent}`
163
- );
184
+ throw new CacheError('[MemoryCache] maxMemoryPercent must be between 0 and 1', {
185
+ driver: 'memory',
186
+ operation: 'constructor',
187
+ statusCode: 400,
188
+ retriable: false,
189
+ suggestion: 'Provide a fraction between 0 and 1 (e.g., 0.1 for 10%).',
190
+ maxMemoryPercent: config.maxMemoryPercent
191
+ });
164
192
  }
165
193
 
166
194
  const totalMemory = os.totalmem();
@@ -188,17 +216,82 @@ export class MemoryCache extends Cache {
188
216
  // Memory tracking
189
217
  this.currentMemoryBytes = 0;
190
218
  this.evictedDueToMemory = 0;
219
+
220
+ // Monotonic counter for LRU ordering (prevents timestamp collisions)
221
+ this._accessCounter = 0;
222
+
223
+ this.stats = {
224
+ hits: 0,
225
+ misses: 0,
226
+ sets: 0,
227
+ deletes: 0,
228
+ evictions: 0
229
+ };
230
+ }
231
+
232
+ _normalizeKey(key) {
233
+ return this.caseSensitive ? key : key.toLowerCase();
234
+ }
235
+
236
+ _recordStat(type) {
237
+ if (!this.enableStats) return;
238
+ if (Object.prototype.hasOwnProperty.call(this.stats, type)) {
239
+ this.stats[type] += 1;
240
+ }
241
+ }
242
+
243
+ _selectEvictionCandidate() {
244
+ const entries = Object.entries(this.meta);
245
+ if (entries.length === 0) {
246
+ return null;
247
+ }
248
+
249
+ if (this.evictionPolicy === 'lru') {
250
+ // Use accessOrder (monotonic counter) for stable LRU ordering
251
+ entries.sort((a, b) => (a[1].accessOrder ?? a[1].insertOrder ?? 0) - (b[1].accessOrder ?? b[1].insertOrder ?? 0));
252
+ } else {
253
+ // Default to FIFO (order of insertion)
254
+ entries.sort((a, b) => (a[1].insertOrder ?? a[1].createdAt ?? a[1].ts) - (b[1].insertOrder ?? b[1].createdAt ?? b[1].ts));
255
+ }
256
+
257
+ return entries[0]?.[0] || null;
191
258
  }
192
259
 
193
260
  async _set(key, data) {
261
+ const normalizedKey = this._normalizeKey(key);
262
+
263
+ // Serialize first (needed for both compression and memory limit checks)
264
+ let serialized;
265
+ try {
266
+ serialized = this.serializer(data);
267
+ } catch (error) {
268
+ throw new CacheError(`Failed to serialize data for key '${key}'`, {
269
+ driver: 'memory',
270
+ operation: 'set',
271
+ statusCode: 500,
272
+ retriable: false,
273
+ suggestion: 'Ensure the custom serializer handles the provided data type.',
274
+ key,
275
+ original: error
276
+ });
277
+ }
278
+
194
279
  // Prepare data for storage
195
- let finalData = data;
280
+ let finalData = serialized;
196
281
  let compressed = false;
197
282
  let originalSize = 0;
198
283
  let compressedSize = 0;
199
284
 
200
- // Calculate size first (needed for both compression and memory limit checks)
201
- const serialized = JSON.stringify(data);
285
+ if (typeof serialized !== 'string') {
286
+ throw new CacheError('MemoryCache serializer must return a string', {
287
+ driver: 'memory',
288
+ operation: 'set',
289
+ statusCode: 500,
290
+ retriable: false,
291
+ suggestion: 'Update the custom serializer to return a string output.'
292
+ });
293
+ }
294
+
202
295
  originalSize = Buffer.byteLength(serialized, 'utf8');
203
296
 
204
297
  // Apply compression if enabled
@@ -232,105 +325,144 @@ export class MemoryCache extends Cache {
232
325
  const itemSize = compressed ? compressedSize : originalSize;
233
326
 
234
327
  // If replacing existing key, subtract its old size from current memory
235
- if (Object.prototype.hasOwnProperty.call(this.cache, key)) {
236
- const oldSize = this.meta[key]?.compressedSize || 0;
328
+ if (Object.prototype.hasOwnProperty.call(this.cache, normalizedKey)) {
329
+ const oldSize = this.meta[normalizedKey]?.compressedSize || 0;
237
330
  this.currentMemoryBytes -= oldSize;
238
331
  }
239
332
 
240
333
  // Memory-aware eviction: Remove items until we have space
241
334
  if (this.maxMemoryBytes > 0) {
335
+ // If item is too large to fit even in empty cache, don't cache it
336
+ if (itemSize > this.maxMemoryBytes) {
337
+ this.evictedDueToMemory++;
338
+ return data;
339
+ }
340
+
242
341
  while (this.currentMemoryBytes + itemSize > this.maxMemoryBytes && Object.keys(this.cache).length > 0) {
243
- // Remove the oldest item
244
- const oldestKey = Object.entries(this.meta)
245
- .sort((a, b) => a[1].ts - b[1].ts)[0]?.[0];
246
- if (oldestKey) {
247
- const evictedSize = this.meta[oldestKey]?.compressedSize || 0;
248
- delete this.cache[oldestKey];
249
- delete this.meta[oldestKey];
250
- this.currentMemoryBytes -= evictedSize;
251
- this.evictedDueToMemory++;
252
- } else {
253
- break; // No more items to evict
254
- }
342
+ const candidate = this._selectEvictionCandidate();
343
+ if (!candidate) break;
344
+ const evictedSize = this.meta[candidate]?.compressedSize || 0;
345
+ delete this.cache[candidate];
346
+ delete this.meta[candidate];
347
+ this.currentMemoryBytes -= evictedSize;
348
+ this.evictedDueToMemory++;
349
+ this._recordStat('evictions');
255
350
  }
256
351
  }
257
352
 
258
- // Item count eviction (original logic)
259
- if (this.maxSize > 0 && Object.keys(this.cache).length >= this.maxSize) {
260
- // Remove o item mais antigo
261
- const oldestKey = Object.entries(this.meta)
262
- .sort((a, b) => a[1].ts - b[1].ts)[0]?.[0];
263
- if (oldestKey) {
264
- const evictedSize = this.meta[oldestKey]?.compressedSize || 0;
265
- delete this.cache[oldestKey];
266
- delete this.meta[oldestKey];
353
+ // Item count eviction: only evict if we're about to exceed maxSize
354
+ // Check length before adding the new item (so maxSize=2 allows 2 items, not 1)
355
+ if (this.maxSize > 0 && !Object.prototype.hasOwnProperty.call(this.cache, normalizedKey) && Object.keys(this.cache).length >= this.maxSize) {
356
+ const candidate = this._selectEvictionCandidate();
357
+ if (candidate) {
358
+ const evictedSize = this.meta[candidate]?.compressedSize || 0;
359
+ delete this.cache[candidate];
360
+ delete this.meta[candidate];
267
361
  this.currentMemoryBytes -= evictedSize;
362
+ this._recordStat('evictions');
268
363
  }
269
364
  }
270
365
 
271
366
  // Store the item
272
- this.cache[key] = finalData;
273
- this.meta[key] = {
274
- ts: Date.now(),
367
+ this.cache[normalizedKey] = finalData;
368
+ const timestamp = Date.now();
369
+ const insertOrder = ++this._accessCounter;
370
+ this.meta[normalizedKey] = {
371
+ ts: timestamp,
372
+ createdAt: timestamp,
373
+ lastAccess: timestamp,
374
+ insertOrder,
375
+ accessOrder: insertOrder,
275
376
  compressed,
276
377
  originalSize,
277
- compressedSize: itemSize
378
+ compressedSize: itemSize,
379
+ originalKey: key
278
380
  };
279
381
 
280
382
  // Update current memory usage
281
383
  this.currentMemoryBytes += itemSize;
282
384
 
385
+ this._recordStat('sets');
386
+
283
387
  return data;
284
388
  }
285
389
 
286
390
  async _get(key) {
287
- if (!Object.prototype.hasOwnProperty.call(this.cache, key)) return null;
391
+ const normalizedKey = this._normalizeKey(key);
392
+
393
+ if (!Object.prototype.hasOwnProperty.call(this.cache, normalizedKey)) {
394
+ this._recordStat('misses');
395
+ return null;
396
+ }
288
397
 
289
398
  // Check TTL expiration
290
399
  if (this.ttl > 0) {
291
400
  const now = Date.now();
292
- const meta = this.meta[key];
293
- if (meta && now - meta.ts > this.ttl) {
401
+ const meta = this.meta[normalizedKey];
402
+ if (meta && now - (meta.createdAt ?? meta.ts) > this.ttl) {
294
403
  // Expired - decrement memory before deleting
295
404
  const itemSize = meta.compressedSize || 0;
296
405
  this.currentMemoryBytes -= itemSize;
297
- delete this.cache[key];
298
- delete this.meta[key];
406
+ delete this.cache[normalizedKey];
407
+ delete this.meta[normalizedKey];
408
+ this._recordStat('misses');
299
409
  return null;
300
410
  }
301
411
  }
302
412
 
303
- const rawData = this.cache[key];
304
-
413
+ const rawData = this.cache[normalizedKey];
414
+
305
415
  // Check if data is compressed
306
416
  if (rawData && typeof rawData === 'object' && rawData.__compressed) {
307
417
  try {
308
418
  // Decompress data
309
419
  const compressedBuffer = Buffer.from(rawData.__data, 'base64');
310
420
  const decompressed = zlib.gunzipSync(compressedBuffer).toString('utf8');
311
- return JSON.parse(decompressed);
421
+ const value = this.deserializer(decompressed);
422
+ this._recordStat('hits');
423
+ if (this.evictionPolicy === 'lru' && this.meta[normalizedKey]) {
424
+ this.meta[normalizedKey].lastAccess = Date.now();
425
+ this.meta[normalizedKey].accessOrder = ++this._accessCounter;
426
+ }
427
+ return value;
312
428
  } catch (error) {
313
429
  console.warn(`[MemoryCache] Decompression failed for key '${key}':`, error.message);
314
430
  // If decompression fails, remove corrupted entry
315
- delete this.cache[key];
316
- delete this.meta[key];
431
+ delete this.cache[normalizedKey];
432
+ delete this.meta[normalizedKey];
433
+ this._recordStat('misses');
317
434
  return null;
318
435
  }
319
436
  }
320
437
 
321
- // Return uncompressed data
322
- return rawData;
438
+ try {
439
+ const value = typeof rawData === 'string' ? this.deserializer(rawData) : rawData;
440
+ this._recordStat('hits');
441
+ if (this.evictionPolicy === 'lru' && this.meta[normalizedKey]) {
442
+ this.meta[normalizedKey].lastAccess = Date.now();
443
+ this.meta[normalizedKey].accessOrder = ++this._accessCounter;
444
+ }
445
+ return value;
446
+ } catch (error) {
447
+ console.warn(`[MemoryCache] Deserialization failed for key '${key}':`, error.message);
448
+ delete this.cache[normalizedKey];
449
+ delete this.meta[normalizedKey];
450
+ this._recordStat('misses');
451
+ return null;
452
+ }
323
453
  }
324
454
 
325
455
  async _del(key) {
326
- // Decrement memory usage
327
- if (Object.prototype.hasOwnProperty.call(this.cache, key)) {
328
- const itemSize = this.meta[key]?.compressedSize || 0;
456
+ const normalizedKey = this._normalizeKey(key);
457
+
458
+ if (Object.prototype.hasOwnProperty.call(this.cache, normalizedKey)) {
459
+ const itemSize = this.meta[normalizedKey]?.compressedSize || 0;
329
460
  this.currentMemoryBytes -= itemSize;
330
461
  }
331
462
 
332
- delete this.cache[key];
333
- delete this.meta[key];
463
+ delete this.cache[normalizedKey];
464
+ delete this.meta[normalizedKey];
465
+ this._recordStat('deletes');
334
466
  return true;
335
467
  }
336
468
 
@@ -339,12 +471,17 @@ export class MemoryCache extends Cache {
339
471
  this.cache = {};
340
472
  this.meta = {};
341
473
  this.currentMemoryBytes = 0; // Reset memory counter
474
+ this.evictedDueToMemory = 0;
475
+ if (this.enableStats) {
476
+ this.stats = { hits: 0, misses: 0, sets: 0, deletes: 0, evictions: 0 };
477
+ }
342
478
  return true;
343
479
  }
344
480
  // Remove only keys that start with the prefix
345
481
  const removed = [];
482
+ const normalizedPrefix = this._normalizeKey(prefix);
346
483
  for (const key of Object.keys(this.cache)) {
347
- if (key.startsWith(prefix)) {
484
+ if (key.startsWith(normalizedPrefix)) {
348
485
  removed.push(key);
349
486
  // Decrement memory usage
350
487
  const itemSize = this.meta[key]?.compressedSize || 0;
@@ -363,7 +500,24 @@ export class MemoryCache extends Cache {
363
500
  }
364
501
 
365
502
  async keys() {
366
- return Object.keys(this.cache);
503
+ return Object.keys(this.cache).map(key => this.meta[key]?.originalKey || key);
504
+ }
505
+
506
+ getStats() {
507
+ if (!this.enableStats) {
508
+ return { enabled: false };
509
+ }
510
+
511
+ const total = this.stats.hits + this.stats.misses;
512
+ const hitRate = total > 0 ? this.stats.hits / total : 0;
513
+
514
+ return {
515
+ ...this.stats,
516
+ memoryUsageBytes: this.currentMemoryBytes,
517
+ maxMemoryBytes: this.maxMemoryBytes,
518
+ evictedDueToMemory: this.evictedDueToMemory,
519
+ hitRate
520
+ };
367
521
  }
368
522
 
369
523
  /**