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.
- package/README.md +139 -43
- package/dist/s3db.cjs +72425 -38970
- package/dist/s3db.cjs.map +1 -1
- package/dist/s3db.es.js +72177 -38764
- package/dist/s3db.es.js.map +1 -1
- package/mcp/lib/base-handler.js +157 -0
- package/mcp/lib/handlers/connection-handler.js +280 -0
- package/mcp/lib/handlers/query-handler.js +533 -0
- package/mcp/lib/handlers/resource-handler.js +428 -0
- package/mcp/lib/tool-registry.js +336 -0
- package/mcp/lib/tools/connection-tools.js +161 -0
- package/mcp/lib/tools/query-tools.js +267 -0
- package/mcp/lib/tools/resource-tools.js +404 -0
- package/package.json +94 -49
- package/src/clients/memory-client.class.js +346 -191
- package/src/clients/memory-storage.class.js +300 -84
- package/src/clients/s3-client.class.js +7 -6
- package/src/concerns/geo-encoding.js +19 -2
- package/src/concerns/ip.js +59 -9
- package/src/concerns/money.js +8 -1
- package/src/concerns/password-hashing.js +49 -8
- package/src/concerns/plugin-storage.js +186 -18
- package/src/concerns/storage-drivers/filesystem-driver.js +284 -0
- package/src/database.class.js +139 -29
- package/src/errors.js +332 -42
- package/src/plugins/api/auth/oidc-auth.js +66 -17
- package/src/plugins/api/auth/strategies/base-strategy.class.js +74 -0
- package/src/plugins/api/auth/strategies/factory.class.js +63 -0
- package/src/plugins/api/auth/strategies/global-strategy.class.js +44 -0
- package/src/plugins/api/auth/strategies/path-based-strategy.class.js +83 -0
- package/src/plugins/api/auth/strategies/path-rules-strategy.class.js +118 -0
- package/src/plugins/api/concerns/failban-manager.js +106 -57
- package/src/plugins/api/concerns/opengraph-helper.js +116 -0
- package/src/plugins/api/concerns/route-context.js +601 -0
- package/src/plugins/api/concerns/state-machine.js +288 -0
- package/src/plugins/api/index.js +180 -41
- package/src/plugins/api/routes/auth-routes.js +198 -30
- package/src/plugins/api/routes/resource-routes.js +19 -4
- package/src/plugins/api/server/health-manager.class.js +163 -0
- package/src/plugins/api/server/middleware-chain.class.js +310 -0
- package/src/plugins/api/server/router.class.js +472 -0
- package/src/plugins/api/server.js +280 -1303
- package/src/plugins/api/utils/custom-routes.js +17 -5
- package/src/plugins/api/utils/guards.js +76 -17
- package/src/plugins/api/utils/openapi-generator-cached.class.js +133 -0
- package/src/plugins/api/utils/openapi-generator.js +7 -6
- package/src/plugins/api/utils/template-engine.js +77 -3
- package/src/plugins/audit.plugin.js +30 -8
- package/src/plugins/backup.plugin.js +110 -14
- package/src/plugins/cache/cache.class.js +22 -5
- package/src/plugins/cache/filesystem-cache.class.js +116 -19
- package/src/plugins/cache/memory-cache.class.js +211 -57
- package/src/plugins/cache/multi-tier-cache.class.js +371 -0
- package/src/plugins/cache/partition-aware-filesystem-cache.class.js +168 -47
- package/src/plugins/cache/redis-cache.class.js +552 -0
- package/src/plugins/cache/s3-cache.class.js +17 -8
- package/src/plugins/cache.plugin.js +176 -61
- package/src/plugins/cloud-inventory/drivers/alibaba-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/aws-driver.js +60 -29
- package/src/plugins/cloud-inventory/drivers/azure-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/base-driver.js +16 -2
- package/src/plugins/cloud-inventory/drivers/cloudflare-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/digitalocean-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/hetzner-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/linode-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/mongodb-atlas-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/vultr-driver.js +8 -1
- package/src/plugins/cloud-inventory/index.js +29 -8
- package/src/plugins/cloud-inventory/registry.js +64 -42
- package/src/plugins/cloud-inventory.plugin.js +240 -138
- package/src/plugins/concerns/plugin-dependencies.js +54 -0
- package/src/plugins/concerns/resource-names.js +100 -0
- package/src/plugins/consumers/index.js +10 -2
- package/src/plugins/consumers/sqs-consumer.js +12 -2
- package/src/plugins/cookie-farm-suite.plugin.js +278 -0
- package/src/plugins/cookie-farm.errors.js +73 -0
- package/src/plugins/cookie-farm.plugin.js +869 -0
- package/src/plugins/costs.plugin.js +7 -1
- package/src/plugins/eventual-consistency/analytics.js +94 -19
- package/src/plugins/eventual-consistency/config.js +15 -7
- package/src/plugins/eventual-consistency/consolidation.js +29 -11
- package/src/plugins/eventual-consistency/garbage-collection.js +3 -1
- package/src/plugins/eventual-consistency/helpers.js +39 -14
- package/src/plugins/eventual-consistency/install.js +21 -2
- package/src/plugins/eventual-consistency/utils.js +32 -10
- package/src/plugins/fulltext.plugin.js +38 -11
- package/src/plugins/geo.plugin.js +61 -9
- package/src/plugins/identity/concerns/config.js +61 -0
- package/src/plugins/identity/concerns/mfa-manager.js +15 -2
- package/src/plugins/identity/concerns/rate-limit.js +124 -0
- package/src/plugins/identity/concerns/resource-schemas.js +9 -1
- package/src/plugins/identity/concerns/token-generator.js +29 -4
- package/src/plugins/identity/drivers/auth-driver.interface.js +76 -0
- package/src/plugins/identity/drivers/client-credentials-driver.js +127 -0
- package/src/plugins/identity/drivers/index.js +18 -0
- package/src/plugins/identity/drivers/password-driver.js +122 -0
- package/src/plugins/identity/email-service.js +17 -2
- package/src/plugins/identity/index.js +413 -69
- package/src/plugins/identity/oauth2-server.js +413 -30
- package/src/plugins/identity/oidc-discovery.js +16 -8
- package/src/plugins/identity/rsa-keys.js +115 -35
- package/src/plugins/identity/server.js +166 -45
- package/src/plugins/identity/session-manager.js +53 -7
- package/src/plugins/identity/ui/pages/mfa-verification.js +17 -15
- package/src/plugins/identity/ui/routes.js +363 -255
- package/src/plugins/importer/index.js +153 -20
- package/src/plugins/index.js +9 -2
- package/src/plugins/kubernetes-inventory/index.js +6 -0
- package/src/plugins/kubernetes-inventory/k8s-driver.js +867 -0
- package/src/plugins/kubernetes-inventory/resource-types.js +274 -0
- package/src/plugins/kubernetes-inventory.plugin.js +980 -0
- package/src/plugins/metrics.plugin.js +64 -16
- package/src/plugins/ml/base-model.class.js +25 -15
- package/src/plugins/ml/regression-model.class.js +1 -1
- package/src/plugins/ml.errors.js +57 -25
- package/src/plugins/ml.plugin.js +28 -4
- package/src/plugins/namespace.js +210 -0
- package/src/plugins/plugin.class.js +180 -8
- package/src/plugins/puppeteer/console-monitor.js +729 -0
- package/src/plugins/puppeteer/cookie-manager.js +492 -0
- package/src/plugins/puppeteer/network-monitor.js +816 -0
- package/src/plugins/puppeteer/performance-manager.js +746 -0
- package/src/plugins/puppeteer/proxy-manager.js +478 -0
- package/src/plugins/puppeteer/stealth-manager.js +556 -0
- package/src/plugins/puppeteer.errors.js +81 -0
- package/src/plugins/puppeteer.plugin.js +1327 -0
- package/src/plugins/queue-consumer.plugin.js +69 -14
- package/src/plugins/recon/behaviors/uptime-behavior.js +691 -0
- package/src/plugins/recon/concerns/command-runner.js +148 -0
- package/src/plugins/recon/concerns/diff-detector.js +372 -0
- package/src/plugins/recon/concerns/fingerprint-builder.js +307 -0
- package/src/plugins/recon/concerns/process-manager.js +338 -0
- package/src/plugins/recon/concerns/report-generator.js +478 -0
- package/src/plugins/recon/concerns/security-analyzer.js +571 -0
- package/src/plugins/recon/concerns/target-normalizer.js +68 -0
- package/src/plugins/recon/config/defaults.js +321 -0
- package/src/plugins/recon/config/resources.js +370 -0
- package/src/plugins/recon/index.js +778 -0
- package/src/plugins/recon/managers/dependency-manager.js +174 -0
- package/src/plugins/recon/managers/scheduler-manager.js +179 -0
- package/src/plugins/recon/managers/storage-manager.js +745 -0
- package/src/plugins/recon/managers/target-manager.js +274 -0
- package/src/plugins/recon/stages/asn-stage.js +314 -0
- package/src/plugins/recon/stages/certificate-stage.js +84 -0
- package/src/plugins/recon/stages/dns-stage.js +107 -0
- package/src/plugins/recon/stages/dnsdumpster-stage.js +362 -0
- package/src/plugins/recon/stages/fingerprint-stage.js +71 -0
- package/src/plugins/recon/stages/google-dorks-stage.js +440 -0
- package/src/plugins/recon/stages/http-stage.js +89 -0
- package/src/plugins/recon/stages/latency-stage.js +148 -0
- package/src/plugins/recon/stages/massdns-stage.js +302 -0
- package/src/plugins/recon/stages/osint-stage.js +1373 -0
- package/src/plugins/recon/stages/ports-stage.js +169 -0
- package/src/plugins/recon/stages/screenshot-stage.js +94 -0
- package/src/plugins/recon/stages/secrets-stage.js +514 -0
- package/src/plugins/recon/stages/subdomains-stage.js +295 -0
- package/src/plugins/recon/stages/tls-audit-stage.js +78 -0
- package/src/plugins/recon/stages/vulnerability-stage.js +78 -0
- package/src/plugins/recon/stages/web-discovery-stage.js +113 -0
- package/src/plugins/recon/stages/whois-stage.js +349 -0
- package/src/plugins/recon.plugin.js +75 -0
- package/src/plugins/recon.plugin.js.backup +2635 -0
- package/src/plugins/relation.errors.js +87 -14
- package/src/plugins/replicator.plugin.js +514 -137
- package/src/plugins/replicators/base-replicator.class.js +89 -1
- package/src/plugins/replicators/bigquery-replicator.class.js +66 -22
- package/src/plugins/replicators/dynamodb-replicator.class.js +22 -15
- package/src/plugins/replicators/mongodb-replicator.class.js +22 -15
- package/src/plugins/replicators/mysql-replicator.class.js +52 -17
- package/src/plugins/replicators/planetscale-replicator.class.js +30 -4
- package/src/plugins/replicators/postgres-replicator.class.js +62 -27
- package/src/plugins/replicators/s3db-replicator.class.js +25 -18
- package/src/plugins/replicators/schema-sync.helper.js +3 -3
- package/src/plugins/replicators/sqs-replicator.class.js +8 -2
- package/src/plugins/replicators/turso-replicator.class.js +23 -3
- package/src/plugins/replicators/webhook-replicator.class.js +42 -4
- package/src/plugins/s3-queue.plugin.js +464 -65
- package/src/plugins/scheduler.plugin.js +20 -6
- package/src/plugins/state-machine.plugin.js +40 -9
- package/src/plugins/tfstate/README.md +126 -126
- package/src/plugins/tfstate/base-driver.js +28 -4
- package/src/plugins/tfstate/errors.js +65 -10
- package/src/plugins/tfstate/filesystem-driver.js +52 -8
- package/src/plugins/tfstate/index.js +163 -90
- package/src/plugins/tfstate/s3-driver.js +64 -6
- package/src/plugins/ttl.plugin.js +72 -17
- package/src/plugins/vector/distances.js +18 -12
- package/src/plugins/vector/kmeans.js +26 -4
- package/src/resource.class.js +115 -19
- package/src/testing/factory.class.js +20 -3
- package/src/testing/seeder.class.js +7 -1
- package/src/clients/memory-client.md +0 -917
- 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.
|
|
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.
|
|
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
|
-
|
|
212
|
-
const
|
|
213
|
-
|
|
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
|
-
|
|
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() {
|