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,552 @@
1
+ /**
2
+ * Redis Cache Configuration Documentation
3
+ *
4
+ * This cache implementation uses Redis as a distributed cache backend,
5
+ * providing persistent storage that can be shared across multiple instances.
6
+ * It's suitable for production deployments with multiple servers.
7
+ *
8
+ * @typedef {Object} RedisCacheConfig
9
+ * @property {string} [host='localhost'] - Redis server hostname
10
+ * @property {number} [port=6379] - Redis server port
11
+ * @property {string} [password] - Redis authentication password
12
+ * @property {number} [db=0] - Redis database number (0-15)
13
+ * @property {string} [keyPrefix='cache'] - Prefix for all Redis keys
14
+ * @property {number} [ttl=3600000] - Time to live in milliseconds (1 hour default)
15
+ * @property {boolean} [enableCompression=true] - Whether to compress cache values using gzip
16
+ * @property {number} [compressionThreshold=1024] - Minimum size in bytes to trigger compression
17
+ * @property {number} [connectTimeout=5000] - Connection timeout in milliseconds
18
+ * @property {number} [commandTimeout=5000] - Command execution timeout in milliseconds
19
+ * @property {number} [retryAttempts=3] - Number of retry attempts for failed operations
20
+ * @property {number} [retryDelay=1000] - Delay in milliseconds between retry attempts
21
+ * @property {boolean} [lazyConnect=true] - Connect to Redis on first operation instead of constructor
22
+ * @property {boolean} [keepAlive=true] - Enable TCP keepalive
23
+ * @property {number} [keepAliveInitialDelay=0] - Initial delay for keepalive probes
24
+ * @property {Function} [retryStrategy] - Custom retry strategy function
25
+ * @property {boolean} [enableStats=false] - Track hits/misses/errors
26
+ *
27
+ * @example
28
+ * // Basic configuration
29
+ * {
30
+ * host: 'localhost',
31
+ * port: 6379,
32
+ * keyPrefix: 'app-cache/',
33
+ * ttl: 3600000, // 1 hour
34
+ * enableCompression: true
35
+ * }
36
+ *
37
+ * @example
38
+ * // Production with authentication and compression
39
+ * {
40
+ * host: 'redis.production.com',
41
+ * port: 6379,
42
+ * password: 'secret',
43
+ * db: 1,
44
+ * keyPrefix: 'myapp-cache/',
45
+ * ttl: 7200000, // 2 hours
46
+ * enableCompression: true,
47
+ * compressionThreshold: 512,
48
+ * retryAttempts: 5
49
+ * }
50
+ *
51
+ * @example
52
+ * // High-performance configuration
53
+ * {
54
+ * host: 'localhost',
55
+ * port: 6379,
56
+ * keyPrefix: 'cache/',
57
+ * enableCompression: false, // Disable for speed
58
+ * connectTimeout: 3000,
59
+ * commandTimeout: 3000,
60
+ * keepAlive: true
61
+ * }
62
+ *
63
+ * @notes
64
+ * - Requires 'ioredis' package as peer dependency
65
+ * - TTL is enforced by Redis native expiration
66
+ * - Compression reduces network bandwidth and storage
67
+ * - All operations are async and return Promises
68
+ * - Connection pooling is handled by ioredis
69
+ * - Supports Redis Cluster and Sentinel (via ioredis options)
70
+ * - Keys are namespaced with keyPrefix to avoid collisions
71
+ * - Automatic reconnection with exponential backoff
72
+ */
73
+ import zlib from "node:zlib";
74
+ import { promisify } from "node:util";
75
+ import { Cache } from "./cache.class.js";
76
+ import { CacheError } from "../cache.errors.js";
77
+ import { requirePluginDependency } from "../concerns/plugin-dependencies.js";
78
+
79
+ const gzip = promisify(zlib.gzip);
80
+ const unzip = promisify(zlib.unzip);
81
+
82
+ export class RedisCache extends Cache {
83
+ constructor({
84
+ host = 'localhost',
85
+ port = 6379,
86
+ password,
87
+ db = 0,
88
+ keyPrefix = 'cache',
89
+ ttl = 3600000,
90
+ enableCompression = true,
91
+ compressionThreshold = 1024,
92
+ connectTimeout = 5000,
93
+ commandTimeout = 5000,
94
+ retryAttempts = 3,
95
+ retryDelay = 1000,
96
+ lazyConnect = true,
97
+ keepAlive = true,
98
+ keepAliveInitialDelay = 0,
99
+ retryStrategy,
100
+ enableStats = false,
101
+ ...redisOptions
102
+ }) {
103
+ super();
104
+
105
+ // Validate ioredis dependency
106
+ requirePluginDependency('ioredis', 'RedisCache');
107
+
108
+ this.config = {
109
+ host,
110
+ port,
111
+ password,
112
+ db,
113
+ keyPrefix: keyPrefix.endsWith('/') ? keyPrefix : keyPrefix + '/',
114
+ ttl,
115
+ enableCompression,
116
+ compressionThreshold,
117
+ connectTimeout,
118
+ commandTimeout,
119
+ retryAttempts,
120
+ retryDelay,
121
+ lazyConnect,
122
+ keepAlive,
123
+ keepAliveInitialDelay,
124
+ retryStrategy,
125
+ enableStats,
126
+ ...redisOptions
127
+ };
128
+
129
+ this.ttlMs = typeof ttl === 'number' && ttl > 0 ? ttl : 0;
130
+ this.ttlSeconds = this.ttlMs > 0 ? Math.ceil(this.ttlMs / 1000) : 0;
131
+
132
+ // Statistics
133
+ this.stats = {
134
+ hits: 0,
135
+ misses: 0,
136
+ errors: 0,
137
+ sets: 0,
138
+ deletes: 0,
139
+ enabled: enableStats
140
+ };
141
+
142
+ // Redis client will be initialized on first use (lazy connect)
143
+ this.client = null;
144
+ this.connected = false;
145
+ this.connecting = false;
146
+ }
147
+
148
+ /**
149
+ * Initialize Redis connection
150
+ * @private
151
+ */
152
+ async _ensureConnection() {
153
+ if (this.connected) return;
154
+ if (this.connecting) {
155
+ // Wait for existing connection attempt
156
+ await new Promise(resolve => {
157
+ const check = setInterval(() => {
158
+ if (this.connected || !this.connecting) {
159
+ clearInterval(check);
160
+ resolve();
161
+ }
162
+ }, 50);
163
+ });
164
+ return;
165
+ }
166
+
167
+ this.connecting = true;
168
+
169
+ try {
170
+ // Dynamic import of ioredis
171
+ const Redis = (await import('ioredis')).default;
172
+
173
+ // Create Redis client with configuration
174
+ this.client = new Redis({
175
+ host: this.config.host,
176
+ port: this.config.port,
177
+ password: this.config.password,
178
+ db: this.config.db,
179
+ connectTimeout: this.config.connectTimeout,
180
+ commandTimeout: this.config.commandTimeout,
181
+ lazyConnect: this.config.lazyConnect,
182
+ keepAlive: this.config.keepAlive,
183
+ keepAliveInitialDelay: this.config.keepAliveInitialDelay,
184
+ retryStrategy: this.config.retryStrategy || ((times) => {
185
+ if (times > this.config.retryAttempts) {
186
+ return null; // Stop retrying
187
+ }
188
+ return Math.min(times * this.config.retryDelay, 5000); // Max 5s delay
189
+ }),
190
+ ...this.config.redisOptions
191
+ });
192
+
193
+ // Wait for connection if lazy connect
194
+ if (this.config.lazyConnect) {
195
+ await this.client.connect();
196
+ }
197
+
198
+ this.connected = true;
199
+ this.connecting = false;
200
+
201
+ // Handle connection events
202
+ this.client.on('error', (err) => {
203
+ if (this.config.enableStats) {
204
+ this.stats.errors++;
205
+ }
206
+ console.error('Redis connection error:', err);
207
+ });
208
+
209
+ this.client.on('close', () => {
210
+ this.connected = false;
211
+ });
212
+
213
+ this.client.on('reconnecting', () => {
214
+ this.connected = false;
215
+ this.connecting = true;
216
+ });
217
+
218
+ this.client.on('ready', () => {
219
+ this.connected = true;
220
+ this.connecting = false;
221
+ });
222
+
223
+ } catch (error) {
224
+ this.connecting = false;
225
+ throw new CacheError('Failed to connect to Redis', {
226
+ operation: 'connect',
227
+ driver: 'RedisCache',
228
+ config: {
229
+ host: this.config.host,
230
+ port: this.config.port,
231
+ db: this.config.db
232
+ },
233
+ cause: error,
234
+ suggestion: 'Ensure Redis server is running and accessible. Install ioredis: npm install ioredis'
235
+ });
236
+ }
237
+ }
238
+
239
+ /**
240
+ * Build full Redis key with prefix
241
+ * @private
242
+ */
243
+ _getKey(key) {
244
+ return `${this.config.keyPrefix}${key}`;
245
+ }
246
+
247
+ /**
248
+ * Compress data if enabled and above threshold
249
+ * @private
250
+ */
251
+ async _compressData(data) {
252
+ const jsonString = JSON.stringify(data);
253
+
254
+ // Don't compress if disabled or below threshold
255
+ if (!this.config.enableCompression || jsonString.length < this.config.compressionThreshold) {
256
+ return {
257
+ data: jsonString,
258
+ compressed: false,
259
+ originalSize: jsonString.length
260
+ };
261
+ }
262
+
263
+ // Compress with gzip
264
+ const compressed = await gzip(Buffer.from(jsonString, 'utf-8'));
265
+ return {
266
+ data: compressed.toString('base64'),
267
+ compressed: true,
268
+ originalSize: jsonString.length,
269
+ compressedSize: compressed.length,
270
+ compressionRatio: (compressed.length / jsonString.length).toFixed(2)
271
+ };
272
+ }
273
+
274
+ /**
275
+ * Decompress data if needed
276
+ * @private
277
+ */
278
+ async _decompressData(storedData) {
279
+ if (!storedData) return null;
280
+
281
+ // Parse the stored metadata
282
+ const metadata = JSON.parse(storedData);
283
+
284
+ if (!metadata.compressed) {
285
+ // Not compressed - parse JSON directly
286
+ return JSON.parse(metadata.data);
287
+ }
288
+
289
+ // Decompress gzip data
290
+ const buffer = Buffer.from(metadata.data, 'base64');
291
+ const decompressed = await unzip(buffer);
292
+ return JSON.parse(decompressed.toString('utf-8'));
293
+ }
294
+
295
+ async _set(key, data) {
296
+ await this._ensureConnection();
297
+
298
+ try {
299
+ const compressed = await this._compressData(data);
300
+ const redisKey = this._getKey(key);
301
+ const value = JSON.stringify(compressed);
302
+
303
+ // Set with TTL if configured
304
+ if (this.ttlSeconds > 0) {
305
+ await this.client.setex(redisKey, this.ttlSeconds, value);
306
+ } else {
307
+ await this.client.set(redisKey, value);
308
+ }
309
+
310
+ if (this.config.enableStats) {
311
+ this.stats.sets++;
312
+ }
313
+
314
+ return true;
315
+ } catch (error) {
316
+ if (this.config.enableStats) {
317
+ this.stats.errors++;
318
+ }
319
+ throw new CacheError('Failed to set cache value in Redis', {
320
+ operation: 'set',
321
+ driver: 'RedisCache',
322
+ key,
323
+ cause: error,
324
+ retriable: true,
325
+ suggestion: 'Check Redis connection and server status'
326
+ });
327
+ }
328
+ }
329
+
330
+ async _get(key) {
331
+ await this._ensureConnection();
332
+
333
+ try {
334
+ const redisKey = this._getKey(key);
335
+ const value = await this.client.get(redisKey);
336
+
337
+ if (!value) {
338
+ if (this.config.enableStats) {
339
+ this.stats.misses++;
340
+ }
341
+ return null;
342
+ }
343
+
344
+ if (this.config.enableStats) {
345
+ this.stats.hits++;
346
+ }
347
+
348
+ return await this._decompressData(value);
349
+ } catch (error) {
350
+ if (this.config.enableStats) {
351
+ this.stats.errors++;
352
+ }
353
+ throw new CacheError('Failed to get cache value from Redis', {
354
+ operation: 'get',
355
+ driver: 'RedisCache',
356
+ key,
357
+ cause: error,
358
+ retriable: true,
359
+ suggestion: 'Check Redis connection and server status'
360
+ });
361
+ }
362
+ }
363
+
364
+ async _del(key) {
365
+ await this._ensureConnection();
366
+
367
+ try {
368
+ const redisKey = this._getKey(key);
369
+ await this.client.del(redisKey);
370
+
371
+ if (this.config.enableStats) {
372
+ this.stats.deletes++;
373
+ }
374
+
375
+ return true;
376
+ } catch (error) {
377
+ if (this.config.enableStats) {
378
+ this.stats.errors++;
379
+ }
380
+ throw new CacheError('Failed to delete cache key from Redis', {
381
+ operation: 'delete',
382
+ driver: 'RedisCache',
383
+ key,
384
+ cause: error,
385
+ retriable: true,
386
+ suggestion: 'Check Redis connection and server status'
387
+ });
388
+ }
389
+ }
390
+
391
+ async _clear(prefix) {
392
+ await this._ensureConnection();
393
+
394
+ try {
395
+ const pattern = prefix
396
+ ? `${this.config.keyPrefix}${prefix}*`
397
+ : `${this.config.keyPrefix}*`;
398
+
399
+ // Use SCAN to avoid blocking Redis (safer than KEYS)
400
+ let cursor = '0';
401
+ let deletedCount = 0;
402
+
403
+ do {
404
+ const [nextCursor, keys] = await this.client.scan(
405
+ cursor,
406
+ 'MATCH',
407
+ pattern,
408
+ 'COUNT',
409
+ 100
410
+ );
411
+
412
+ cursor = nextCursor;
413
+
414
+ if (keys.length > 0) {
415
+ await this.client.del(...keys);
416
+ deletedCount += keys.length;
417
+ }
418
+ } while (cursor !== '0');
419
+
420
+ if (this.config.enableStats) {
421
+ this.stats.deletes += deletedCount;
422
+ }
423
+
424
+ return true;
425
+ } catch (error) {
426
+ if (this.config.enableStats) {
427
+ this.stats.errors++;
428
+ }
429
+ throw new CacheError('Failed to clear cache keys from Redis', {
430
+ operation: 'clear',
431
+ driver: 'RedisCache',
432
+ prefix,
433
+ cause: error,
434
+ retriable: true,
435
+ suggestion: 'Check Redis connection and server status'
436
+ });
437
+ }
438
+ }
439
+
440
+ async size() {
441
+ await this._ensureConnection();
442
+
443
+ try {
444
+ const pattern = `${this.config.keyPrefix}*`;
445
+ let cursor = '0';
446
+ let count = 0;
447
+
448
+ do {
449
+ const [nextCursor, keys] = await this.client.scan(
450
+ cursor,
451
+ 'MATCH',
452
+ pattern,
453
+ 'COUNT',
454
+ 100
455
+ );
456
+
457
+ cursor = nextCursor;
458
+ count += keys.length;
459
+ } while (cursor !== '0');
460
+
461
+ return count;
462
+ } catch (error) {
463
+ throw new CacheError('Failed to get cache size from Redis', {
464
+ operation: 'size',
465
+ driver: 'RedisCache',
466
+ cause: error,
467
+ retriable: true,
468
+ suggestion: 'Check Redis connection and server status'
469
+ });
470
+ }
471
+ }
472
+
473
+ async keys() {
474
+ await this._ensureConnection();
475
+
476
+ try {
477
+ const pattern = `${this.config.keyPrefix}*`;
478
+ const allKeys = [];
479
+ let cursor = '0';
480
+
481
+ do {
482
+ const [nextCursor, keys] = await this.client.scan(
483
+ cursor,
484
+ 'MATCH',
485
+ pattern,
486
+ 'COUNT',
487
+ 100
488
+ );
489
+
490
+ cursor = nextCursor;
491
+
492
+ // Remove prefix from keys
493
+ const cleanKeys = keys.map(k => k.startsWith(this.config.keyPrefix)
494
+ ? k.slice(this.config.keyPrefix.length)
495
+ : k
496
+ );
497
+
498
+ allKeys.push(...cleanKeys);
499
+ } while (cursor !== '0');
500
+
501
+ return allKeys;
502
+ } catch (error) {
503
+ throw new CacheError('Failed to get cache keys from Redis', {
504
+ operation: 'keys',
505
+ driver: 'RedisCache',
506
+ cause: error,
507
+ retriable: true,
508
+ suggestion: 'Check Redis connection and server status'
509
+ });
510
+ }
511
+ }
512
+
513
+ /**
514
+ * Get cache statistics
515
+ */
516
+ getStats() {
517
+ if (!this.stats.enabled) {
518
+ return {
519
+ enabled: false,
520
+ message: 'Statistics are disabled. Enable with enableStats: true'
521
+ };
522
+ }
523
+
524
+ const total = this.stats.hits + this.stats.misses;
525
+ const hitRate = total > 0 ? this.stats.hits / total : 0;
526
+
527
+ return {
528
+ enabled: true,
529
+ hits: this.stats.hits,
530
+ misses: this.stats.misses,
531
+ errors: this.stats.errors,
532
+ sets: this.stats.sets,
533
+ deletes: this.stats.deletes,
534
+ total,
535
+ hitRate,
536
+ hitRatePercent: (hitRate * 100).toFixed(2) + '%'
537
+ };
538
+ }
539
+
540
+ /**
541
+ * Disconnect from Redis
542
+ */
543
+ async disconnect() {
544
+ if (this.client && this.connected) {
545
+ await this.client.quit();
546
+ this.connected = false;
547
+ this.client = null;
548
+ }
549
+ }
550
+ }
551
+
552
+ export default RedisCache;
@@ -120,7 +120,9 @@ export class S3Cache extends Cache {
120
120
  super();
121
121
  this.client = client;
122
122
  this.keyPrefix = keyPrefix;
123
- this.config.ttl = ttl;
123
+ this.ttlMs = typeof ttl === 'number' && ttl > 0 ? ttl : 0;
124
+ this.ttlSeconds = this.ttlMs > 0 ? Math.ceil(this.ttlMs / 1000) : 0;
125
+ this.config.ttl = this.ttlMs;
124
126
  this.config.client = client;
125
127
  this.config.prefix = prefix !== undefined ? prefix : keyPrefix + (keyPrefix.endsWith('/') ? '' : '/');
126
128
  this.config.enableCompression = enableCompression;
@@ -182,7 +184,7 @@ export class S3Cache extends Cache {
182
184
  this.storage.getPluginKey(null, this.keyPrefix, key),
183
185
  compressed,
184
186
  {
185
- ttl: this.config.ttl,
187
+ ttl: this.ttlSeconds,
186
188
  behavior: 'body-only', // Compressed data is already optimized, skip metadata encoding
187
189
  contentType: compressed.compressed ? 'application/gzip' : 'application/json'
188
190
  }
@@ -207,15 +209,22 @@ export class S3Cache extends Cache {
207
209
  return true;
208
210
  }
209
211
 
210
- async _clear() {
211
- // Get all keys with the cache plugin prefix
212
- const pluginPrefix = `plugin=cache/${this.keyPrefix}`;
213
- const allKeys = await this.client.getAllKeys({ prefix: pluginPrefix });
212
+ async _clear(prefix) {
213
+ const basePrefix = `plugin=cache/${this.keyPrefix}`;
214
+ const listPrefix = prefix
215
+ ? `${basePrefix}/${prefix}`
216
+ : basePrefix;
217
+
218
+ const allKeys = await this.client.getAllKeys({ prefix: listPrefix });
214
219
 
215
- // Delete all cache keys
216
220
  for (const key of allKeys) {
217
- await this.storage.delete(key);
221
+ // When listing without prefix, filter manually if prefix supplied (defensive)
222
+ if (!prefix || key.startsWith(`${basePrefix}/${prefix}`)) {
223
+ await this.storage.delete(key);
224
+ }
218
225
  }
226
+
227
+ return true;
219
228
  }
220
229
 
221
230
  async size() {