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
|
@@ -9,6 +9,7 @@ import { mkdir, writeFile, readFile, unlink, stat, readdir } from 'fs/promises';
|
|
|
9
9
|
import path from 'path';
|
|
10
10
|
import crypto from 'crypto';
|
|
11
11
|
import os from 'os';
|
|
12
|
+
import { PluginError } from "../errors.js";
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* BackupPlugin - Automated Database Backup System
|
|
@@ -121,15 +122,44 @@ export class BackupPlugin extends Plugin {
|
|
|
121
122
|
this._validateConfiguration();
|
|
122
123
|
}
|
|
123
124
|
|
|
125
|
+
createError(message, details = {}) {
|
|
126
|
+
const {
|
|
127
|
+
operation = 'unknown',
|
|
128
|
+
statusCode = 500,
|
|
129
|
+
retriable = false,
|
|
130
|
+
docs = 'docs/plugins/backup.md',
|
|
131
|
+
...rest
|
|
132
|
+
} = details;
|
|
133
|
+
|
|
134
|
+
return new PluginError(message, {
|
|
135
|
+
pluginName: 'BackupPlugin',
|
|
136
|
+
operation,
|
|
137
|
+
statusCode,
|
|
138
|
+
retriable,
|
|
139
|
+
docs,
|
|
140
|
+
...rest
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
124
144
|
_validateConfiguration() {
|
|
125
145
|
// Driver validation is done in constructor
|
|
126
146
|
|
|
127
147
|
if (this.config.encryption && (!this.config.encryption.key || !this.config.encryption.algorithm)) {
|
|
128
|
-
throw
|
|
148
|
+
throw this.createError('BackupPlugin: Encryption requires both key and algorithm', {
|
|
149
|
+
operation: 'validateConfiguration',
|
|
150
|
+
statusCode: 400,
|
|
151
|
+
retriable: false,
|
|
152
|
+
suggestion: 'Provide both encryption.key and encryption.algorithm (e.g. aes-256-gcm) or disable encryption.'
|
|
153
|
+
});
|
|
129
154
|
}
|
|
130
155
|
|
|
131
156
|
if (this.config.compression && !['none', 'gzip', 'brotli', 'deflate'].includes(this.config.compression)) {
|
|
132
|
-
throw
|
|
157
|
+
throw this.createError('BackupPlugin: Invalid compression type. Use: none, gzip, brotli, deflate', {
|
|
158
|
+
operation: 'validateConfiguration',
|
|
159
|
+
statusCode: 400,
|
|
160
|
+
retriable: false,
|
|
161
|
+
suggestion: 'Choose one of the supported compression strategies: none, gzip, brotli, or deflate.'
|
|
162
|
+
});
|
|
133
163
|
}
|
|
134
164
|
}
|
|
135
165
|
|
|
@@ -194,7 +224,13 @@ export class BackupPlugin extends Plugin {
|
|
|
194
224
|
|
|
195
225
|
// Check for race condition
|
|
196
226
|
if (this.activeBackups.has(backupId)) {
|
|
197
|
-
throw
|
|
227
|
+
throw this.createError(`Backup '${backupId}' is already in progress`, {
|
|
228
|
+
operation: 'createBackup',
|
|
229
|
+
statusCode: 409,
|
|
230
|
+
retriable: true,
|
|
231
|
+
suggestion: 'Wait for the current backup task to finish or use a different backupId before retrying.',
|
|
232
|
+
metadata: { backupId }
|
|
233
|
+
});
|
|
198
234
|
}
|
|
199
235
|
|
|
200
236
|
try {
|
|
@@ -223,7 +259,13 @@ export class BackupPlugin extends Plugin {
|
|
|
223
259
|
|
|
224
260
|
// Check if we have any files to backup
|
|
225
261
|
if (exportedFiles.length === 0) {
|
|
226
|
-
throw
|
|
262
|
+
throw this.createError('No resources were exported for backup', {
|
|
263
|
+
operation: 'exportResources',
|
|
264
|
+
statusCode: 500,
|
|
265
|
+
retriable: false,
|
|
266
|
+
suggestion: 'Check include/exclude filters and ensure resources have data before starting the backup.',
|
|
267
|
+
metadata: { backupId, type }
|
|
268
|
+
});
|
|
227
269
|
}
|
|
228
270
|
|
|
229
271
|
// Create archive
|
|
@@ -241,7 +283,13 @@ export class BackupPlugin extends Plugin {
|
|
|
241
283
|
if (this.config.verification) {
|
|
242
284
|
const isValid = await this.driver.verify(backupId, checksum, uploadResult);
|
|
243
285
|
if (!isValid) {
|
|
244
|
-
throw
|
|
286
|
+
throw this.createError('Backup verification failed', {
|
|
287
|
+
operation: 'verifyBackup',
|
|
288
|
+
statusCode: 502,
|
|
289
|
+
retriable: true,
|
|
290
|
+
suggestion: 'Inspect driver logs or rerun the backup with verbose logging to diagnose verification failures.',
|
|
291
|
+
metadata: { backupId, checksum }
|
|
292
|
+
});
|
|
245
293
|
}
|
|
246
294
|
}
|
|
247
295
|
|
|
@@ -554,7 +602,13 @@ export class BackupPlugin extends Plugin {
|
|
|
554
602
|
});
|
|
555
603
|
|
|
556
604
|
if (!ok) {
|
|
557
|
-
throw
|
|
605
|
+
throw this.createError(`Failed to generate checksum for ${filePath}: ${err?.message}`, {
|
|
606
|
+
operation: 'generateChecksum',
|
|
607
|
+
statusCode: 500,
|
|
608
|
+
retriable: true,
|
|
609
|
+
suggestion: 'Ensure the archive is readable and rerun the backup with verbose logging.',
|
|
610
|
+
metadata: { filePath }
|
|
611
|
+
});
|
|
558
612
|
}
|
|
559
613
|
|
|
560
614
|
return result;
|
|
@@ -584,11 +638,23 @@ export class BackupPlugin extends Plugin {
|
|
|
584
638
|
// Get backup metadata
|
|
585
639
|
const backup = await this.getBackupStatus(backupId);
|
|
586
640
|
if (!backup) {
|
|
587
|
-
throw
|
|
641
|
+
throw this.createError(`Backup '${backupId}' not found`, {
|
|
642
|
+
operation: 'restore',
|
|
643
|
+
statusCode: 404,
|
|
644
|
+
retriable: false,
|
|
645
|
+
suggestion: 'Confirm the backupId exists or create a new backup before attempting restore.',
|
|
646
|
+
metadata: { backupId }
|
|
647
|
+
});
|
|
588
648
|
}
|
|
589
649
|
|
|
590
650
|
if (backup.status !== 'completed') {
|
|
591
|
-
throw
|
|
651
|
+
throw this.createError(`Backup '${backupId}' is not in completed status`, {
|
|
652
|
+
operation: 'restore',
|
|
653
|
+
statusCode: 409,
|
|
654
|
+
retriable: true,
|
|
655
|
+
suggestion: 'Allow the running backup to finish or investigate previous errors before retrying restore.',
|
|
656
|
+
metadata: { backupId, status: backup.status }
|
|
657
|
+
});
|
|
592
658
|
}
|
|
593
659
|
|
|
594
660
|
// Create temporary restore directory
|
|
@@ -604,7 +670,13 @@ export class BackupPlugin extends Plugin {
|
|
|
604
670
|
if (this.config.verification && backup.checksum) {
|
|
605
671
|
const actualChecksum = await this._generateChecksum(downloadPath);
|
|
606
672
|
if (actualChecksum !== backup.checksum) {
|
|
607
|
-
throw
|
|
673
|
+
throw this.createError('Backup verification failed during restore', {
|
|
674
|
+
operation: 'restoreVerify',
|
|
675
|
+
statusCode: 422,
|
|
676
|
+
retriable: false,
|
|
677
|
+
suggestion: 'Recreate the backup to generate a fresh checksum or disable verification temporarily.',
|
|
678
|
+
metadata: { backupId, expectedChecksum: backup.checksum, actualChecksum }
|
|
679
|
+
});
|
|
608
680
|
}
|
|
609
681
|
}
|
|
610
682
|
|
|
@@ -674,15 +746,33 @@ export class BackupPlugin extends Plugin {
|
|
|
674
746
|
try {
|
|
675
747
|
archive = JSON.parse(archiveData);
|
|
676
748
|
} catch (parseError) {
|
|
677
|
-
throw
|
|
749
|
+
throw this.createError(`Failed to parse backup archive: ${parseError.message}`, {
|
|
750
|
+
operation: 'restoreParse',
|
|
751
|
+
statusCode: 400,
|
|
752
|
+
retriable: false,
|
|
753
|
+
suggestion: 'Verify the backup file is intact or recreate the backup before restoring.',
|
|
754
|
+
metadata: { backupPath }
|
|
755
|
+
});
|
|
678
756
|
}
|
|
679
757
|
|
|
680
758
|
if (!archive || typeof archive !== 'object') {
|
|
681
|
-
throw
|
|
759
|
+
throw this.createError('Invalid backup archive: not a valid JSON object', {
|
|
760
|
+
operation: 'restoreParse',
|
|
761
|
+
statusCode: 400,
|
|
762
|
+
retriable: false,
|
|
763
|
+
suggestion: 'Ensure the uploaded archive has JSON content and is not truncated.',
|
|
764
|
+
metadata: { backupPath }
|
|
765
|
+
});
|
|
682
766
|
}
|
|
683
767
|
|
|
684
768
|
if (!archive.version || !archive.files) {
|
|
685
|
-
throw
|
|
769
|
+
throw this.createError('Invalid backup archive format: missing version or files array', {
|
|
770
|
+
operation: 'restoreParse',
|
|
771
|
+
statusCode: 400,
|
|
772
|
+
retriable: false,
|
|
773
|
+
suggestion: 'Generate backups with the current plugin version to include version and files metadata.',
|
|
774
|
+
metadata: { backupPath }
|
|
775
|
+
});
|
|
686
776
|
}
|
|
687
777
|
|
|
688
778
|
if (this.config.verbose) {
|
|
@@ -786,7 +876,13 @@ export class BackupPlugin extends Plugin {
|
|
|
786
876
|
if (this.config.verbose) {
|
|
787
877
|
console.error(`[BackupPlugin] Error restoring backup: ${error.message}`);
|
|
788
878
|
}
|
|
789
|
-
throw
|
|
879
|
+
throw this.createError(`Failed to restore backup: ${error.message}`, {
|
|
880
|
+
operation: 'restore',
|
|
881
|
+
statusCode: 500,
|
|
882
|
+
retriable: false,
|
|
883
|
+
suggestion: 'Review the nested error message above and address resource-level failures before retrying.',
|
|
884
|
+
original: error
|
|
885
|
+
});
|
|
790
886
|
}
|
|
791
887
|
}
|
|
792
888
|
|
|
@@ -982,4 +1078,4 @@ export class BackupPlugin extends Plugin {
|
|
|
982
1078
|
await this.driver.cleanup();
|
|
983
1079
|
}
|
|
984
1080
|
}
|
|
985
|
-
}
|
|
1081
|
+
}
|
|
@@ -5,6 +5,7 @@ export class Cache extends EventEmitter {
|
|
|
5
5
|
constructor(config = {}) {
|
|
6
6
|
super();
|
|
7
7
|
this.config = config;
|
|
8
|
+
this._fallbackStore = new Map();
|
|
8
9
|
}
|
|
9
10
|
// to implement:
|
|
10
11
|
async _set (key, data) {}
|
|
@@ -27,22 +28,25 @@ export class Cache extends EventEmitter {
|
|
|
27
28
|
// generic class methods
|
|
28
29
|
async set(key, data) {
|
|
29
30
|
this.validateKey(key);
|
|
31
|
+
this._fallbackStore.set(key, data);
|
|
30
32
|
await this._set(key, data);
|
|
31
|
-
this.emit("set", data);
|
|
33
|
+
this.emit("set", { key, value: data });
|
|
32
34
|
return data
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
async get(key) {
|
|
36
38
|
this.validateKey(key);
|
|
37
39
|
const data = await this._get(key);
|
|
38
|
-
this.
|
|
39
|
-
|
|
40
|
+
const value = data !== undefined ? data : this._fallbackStore.get(key);
|
|
41
|
+
this.emit("fetched", { key, value });
|
|
42
|
+
return value;
|
|
40
43
|
}
|
|
41
44
|
|
|
42
45
|
async del(key) {
|
|
43
46
|
this.validateKey(key);
|
|
44
47
|
const data = await this._del(key);
|
|
45
|
-
this.
|
|
48
|
+
this._fallbackStore.delete(key);
|
|
49
|
+
this.emit("deleted", { key, value: data });
|
|
46
50
|
return data;
|
|
47
51
|
}
|
|
48
52
|
|
|
@@ -52,9 +56,22 @@ export class Cache extends EventEmitter {
|
|
|
52
56
|
|
|
53
57
|
async clear(prefix) {
|
|
54
58
|
const data = await this._clear(prefix);
|
|
55
|
-
|
|
59
|
+
if (!prefix) {
|
|
60
|
+
this._fallbackStore.clear();
|
|
61
|
+
} else {
|
|
62
|
+
for (const key of this._fallbackStore.keys()) {
|
|
63
|
+
if (key.startsWith(prefix)) {
|
|
64
|
+
this._fallbackStore.delete(key);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
this.emit("clear", { prefix, value: data });
|
|
56
69
|
return data;
|
|
57
70
|
}
|
|
71
|
+
|
|
72
|
+
stats() {
|
|
73
|
+
return typeof this.getStats === 'function' ? this.getStats() : {};
|
|
74
|
+
}
|
|
58
75
|
}
|
|
59
76
|
|
|
60
77
|
export default Cache
|
|
@@ -84,6 +84,7 @@ import path from 'path';
|
|
|
84
84
|
import zlib from 'node:zlib';
|
|
85
85
|
import { Cache } from './cache.class.js';
|
|
86
86
|
import tryFn from '../../concerns/try-fn.js';
|
|
87
|
+
import { CacheError } from '../cache.errors.js';
|
|
87
88
|
|
|
88
89
|
export class FilesystemCache extends Cache {
|
|
89
90
|
constructor({
|
|
@@ -112,7 +113,13 @@ export class FilesystemCache extends Cache {
|
|
|
112
113
|
super(config);
|
|
113
114
|
|
|
114
115
|
if (!directory) {
|
|
115
|
-
throw new
|
|
116
|
+
throw new CacheError('FilesystemCache requires a directory', {
|
|
117
|
+
driver: 'filesystem',
|
|
118
|
+
operation: 'constructor',
|
|
119
|
+
statusCode: 400,
|
|
120
|
+
retriable: false,
|
|
121
|
+
suggestion: 'Pass { directory: "./cache" } or configure a valid cache directory before enabling FilesystemCache.'
|
|
122
|
+
});
|
|
116
123
|
}
|
|
117
124
|
|
|
118
125
|
this.directory = path.resolve(directory);
|
|
@@ -147,14 +154,34 @@ export class FilesystemCache extends Cache {
|
|
|
147
154
|
|
|
148
155
|
this.locks = new Map(); // For file locking
|
|
149
156
|
this.cleanupTimer = null;
|
|
150
|
-
|
|
151
|
-
|
|
157
|
+
|
|
158
|
+
// Store _init promise to allow tests to handle initialization errors
|
|
159
|
+
this._initPromise = this._init().catch(err => {
|
|
160
|
+
this._initError = err;
|
|
161
|
+
// Silently capture initialization error - will be thrown on first operation
|
|
162
|
+
});
|
|
152
163
|
}
|
|
153
164
|
|
|
154
165
|
async _init() {
|
|
155
166
|
// Create cache directory if needed
|
|
156
167
|
if (this.createDirectory) {
|
|
157
168
|
await this._ensureDirectory(this.directory);
|
|
169
|
+
} else {
|
|
170
|
+
const [exists] = await tryFn(async () => {
|
|
171
|
+
const stats = await stat(this.directory);
|
|
172
|
+
return stats.isDirectory();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
if (!exists) {
|
|
176
|
+
throw new CacheError(`Cache directory "${this.directory}" does not exist and createDirectory is disabled`, {
|
|
177
|
+
driver: 'filesystem',
|
|
178
|
+
operation: 'init',
|
|
179
|
+
statusCode: 500,
|
|
180
|
+
retriable: false,
|
|
181
|
+
suggestion: 'Create the cache directory manually or enable createDirectory in the FilesystemCache configuration.',
|
|
182
|
+
directory: this.directory
|
|
183
|
+
});
|
|
184
|
+
}
|
|
158
185
|
}
|
|
159
186
|
|
|
160
187
|
// Start cleanup timer if enabled
|
|
@@ -168,12 +195,39 @@ export class FilesystemCache extends Cache {
|
|
|
168
195
|
}
|
|
169
196
|
|
|
170
197
|
async _ensureDirectory(dir) {
|
|
198
|
+
if (!this.createDirectory) {
|
|
199
|
+
const [exists] = await tryFn(async () => {
|
|
200
|
+
const stats = await stat(dir);
|
|
201
|
+
return stats.isDirectory();
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
if (!exists) {
|
|
205
|
+
throw new CacheError(`Cache directory "${dir}" is missing (createDirectory disabled)`, {
|
|
206
|
+
driver: 'filesystem',
|
|
207
|
+
operation: 'ensureDirectory',
|
|
208
|
+
statusCode: 500,
|
|
209
|
+
retriable: false,
|
|
210
|
+
suggestion: 'Create the directory before writing cache entries or enable createDirectory.',
|
|
211
|
+
directory: dir
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
171
217
|
const [ok, err] = await tryFn(async () => {
|
|
172
218
|
await mkdir(dir, { recursive: true });
|
|
173
219
|
});
|
|
174
220
|
|
|
175
221
|
if (!ok && err.code !== 'EEXIST') {
|
|
176
|
-
throw new
|
|
222
|
+
throw new CacheError(`Failed to create cache directory: ${err.message}`, {
|
|
223
|
+
driver: 'filesystem',
|
|
224
|
+
operation: 'ensureDirectory',
|
|
225
|
+
statusCode: 500,
|
|
226
|
+
retriable: false,
|
|
227
|
+
suggestion: 'Check filesystem permissions and ensure the process can create directories.',
|
|
228
|
+
directory: dir,
|
|
229
|
+
original: err
|
|
230
|
+
});
|
|
177
231
|
}
|
|
178
232
|
}
|
|
179
233
|
|
|
@@ -190,43 +244,56 @@ export class FilesystemCache extends Cache {
|
|
|
190
244
|
|
|
191
245
|
async _set(key, data) {
|
|
192
246
|
const filePath = this._getFilePath(key);
|
|
193
|
-
|
|
247
|
+
|
|
194
248
|
try {
|
|
195
249
|
// Prepare data
|
|
196
250
|
let serialized = JSON.stringify(data);
|
|
197
251
|
const originalSize = Buffer.byteLength(serialized, this.encoding);
|
|
198
|
-
|
|
252
|
+
|
|
199
253
|
// Check size limit
|
|
200
254
|
if (originalSize > this.maxFileSize) {
|
|
201
|
-
throw new
|
|
255
|
+
throw new CacheError('Cache data exceeds maximum file size', {
|
|
256
|
+
driver: 'filesystem',
|
|
257
|
+
operation: 'set',
|
|
258
|
+
statusCode: 413,
|
|
259
|
+
retriable: false,
|
|
260
|
+
suggestion: 'Increase maxFileSize or reduce the cached payload size.',
|
|
261
|
+
key,
|
|
262
|
+
size: originalSize,
|
|
263
|
+
maxFileSize: this.maxFileSize
|
|
264
|
+
});
|
|
202
265
|
}
|
|
203
|
-
|
|
266
|
+
|
|
204
267
|
let compressed = false;
|
|
205
268
|
let finalData = serialized;
|
|
206
|
-
|
|
269
|
+
|
|
207
270
|
// Compress if enabled and over threshold
|
|
208
271
|
if (this.enableCompression && originalSize >= this.compressionThreshold) {
|
|
209
272
|
const compressedBuffer = zlib.gzipSync(Buffer.from(serialized, this.encoding));
|
|
210
273
|
finalData = compressedBuffer.toString('base64');
|
|
211
274
|
compressed = true;
|
|
212
275
|
}
|
|
213
|
-
|
|
276
|
+
|
|
277
|
+
// Ensure directory exists before writing
|
|
278
|
+
const dir = path.dirname(filePath);
|
|
279
|
+
await this._ensureDirectory(dir);
|
|
280
|
+
|
|
214
281
|
// Create backup if enabled
|
|
215
282
|
if (this.enableBackup && await this._fileExists(filePath)) {
|
|
216
283
|
const backupPath = filePath + this.backupSuffix;
|
|
217
284
|
await this._copyFile(filePath, backupPath);
|
|
218
285
|
}
|
|
219
|
-
|
|
286
|
+
|
|
220
287
|
// Acquire lock if enabled
|
|
221
288
|
if (this.enableLocking) {
|
|
222
289
|
await this._acquireLock(filePath);
|
|
223
290
|
}
|
|
224
|
-
|
|
291
|
+
|
|
225
292
|
try {
|
|
226
293
|
// Write data
|
|
227
|
-
await writeFile(filePath, finalData, {
|
|
294
|
+
await writeFile(filePath, finalData, {
|
|
228
295
|
encoding: compressed ? 'utf8' : this.encoding,
|
|
229
|
-
mode: this.fileMode
|
|
296
|
+
mode: this.fileMode
|
|
230
297
|
});
|
|
231
298
|
|
|
232
299
|
// Write metadata if enabled
|
|
@@ -270,7 +337,15 @@ export class FilesystemCache extends Cache {
|
|
|
270
337
|
if (this.enableStats) {
|
|
271
338
|
this.stats.errors++;
|
|
272
339
|
}
|
|
273
|
-
throw new
|
|
340
|
+
throw new CacheError(`Failed to set cache key '${key}': ${error.message}`, {
|
|
341
|
+
driver: 'filesystem',
|
|
342
|
+
operation: 'set',
|
|
343
|
+
statusCode: 500,
|
|
344
|
+
retriable: false,
|
|
345
|
+
suggestion: 'Verify filesystem permissions and available disk space.',
|
|
346
|
+
key,
|
|
347
|
+
original: error
|
|
348
|
+
});
|
|
274
349
|
}
|
|
275
350
|
}
|
|
276
351
|
|
|
@@ -422,7 +497,15 @@ export class FilesystemCache extends Cache {
|
|
|
422
497
|
if (this.enableStats) {
|
|
423
498
|
this.stats.errors++;
|
|
424
499
|
}
|
|
425
|
-
throw new
|
|
500
|
+
throw new CacheError(`Failed to delete cache key '${key}': ${error.message}`, {
|
|
501
|
+
driver: 'filesystem',
|
|
502
|
+
operation: 'delete',
|
|
503
|
+
statusCode: 500,
|
|
504
|
+
retriable: false,
|
|
505
|
+
suggestion: 'Ensure cache files are writable and not locked by another process.',
|
|
506
|
+
key,
|
|
507
|
+
original: error
|
|
508
|
+
});
|
|
426
509
|
}
|
|
427
510
|
}
|
|
428
511
|
|
|
@@ -522,7 +605,14 @@ export class FilesystemCache extends Cache {
|
|
|
522
605
|
if (this.enableStats) {
|
|
523
606
|
this.stats.errors++;
|
|
524
607
|
}
|
|
525
|
-
throw new
|
|
608
|
+
throw new CacheError(`Failed to clear cache: ${error.message}`, {
|
|
609
|
+
driver: 'filesystem',
|
|
610
|
+
operation: 'clear',
|
|
611
|
+
statusCode: 500,
|
|
612
|
+
retriable: false,
|
|
613
|
+
suggestion: 'Verify the cache directory is accessible and not in use by another process.',
|
|
614
|
+
original: error
|
|
615
|
+
});
|
|
526
616
|
}
|
|
527
617
|
}
|
|
528
618
|
|
|
@@ -633,7 +723,14 @@ export class FilesystemCache extends Cache {
|
|
|
633
723
|
|
|
634
724
|
while (this.locks.has(lockKey)) {
|
|
635
725
|
if (Date.now() - startTime > this.lockTimeout) {
|
|
636
|
-
throw new
|
|
726
|
+
throw new CacheError(`Lock timeout for file: ${filePath}`, {
|
|
727
|
+
driver: 'filesystem',
|
|
728
|
+
operation: 'acquireLock',
|
|
729
|
+
statusCode: 408,
|
|
730
|
+
retriable: true,
|
|
731
|
+
suggestion: 'Increase lockTimeout or investigate long-running cache writes holding the lock.',
|
|
732
|
+
key: lockKey
|
|
733
|
+
});
|
|
637
734
|
}
|
|
638
735
|
await new Promise(resolve => setTimeout(resolve, 10));
|
|
639
736
|
}
|
|
@@ -689,4 +786,4 @@ export class FilesystemCache extends Cache {
|
|
|
689
786
|
}
|
|
690
787
|
}
|
|
691
788
|
|
|
692
|
-
export default FilesystemCache;
|
|
789
|
+
export default FilesystemCache;
|