s3db.js 13.6.0 → 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 (193) hide show
  1. package/README.md +139 -43
  2. package/dist/s3db.cjs +72425 -38970
  3. package/dist/s3db.cjs.map +1 -1
  4. package/dist/s3db.es.js +72177 -38764
  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 +94 -49
  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/opengraph-helper.js +116 -0
  34. package/src/plugins/api/concerns/route-context.js +601 -0
  35. package/src/plugins/api/concerns/state-machine.js +288 -0
  36. package/src/plugins/api/index.js +180 -41
  37. package/src/plugins/api/routes/auth-routes.js +198 -30
  38. package/src/plugins/api/routes/resource-routes.js +19 -4
  39. package/src/plugins/api/server/health-manager.class.js +163 -0
  40. package/src/plugins/api/server/middleware-chain.class.js +310 -0
  41. package/src/plugins/api/server/router.class.js +472 -0
  42. package/src/plugins/api/server.js +280 -1303
  43. package/src/plugins/api/utils/custom-routes.js +17 -5
  44. package/src/plugins/api/utils/guards.js +76 -17
  45. package/src/plugins/api/utils/openapi-generator-cached.class.js +133 -0
  46. package/src/plugins/api/utils/openapi-generator.js +7 -6
  47. package/src/plugins/api/utils/template-engine.js +77 -3
  48. package/src/plugins/audit.plugin.js +30 -8
  49. package/src/plugins/backup.plugin.js +110 -14
  50. package/src/plugins/cache/cache.class.js +22 -5
  51. package/src/plugins/cache/filesystem-cache.class.js +116 -19
  52. package/src/plugins/cache/memory-cache.class.js +211 -57
  53. package/src/plugins/cache/multi-tier-cache.class.js +371 -0
  54. package/src/plugins/cache/partition-aware-filesystem-cache.class.js +168 -47
  55. package/src/plugins/cache/redis-cache.class.js +552 -0
  56. package/src/plugins/cache/s3-cache.class.js +17 -8
  57. package/src/plugins/cache.plugin.js +176 -61
  58. package/src/plugins/cloud-inventory/drivers/alibaba-driver.js +8 -1
  59. package/src/plugins/cloud-inventory/drivers/aws-driver.js +60 -29
  60. package/src/plugins/cloud-inventory/drivers/azure-driver.js +8 -1
  61. package/src/plugins/cloud-inventory/drivers/base-driver.js +16 -2
  62. package/src/plugins/cloud-inventory/drivers/cloudflare-driver.js +8 -1
  63. package/src/plugins/cloud-inventory/drivers/digitalocean-driver.js +8 -1
  64. package/src/plugins/cloud-inventory/drivers/hetzner-driver.js +8 -1
  65. package/src/plugins/cloud-inventory/drivers/linode-driver.js +8 -1
  66. package/src/plugins/cloud-inventory/drivers/mongodb-atlas-driver.js +8 -1
  67. package/src/plugins/cloud-inventory/drivers/vultr-driver.js +8 -1
  68. package/src/plugins/cloud-inventory/index.js +29 -8
  69. package/src/plugins/cloud-inventory/registry.js +64 -42
  70. package/src/plugins/cloud-inventory.plugin.js +240 -138
  71. package/src/plugins/concerns/plugin-dependencies.js +54 -0
  72. package/src/plugins/concerns/resource-names.js +100 -0
  73. package/src/plugins/consumers/index.js +10 -2
  74. package/src/plugins/consumers/sqs-consumer.js +12 -2
  75. package/src/plugins/cookie-farm-suite.plugin.js +278 -0
  76. package/src/plugins/cookie-farm.errors.js +73 -0
  77. package/src/plugins/cookie-farm.plugin.js +869 -0
  78. package/src/plugins/costs.plugin.js +7 -1
  79. package/src/plugins/eventual-consistency/analytics.js +94 -19
  80. package/src/plugins/eventual-consistency/config.js +15 -7
  81. package/src/plugins/eventual-consistency/consolidation.js +29 -11
  82. package/src/plugins/eventual-consistency/garbage-collection.js +3 -1
  83. package/src/plugins/eventual-consistency/helpers.js +39 -14
  84. package/src/plugins/eventual-consistency/install.js +21 -2
  85. package/src/plugins/eventual-consistency/utils.js +32 -10
  86. package/src/plugins/fulltext.plugin.js +38 -11
  87. package/src/plugins/geo.plugin.js +61 -9
  88. package/src/plugins/identity/concerns/config.js +61 -0
  89. package/src/plugins/identity/concerns/mfa-manager.js +15 -2
  90. package/src/plugins/identity/concerns/rate-limit.js +124 -0
  91. package/src/plugins/identity/concerns/resource-schemas.js +9 -1
  92. package/src/plugins/identity/concerns/token-generator.js +29 -4
  93. package/src/plugins/identity/drivers/auth-driver.interface.js +76 -0
  94. package/src/plugins/identity/drivers/client-credentials-driver.js +127 -0
  95. package/src/plugins/identity/drivers/index.js +18 -0
  96. package/src/plugins/identity/drivers/password-driver.js +122 -0
  97. package/src/plugins/identity/email-service.js +17 -2
  98. package/src/plugins/identity/index.js +413 -69
  99. package/src/plugins/identity/oauth2-server.js +413 -30
  100. package/src/plugins/identity/oidc-discovery.js +16 -8
  101. package/src/plugins/identity/rsa-keys.js +115 -35
  102. package/src/plugins/identity/server.js +166 -45
  103. package/src/plugins/identity/session-manager.js +53 -7
  104. package/src/plugins/identity/ui/pages/mfa-verification.js +17 -15
  105. package/src/plugins/identity/ui/routes.js +363 -255
  106. package/src/plugins/importer/index.js +153 -20
  107. package/src/plugins/index.js +9 -2
  108. package/src/plugins/kubernetes-inventory/index.js +6 -0
  109. package/src/plugins/kubernetes-inventory/k8s-driver.js +867 -0
  110. package/src/plugins/kubernetes-inventory/resource-types.js +274 -0
  111. package/src/plugins/kubernetes-inventory.plugin.js +980 -0
  112. package/src/plugins/metrics.plugin.js +64 -16
  113. package/src/plugins/ml/base-model.class.js +25 -15
  114. package/src/plugins/ml/regression-model.class.js +1 -1
  115. package/src/plugins/ml.errors.js +57 -25
  116. package/src/plugins/ml.plugin.js +28 -4
  117. package/src/plugins/namespace.js +210 -0
  118. package/src/plugins/plugin.class.js +180 -8
  119. package/src/plugins/puppeteer/console-monitor.js +729 -0
  120. package/src/plugins/puppeteer/cookie-manager.js +492 -0
  121. package/src/plugins/puppeteer/network-monitor.js +816 -0
  122. package/src/plugins/puppeteer/performance-manager.js +746 -0
  123. package/src/plugins/puppeteer/proxy-manager.js +478 -0
  124. package/src/plugins/puppeteer/stealth-manager.js +556 -0
  125. package/src/plugins/puppeteer.errors.js +81 -0
  126. package/src/plugins/puppeteer.plugin.js +1327 -0
  127. package/src/plugins/queue-consumer.plugin.js +69 -14
  128. package/src/plugins/recon/behaviors/uptime-behavior.js +691 -0
  129. package/src/plugins/recon/concerns/command-runner.js +148 -0
  130. package/src/plugins/recon/concerns/diff-detector.js +372 -0
  131. package/src/plugins/recon/concerns/fingerprint-builder.js +307 -0
  132. package/src/plugins/recon/concerns/process-manager.js +338 -0
  133. package/src/plugins/recon/concerns/report-generator.js +478 -0
  134. package/src/plugins/recon/concerns/security-analyzer.js +571 -0
  135. package/src/plugins/recon/concerns/target-normalizer.js +68 -0
  136. package/src/plugins/recon/config/defaults.js +321 -0
  137. package/src/plugins/recon/config/resources.js +370 -0
  138. package/src/plugins/recon/index.js +778 -0
  139. package/src/plugins/recon/managers/dependency-manager.js +174 -0
  140. package/src/plugins/recon/managers/scheduler-manager.js +179 -0
  141. package/src/plugins/recon/managers/storage-manager.js +745 -0
  142. package/src/plugins/recon/managers/target-manager.js +274 -0
  143. package/src/plugins/recon/stages/asn-stage.js +314 -0
  144. package/src/plugins/recon/stages/certificate-stage.js +84 -0
  145. package/src/plugins/recon/stages/dns-stage.js +107 -0
  146. package/src/plugins/recon/stages/dnsdumpster-stage.js +362 -0
  147. package/src/plugins/recon/stages/fingerprint-stage.js +71 -0
  148. package/src/plugins/recon/stages/google-dorks-stage.js +440 -0
  149. package/src/plugins/recon/stages/http-stage.js +89 -0
  150. package/src/plugins/recon/stages/latency-stage.js +148 -0
  151. package/src/plugins/recon/stages/massdns-stage.js +302 -0
  152. package/src/plugins/recon/stages/osint-stage.js +1373 -0
  153. package/src/plugins/recon/stages/ports-stage.js +169 -0
  154. package/src/plugins/recon/stages/screenshot-stage.js +94 -0
  155. package/src/plugins/recon/stages/secrets-stage.js +514 -0
  156. package/src/plugins/recon/stages/subdomains-stage.js +295 -0
  157. package/src/plugins/recon/stages/tls-audit-stage.js +78 -0
  158. package/src/plugins/recon/stages/vulnerability-stage.js +78 -0
  159. package/src/plugins/recon/stages/web-discovery-stage.js +113 -0
  160. package/src/plugins/recon/stages/whois-stage.js +349 -0
  161. package/src/plugins/recon.plugin.js +75 -0
  162. package/src/plugins/recon.plugin.js.backup +2635 -0
  163. package/src/plugins/relation.errors.js +87 -14
  164. package/src/plugins/replicator.plugin.js +514 -137
  165. package/src/plugins/replicators/base-replicator.class.js +89 -1
  166. package/src/plugins/replicators/bigquery-replicator.class.js +66 -22
  167. package/src/plugins/replicators/dynamodb-replicator.class.js +22 -15
  168. package/src/plugins/replicators/mongodb-replicator.class.js +22 -15
  169. package/src/plugins/replicators/mysql-replicator.class.js +52 -17
  170. package/src/plugins/replicators/planetscale-replicator.class.js +30 -4
  171. package/src/plugins/replicators/postgres-replicator.class.js +62 -27
  172. package/src/plugins/replicators/s3db-replicator.class.js +25 -18
  173. package/src/plugins/replicators/schema-sync.helper.js +3 -3
  174. package/src/plugins/replicators/sqs-replicator.class.js +8 -2
  175. package/src/plugins/replicators/turso-replicator.class.js +23 -3
  176. package/src/plugins/replicators/webhook-replicator.class.js +42 -4
  177. package/src/plugins/s3-queue.plugin.js +464 -65
  178. package/src/plugins/scheduler.plugin.js +20 -6
  179. package/src/plugins/state-machine.plugin.js +40 -9
  180. package/src/plugins/tfstate/README.md +126 -126
  181. package/src/plugins/tfstate/base-driver.js +28 -4
  182. package/src/plugins/tfstate/errors.js +65 -10
  183. package/src/plugins/tfstate/filesystem-driver.js +52 -8
  184. package/src/plugins/tfstate/index.js +163 -90
  185. package/src/plugins/tfstate/s3-driver.js +64 -6
  186. package/src/plugins/ttl.plugin.js +72 -17
  187. package/src/plugins/vector/distances.js +18 -12
  188. package/src/plugins/vector/kmeans.js +26 -4
  189. package/src/resource.class.js +115 -19
  190. package/src/testing/factory.class.js +20 -3
  191. package/src/testing/seeder.class.js +7 -1
  192. package/src/clients/memory-client.md +0 -917
  193. package/src/plugins/cloud-inventory/drivers/mock-drivers.js +0 -449
@@ -0,0 +1,371 @@
1
+ /**
2
+ * Multi-Tier Cache
3
+ *
4
+ * Cascading cache implementation that chains multiple cache drivers (L1 → L2 → L3).
5
+ * Provides automatic promotion of hot data to faster layers and fallback on errors.
6
+ *
7
+ * @example
8
+ * // Memory → Redis → S3 cascade
9
+ * const cache = new MultiTierCache({
10
+ * drivers: [
11
+ * { driver: memoryInstance, name: 'L1-Memory' },
12
+ * { driver: redisInstance, name: 'L2-Redis' },
13
+ * { driver: s3Instance, name: 'L3-S3' }
14
+ * ],
15
+ * promoteOnHit: true,
16
+ * strategy: 'write-through'
17
+ * });
18
+ */
19
+ import { Cache } from "./cache.class.js";
20
+ import { CacheError } from "../cache.errors.js";
21
+
22
+ export class MultiTierCache extends Cache {
23
+ constructor({
24
+ drivers = [],
25
+ promoteOnHit = true,
26
+ strategy = 'write-through', // 'write-through' | 'lazy-promotion'
27
+ fallbackOnError = true,
28
+ verbose = false
29
+ }) {
30
+ super();
31
+
32
+ if (!Array.isArray(drivers) || drivers.length === 0) {
33
+ throw new CacheError('MultiTierCache requires at least one driver', {
34
+ operation: 'constructor',
35
+ driver: 'MultiTierCache',
36
+ provided: drivers,
37
+ suggestion: 'Pass drivers array with at least one cache driver instance'
38
+ });
39
+ }
40
+
41
+ this.drivers = drivers.map((d, index) => ({
42
+ instance: d.driver,
43
+ name: d.name || `L${index + 1}`,
44
+ tier: index + 1
45
+ }));
46
+
47
+ this.config = {
48
+ promoteOnHit,
49
+ strategy,
50
+ fallbackOnError,
51
+ verbose
52
+ };
53
+
54
+ // Statistics per tier
55
+ this.stats = {
56
+ enabled: true,
57
+ tiers: this.drivers.map(d => ({
58
+ name: d.name,
59
+ hits: 0,
60
+ misses: 0,
61
+ promotions: 0,
62
+ errors: 0,
63
+ sets: 0
64
+ }))
65
+ };
66
+ }
67
+
68
+ /**
69
+ * Log message if verbose enabled
70
+ * @private
71
+ */
72
+ _log(...args) {
73
+ if (this.config.verbose) {
74
+ console.log('[MultiTierCache]', ...args);
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Get value from cache tiers (cascade L1 → L2 → L3)
80
+ * @private
81
+ */
82
+ async _get(key) {
83
+ for (let i = 0; i < this.drivers.length; i++) {
84
+ const tier = this.drivers[i];
85
+ const tierStats = this.stats.tiers[i];
86
+
87
+ try {
88
+ const value = await tier.instance.get(key);
89
+
90
+ if (value !== null && value !== undefined) {
91
+ // Cache hit!
92
+ tierStats.hits++;
93
+ this._log(`✓ Cache HIT on ${tier.name} for key: ${key}`);
94
+
95
+ // Promote to faster tiers if enabled and not already in L1
96
+ if (this.config.promoteOnHit && i > 0) {
97
+ this._promoteToFasterTiers(key, value, i);
98
+ }
99
+
100
+ return value;
101
+ } else {
102
+ // Cache miss on this tier, try next
103
+ tierStats.misses++;
104
+ this._log(`✗ Cache MISS on ${tier.name} for key: ${key}`);
105
+ }
106
+ } catch (error) {
107
+ tierStats.errors++;
108
+ this._log(`⚠ Error on ${tier.name} for key: ${key}`, error.message);
109
+
110
+ if (!this.config.fallbackOnError) {
111
+ throw new CacheError(`Cache get failed on ${tier.name}`, {
112
+ operation: 'get',
113
+ driver: 'MultiTierCache',
114
+ tier: tier.name,
115
+ key,
116
+ cause: error,
117
+ suggestion: 'Enable fallbackOnError to skip failed tiers'
118
+ });
119
+ }
120
+
121
+ // Continue to next tier on error (fallback)
122
+ continue;
123
+ }
124
+ }
125
+
126
+ // Miss on all tiers
127
+ this._log(`✗ Cache MISS on ALL tiers for key: ${key}`);
128
+ return null;
129
+ }
130
+
131
+ /**
132
+ * Promote value to faster tiers (L2 hit → write to L1, L3 hit → write to L1+L2)
133
+ * @private
134
+ */
135
+ async _promoteToFasterTiers(key, value, hitTierIndex) {
136
+ // Write to all tiers faster than the one where we found the value
137
+ for (let i = 0; i < hitTierIndex; i++) {
138
+ const tier = this.drivers[i];
139
+ const tierStats = this.stats.tiers[i];
140
+
141
+ try {
142
+ await tier.instance.set(key, value);
143
+ tierStats.promotions++;
144
+ this._log(`↑ Promoted key "${key}" to ${tier.name}`);
145
+ } catch (error) {
146
+ tierStats.errors++;
147
+ this._log(`⚠ Failed to promote key "${key}" to ${tier.name}:`, error.message);
148
+ // Continue promoting to other tiers even if one fails
149
+ }
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Set value in cache tiers
155
+ * @private
156
+ */
157
+ async _set(key, data) {
158
+ if (this.config.strategy === 'write-through') {
159
+ // Write to ALL tiers immediately
160
+ return this._writeToAllTiers(key, data);
161
+ } else if (this.config.strategy === 'lazy-promotion') {
162
+ // Write only to L1, let promotions handle the rest
163
+ return this._writeToL1Only(key, data);
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Write-through strategy: write to all tiers immediately
169
+ * @private
170
+ */
171
+ async _writeToAllTiers(key, data) {
172
+ const results = await Promise.allSettled(
173
+ this.drivers.map(async (tier, index) => {
174
+ try {
175
+ await tier.instance.set(key, data);
176
+ this.stats.tiers[index].sets++;
177
+ this._log(`✓ Wrote key "${key}" to ${tier.name}`);
178
+ return { success: true, tier: tier.name };
179
+ } catch (error) {
180
+ this.stats.tiers[index].errors++;
181
+ this._log(`⚠ Failed to write key "${key}" to ${tier.name}:`, error.message);
182
+ return { success: false, tier: tier.name, error };
183
+ }
184
+ })
185
+ );
186
+
187
+ // Check if at least L1 succeeded
188
+ const l1Success = results[0]?.status === 'fulfilled' && results[0].value.success;
189
+
190
+ if (!l1Success && !this.config.fallbackOnError) {
191
+ throw new CacheError('Failed to write to L1 cache', {
192
+ operation: 'set',
193
+ driver: 'MultiTierCache',
194
+ key,
195
+ results,
196
+ suggestion: 'Enable fallbackOnError or check L1 cache health'
197
+ });
198
+ }
199
+
200
+ return true;
201
+ }
202
+
203
+ /**
204
+ * Lazy-promotion strategy: write only to L1
205
+ * @private
206
+ */
207
+ async _writeToL1Only(key, data) {
208
+ const tier = this.drivers[0];
209
+ const tierStats = this.stats.tiers[0];
210
+
211
+ try {
212
+ await tier.instance.set(key, data);
213
+ tierStats.sets++;
214
+ this._log(`✓ Wrote key "${key}" to ${tier.name} (lazy-promotion)`);
215
+ return true;
216
+ } catch (error) {
217
+ tierStats.errors++;
218
+ throw new CacheError(`Failed to write to ${tier.name}`, {
219
+ operation: 'set',
220
+ driver: 'MultiTierCache',
221
+ tier: tier.name,
222
+ key,
223
+ cause: error,
224
+ suggestion: 'Check L1 cache health'
225
+ });
226
+ }
227
+ }
228
+
229
+ /**
230
+ * Delete key from all tiers
231
+ * @private
232
+ */
233
+ async _del(key) {
234
+ const results = await Promise.allSettled(
235
+ this.drivers.map(async (tier) => {
236
+ try {
237
+ await tier.instance.del(key);
238
+ this._log(`✓ Deleted key "${key}" from ${tier.name}`);
239
+ return { success: true, tier: tier.name };
240
+ } catch (error) {
241
+ this._log(`⚠ Failed to delete key "${key}" from ${tier.name}:`, error.message);
242
+ return { success: false, tier: tier.name, error };
243
+ }
244
+ })
245
+ );
246
+
247
+ // Consider successful if at least one tier succeeded
248
+ const anySuccess = results.some(r => r.status === 'fulfilled' && r.value.success);
249
+
250
+ if (!anySuccess && !this.config.fallbackOnError) {
251
+ throw new CacheError('Failed to delete from all cache tiers', {
252
+ operation: 'delete',
253
+ driver: 'MultiTierCache',
254
+ key,
255
+ results,
256
+ suggestion: 'Enable fallbackOnError or check cache health'
257
+ });
258
+ }
259
+
260
+ return true;
261
+ }
262
+
263
+ /**
264
+ * Clear all keys from all tiers
265
+ * @private
266
+ */
267
+ async _clear(prefix) {
268
+ const results = await Promise.allSettled(
269
+ this.drivers.map(async (tier) => {
270
+ try {
271
+ await tier.instance.clear(prefix);
272
+ this._log(`✓ Cleared ${prefix ? `prefix "${prefix}"` : 'all keys'} from ${tier.name}`);
273
+ return { success: true, tier: tier.name };
274
+ } catch (error) {
275
+ this._log(`⚠ Failed to clear ${tier.name}:`, error.message);
276
+ return { success: false, tier: tier.name, error };
277
+ }
278
+ })
279
+ );
280
+
281
+ return true;
282
+ }
283
+
284
+ /**
285
+ * Get total size across all tiers (may have duplicates)
286
+ */
287
+ async size() {
288
+ let totalSize = 0;
289
+
290
+ for (const tier of this.drivers) {
291
+ try {
292
+ if (typeof tier.instance.size === 'function') {
293
+ const size = await tier.instance.size();
294
+ totalSize += size;
295
+ }
296
+ } catch (error) {
297
+ this._log(`⚠ Failed to get size from ${tier.name}:`, error.message);
298
+ }
299
+ }
300
+
301
+ return totalSize;
302
+ }
303
+
304
+ /**
305
+ * Get all keys from all tiers (deduplicated)
306
+ */
307
+ async keys() {
308
+ const allKeys = new Set();
309
+
310
+ for (const tier of this.drivers) {
311
+ try {
312
+ if (typeof tier.instance.keys === 'function') {
313
+ const keys = await tier.instance.keys();
314
+ keys.forEach(k => allKeys.add(k));
315
+ }
316
+ } catch (error) {
317
+ this._log(`⚠ Failed to get keys from ${tier.name}:`, error.message);
318
+ }
319
+ }
320
+
321
+ return Array.from(allKeys);
322
+ }
323
+
324
+ /**
325
+ * Get comprehensive statistics
326
+ */
327
+ getStats() {
328
+ // Calculate totals
329
+ const totals = {
330
+ hits: 0,
331
+ misses: 0,
332
+ promotions: 0,
333
+ errors: 0,
334
+ sets: 0
335
+ };
336
+
337
+ for (const tierStats of this.stats.tiers) {
338
+ totals.hits += tierStats.hits;
339
+ totals.misses += tierStats.misses;
340
+ totals.promotions += tierStats.promotions;
341
+ totals.errors += tierStats.errors;
342
+ totals.sets += tierStats.sets;
343
+ }
344
+
345
+ const total = totals.hits + totals.misses;
346
+ const hitRate = total > 0 ? totals.hits / total : 0;
347
+
348
+ return {
349
+ enabled: true,
350
+ strategy: this.config.strategy,
351
+ promoteOnHit: this.config.promoteOnHit,
352
+ tiers: this.stats.tiers.map(t => {
353
+ const tierTotal = t.hits + t.misses;
354
+ const tierHitRate = tierTotal > 0 ? t.hits / tierTotal : 0;
355
+ return {
356
+ ...t,
357
+ hitRate: tierHitRate,
358
+ hitRatePercent: (tierHitRate * 100).toFixed(2) + '%'
359
+ };
360
+ }),
361
+ totals: {
362
+ ...totals,
363
+ total,
364
+ hitRate,
365
+ hitRatePercent: (hitRate * 100).toFixed(2) + '%'
366
+ }
367
+ };
368
+ }
369
+ }
370
+
371
+ export default MultiTierCache;
@@ -27,6 +27,7 @@ import fs from 'fs';
27
27
  import { mkdir, rm as rmdir, readdir, stat, writeFile, readFile } from 'fs/promises';
28
28
  import { FilesystemCache } from './filesystem-cache.class.js';
29
29
  import tryFn from '../../concerns/try-fn.js';
30
+ import { CacheError } from '../cache.errors.js';
30
31
 
31
32
  export class PartitionAwareFilesystemCache extends FilesystemCache {
32
33
  constructor({
@@ -56,55 +57,68 @@ export class PartitionAwareFilesystemCache extends FilesystemCache {
56
57
  * Generate partition-aware cache key
57
58
  */
58
59
  _getPartitionCacheKey(resource, action, partition, partitionValues = {}, params = {}) {
59
- const keyParts = [`resource=${resource}`, `action=${action}`];
60
+ const segments = [];
61
+
62
+ if (resource) {
63
+ segments.push(`resource=${this._sanitizePathValue(resource)}`);
64
+ }
65
+
66
+ const hasPartitionValues = partitionValues && Object.values(partitionValues).some(value => value !== null && value !== undefined && value !== '');
67
+
68
+ if (partition && hasPartitionValues) {
69
+ segments.push(`partition=${this._sanitizePathValue(partition)}`);
70
+
71
+ const sortedFields = Object.entries(partitionValues)
72
+ .filter(([, value]) => value !== null && value !== undefined)
73
+ .sort(([a], [b]) => a.localeCompare(b));
60
74
 
61
- if (partition && Object.keys(partitionValues).length > 0) {
62
- keyParts.push(`partition=${partition}`);
63
-
64
- // Sort fields for consistent keys
65
- const sortedFields = Object.entries(partitionValues).sort(([a], [b]) => a.localeCompare(b));
66
75
  for (const [field, value] of sortedFields) {
67
- if (value !== null && value !== undefined) {
68
- keyParts.push(`${field}=${value}`);
69
- }
76
+ segments.push(`${field}=${this._sanitizePathValue(value)}`);
70
77
  }
71
78
  }
72
79
 
73
- // Add params hash if exists
74
- if (Object.keys(params).length > 0) {
80
+ if (action) {
81
+ segments.push(`action=${this._sanitizePathValue(action)}`);
82
+ }
83
+
84
+ if (params && Object.keys(params).length > 0) {
75
85
  const paramsStr = Object.entries(params)
76
86
  .sort(([a], [b]) => a.localeCompare(b))
77
87
  .map(([k, v]) => `${k}=${v}`)
78
88
  .join('|');
79
- keyParts.push(`params=${Buffer.from(paramsStr).toString('base64')}`);
89
+ segments.push(`params=${this._sanitizePathValue(Buffer.from(paramsStr).toString('base64url'))}`);
80
90
  }
81
91
 
82
- return keyParts.join('/') + this.fileExtension;
92
+ return segments.join('/');
83
93
  }
84
94
 
85
95
  /**
86
96
  * Get directory path for partition cache
87
97
  */
88
98
  _getPartitionDirectory(resource, partition, partitionValues = {}) {
89
- const basePath = path.join(this.directory, `resource=${resource}`);
99
+ const baseSegments = [];
100
+ if (resource) {
101
+ baseSegments.push(`resource=${this._sanitizePathValue(resource)}`);
102
+ }
90
103
 
91
104
  if (!partition) {
92
- return basePath;
105
+ return path.join(this.directory, ...baseSegments);
93
106
  }
94
107
 
95
108
  if (this.partitionStrategy === 'flat') {
96
- // Flat structure: all partitions in same level
97
- return path.join(basePath, 'partitions');
109
+ return path.join(this.directory, ...baseSegments, 'partitions');
98
110
  }
99
111
 
100
112
  if (this.partitionStrategy === 'temporal' && this._isTemporalPartition(partition, partitionValues)) {
101
- // Temporal structure: organize by time hierarchy
102
- return this._getTemporalDirectory(basePath, partition, partitionValues);
113
+ return this._getTemporalDirectory(path.join(this.directory, ...baseSegments), partition, partitionValues);
103
114
  }
104
115
 
105
- // Hierarchical structure (default)
106
- const pathParts = [basePath, `partition=${partition}`];
107
-
116
+ const pathParts = [
117
+ this.directory,
118
+ ...baseSegments,
119
+ `partition=${this._sanitizePathValue(partition)}`
120
+ ];
121
+
108
122
  const sortedFields = Object.entries(partitionValues).sort(([a], [b]) => a.localeCompare(b));
109
123
  for (const [field, value] of sortedFields) {
110
124
  if (value !== null && value !== undefined) {
@@ -122,21 +136,15 @@ export class PartitionAwareFilesystemCache extends FilesystemCache {
122
136
  const { resource, action, partition, partitionValues, params } = options;
123
137
 
124
138
  if (resource && partition) {
125
- // Use partition-aware storage
126
139
  const partitionKey = this._getPartitionCacheKey(resource, action, partition, partitionValues, params);
127
- const partitionDir = this._getPartitionDirectory(resource, partition, partitionValues);
128
-
129
- await this._ensureDirectory(partitionDir);
130
-
131
- const filePath = path.join(partitionDir, this._sanitizeFileName(partitionKey));
132
-
133
- // Track usage if enabled
140
+
141
+ await this._ensurePartitionDirectoryForKey(partitionKey);
142
+
134
143
  if (this.trackUsage) {
135
144
  await this._trackPartitionUsage(resource, partition, partitionValues);
136
145
  }
137
-
138
- // Store with partition metadata
139
- const partitionData = {
146
+
147
+ const payload = {
140
148
  data,
141
149
  metadata: {
142
150
  resource,
@@ -146,8 +154,9 @@ export class PartitionAwareFilesystemCache extends FilesystemCache {
146
154
  ttl: this.ttl
147
155
  }
148
156
  };
149
-
150
- return this._writeFileWithMetadata(filePath, partitionData);
157
+
158
+ await super._set(partitionKey, payload);
159
+ return data;
151
160
  }
152
161
 
153
162
  // Fallback to standard set
@@ -190,24 +199,20 @@ export class PartitionAwareFilesystemCache extends FilesystemCache {
190
199
 
191
200
  if (resource && partition) {
192
201
  const partitionKey = this._getPartitionCacheKey(resource, action, partition, partitionValues, params);
193
- const partitionDir = this._getPartitionDirectory(resource, partition, partitionValues);
194
- const filePath = path.join(partitionDir, this._sanitizeFileName(partitionKey));
202
+ const payload = await super._get(partitionKey);
195
203
 
196
- if (!await this._fileExists(filePath)) {
197
- // Try preloading related partitions
204
+ if (!payload) {
198
205
  if (this.preloadRelated) {
199
206
  await this._preloadRelatedPartitions(resource, partition, partitionValues);
200
207
  }
201
208
  return null;
202
209
  }
203
210
 
204
- const result = await this._readFileWithMetadata(filePath);
205
-
206
- if (result && this.trackUsage) {
211
+ if (this.trackUsage) {
207
212
  await this._trackPartitionUsage(resource, partition, partitionValues);
208
213
  }
209
214
 
210
- return result?.data || null;
215
+ return payload?.data ?? null;
211
216
  }
212
217
 
213
218
  // Fallback to standard get
@@ -261,6 +266,48 @@ export class PartitionAwareFilesystemCache extends FilesystemCache {
261
266
  return ok;
262
267
  }
263
268
 
269
+ async _clear(prefix) {
270
+ await super._clear(prefix);
271
+
272
+ if (!prefix) {
273
+ const [entriesOk, , entries] = await tryFn(() => readdir(this.directory));
274
+ if (entriesOk && entries) {
275
+ for (const entry of entries) {
276
+ const entryPath = path.join(this.directory, entry);
277
+ const [statOk, , entryStat] = await tryFn(() => stat(entryPath));
278
+ if (statOk && entryStat.isDirectory() && entry.startsWith('resource=')) {
279
+ await rmdir(entryPath, { recursive: true }).catch(() => {});
280
+ }
281
+ }
282
+ }
283
+ this.partitionUsage.clear();
284
+ await this._saveUsageStats();
285
+ return true;
286
+ }
287
+
288
+ const segments = this._splitKeySegments(prefix).map(segment => this._sanitizeFileName(segment));
289
+ if (segments.length > 0) {
290
+ const dirPath = path.join(this.directory, ...segments);
291
+ if (await this._fileExists(dirPath)) {
292
+ await rmdir(dirPath, { recursive: true }).catch(() => {});
293
+ }
294
+
295
+ const resourceSeg = segments.find(seg => seg.startsWith('resource='));
296
+ const partitionSeg = segments.find(seg => seg.startsWith('partition='));
297
+ const resourceVal = resourceSeg ? resourceSeg.split('=').slice(1).join('=') : '';
298
+ const partitionVal = partitionSeg ? partitionSeg.split('=').slice(1).join('=') : '';
299
+ const usagePrefix = `${resourceVal}/${partitionVal}`;
300
+ for (const key of Array.from(this.partitionUsage.keys())) {
301
+ if (key.startsWith(usagePrefix)) {
302
+ this.partitionUsage.delete(key);
303
+ }
304
+ }
305
+ await this._saveUsageStats();
306
+ }
307
+
308
+ return true;
309
+ }
310
+
264
311
  /**
265
312
  * Get partition cache statistics
266
313
  */
@@ -378,12 +425,14 @@ export class PartitionAwareFilesystemCache extends FilesystemCache {
378
425
  }
379
426
 
380
427
  _getUsageKey(resource, partition, partitionValues) {
428
+ const sanitizedResource = this._sanitizePathValue(resource || '');
429
+ const sanitizedPartition = this._sanitizePathValue(partition || '');
381
430
  const valuePart = Object.entries(partitionValues)
382
431
  .sort(([a], [b]) => a.localeCompare(b))
383
- .map(([k, v]) => `${k}=${v}`)
432
+ .map(([k, v]) => `${k}=${this._sanitizePathValue(v)}`)
384
433
  .join('|');
385
434
 
386
- return `${resource}/${partition}/${valuePart}`;
435
+ return `${sanitizedResource}/${sanitizedPartition}/${valuePart}`;
387
436
  }
388
437
 
389
438
  async _preloadRelatedPartitions(resource, partition, partitionValues) {
@@ -426,6 +475,34 @@ export class PartitionAwareFilesystemCache extends FilesystemCache {
426
475
  return filename.replace(/[<>:"/\\|?*]/g, '_');
427
476
  }
428
477
 
478
+ _splitKeySegments(key) {
479
+ return key.split('/').filter(Boolean);
480
+ }
481
+
482
+ async _ensurePartitionDirectoryForKey(key) {
483
+ const segments = this._splitKeySegments(key);
484
+ if (segments.length <= 1) {
485
+ return;
486
+ }
487
+
488
+ const dirPath = path.join(
489
+ this.directory,
490
+ ...segments.slice(0, -1).map(segment => this._sanitizeFileName(segment))
491
+ );
492
+
493
+ await this._ensureDirectory(dirPath);
494
+ }
495
+
496
+ _getFilePath(key) {
497
+ const segments = this._splitKeySegments(key).map(segment => this._sanitizeFileName(segment));
498
+ const fileName = segments.pop() || this._sanitizeFileName(key);
499
+ const dirPath = segments.length > 0
500
+ ? path.join(this.directory, ...segments)
501
+ : this.directory;
502
+
503
+ return path.join(dirPath, `${this.prefix}_${fileName}${this.fileExtension}`);
504
+ }
505
+
429
506
  async _calculateDirectoryStats(dir, stats) {
430
507
  const [ok, err, files] = await tryFn(() => readdir(dir));
431
508
  if (!ok) return;
@@ -479,7 +556,15 @@ export class PartitionAwareFilesystemCache extends FilesystemCache {
479
556
  });
480
557
 
481
558
  if (!ok) {
482
- throw new Error(`Failed to write cache file: ${err.message}`);
559
+ throw new CacheError(`Failed to write cache file: ${err.message}`, {
560
+ driver: 'filesystem-partitioned',
561
+ operation: 'writeFileWithMetadata',
562
+ statusCode: 500,
563
+ retriable: false,
564
+ suggestion: 'Check filesystem permissions and disk space for the partition-aware cache directory.',
565
+ filePath,
566
+ original: err
567
+ });
483
568
  }
484
569
 
485
570
  return true;
@@ -498,4 +583,40 @@ export class PartitionAwareFilesystemCache extends FilesystemCache {
498
583
  return { data: content }; // Fallback for non-JSON data
499
584
  }
500
585
  }
501
- }
586
+
587
+ async size() {
588
+ const keys = await this.keys();
589
+ return keys.length;
590
+ }
591
+
592
+ async keys() {
593
+ const keys = [];
594
+ await this._collectKeysRecursive(this.directory, [], keys);
595
+ return keys;
596
+ }
597
+
598
+ async _collectKeysRecursive(currentDir, segments, result) {
599
+ const [ok, err, entries] = await tryFn(() => readdir(currentDir, { withFileTypes: true }));
600
+ if (!ok || !entries) {
601
+ return;
602
+ }
603
+
604
+ for (const entry of entries) {
605
+ const entryPath = path.join(currentDir, entry.name);
606
+ if (entry.isDirectory()) {
607
+ await this._collectKeysRecursive(entryPath, [...segments, entry.name], result);
608
+ continue;
609
+ }
610
+
611
+ if (!entry.isFile()) continue;
612
+
613
+ if (!entry.name.startsWith(`${this.prefix}_`) || !entry.name.endsWith(this.fileExtension)) {
614
+ continue;
615
+ }
616
+
617
+ const keyPart = entry.name.slice(this.prefix.length + 1, -this.fileExtension.length);
618
+ const fullSegments = segments.length > 0 ? [...segments, keyPart] : [keyPart];
619
+ result.push(fullSegments.join('/'));
620
+ }
621
+ }
622
+ }