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
@@ -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 new Error('BackupPlugin: Encryption requires both key and algorithm');
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 new Error('BackupPlugin: Invalid compression type. Use: none, gzip, brotli, deflate');
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 new Error(`Backup '${backupId}' is already in progress`);
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 new Error('No resources were exported for backup');
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 new Error('Backup verification failed');
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 new Error(`Failed to generate checksum for ${filePath}: ${err?.message}`);
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 new Error(`Backup '${backupId}' not found`);
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 new Error(`Backup '${backupId}' is not in completed status`);
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 new Error('Backup verification failed during restore');
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 new Error(`Failed to parse backup archive: ${parseError.message}`);
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 new Error('Invalid backup archive: not a valid JSON object');
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 new Error('Invalid backup archive format: missing version or files array');
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 new Error(`Failed to restore backup: ${error.message}`);
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.emit("fetched", data);
39
- return data;
40
+ const value = data !== undefined ? data : this._fallbackStore.get(key);
41
+ this.emit("fetched", { key, value });
42
+ return value;
40
43
  }
41
44
 
42
45
  async del(key) {
43
46
  this.validateKey(key);
44
47
  const data = await this._del(key);
45
- this.emit("deleted", data);
48
+ this._fallbackStore.delete(key);
49
+ this.emit("deleted", { key, value: data });
46
50
  return data;
47
51
  }
48
52
 
@@ -52,9 +56,22 @@ export class Cache extends EventEmitter {
52
56
 
53
57
  async clear(prefix) {
54
58
  const data = await this._clear(prefix);
55
- this.emit("clear", data);
59
+ if (!prefix) {
60
+ this._fallbackStore.clear();
61
+ } else {
62
+ for (const key of this._fallbackStore.keys()) {
63
+ if (key.startsWith(prefix)) {
64
+ this._fallbackStore.delete(key);
65
+ }
66
+ }
67
+ }
68
+ this.emit("clear", { prefix, value: data });
56
69
  return data;
57
70
  }
71
+
72
+ stats() {
73
+ return typeof this.getStats === 'function' ? this.getStats() : {};
74
+ }
58
75
  }
59
76
 
60
77
  export default Cache
@@ -84,6 +84,7 @@ import path from 'path';
84
84
  import zlib from 'node:zlib';
85
85
  import { Cache } from './cache.class.js';
86
86
  import tryFn from '../../concerns/try-fn.js';
87
+ import { CacheError } from '../cache.errors.js';
87
88
 
88
89
  export class FilesystemCache extends Cache {
89
90
  constructor({
@@ -112,7 +113,13 @@ export class FilesystemCache extends Cache {
112
113
  super(config);
113
114
 
114
115
  if (!directory) {
115
- throw new Error('FilesystemCache: directory parameter is required');
116
+ throw new CacheError('FilesystemCache requires a directory', {
117
+ driver: 'filesystem',
118
+ operation: 'constructor',
119
+ statusCode: 400,
120
+ retriable: false,
121
+ suggestion: 'Pass { directory: "./cache" } or configure a valid cache directory before enabling FilesystemCache.'
122
+ });
116
123
  }
117
124
 
118
125
  this.directory = path.resolve(directory);
@@ -147,14 +154,34 @@ export class FilesystemCache extends Cache {
147
154
 
148
155
  this.locks = new Map(); // For file locking
149
156
  this.cleanupTimer = null;
150
-
151
- this._init();
157
+
158
+ // Store _init promise to allow tests to handle initialization errors
159
+ this._initPromise = this._init().catch(err => {
160
+ this._initError = err;
161
+ // Silently capture initialization error - will be thrown on first operation
162
+ });
152
163
  }
153
164
 
154
165
  async _init() {
155
166
  // Create cache directory if needed
156
167
  if (this.createDirectory) {
157
168
  await this._ensureDirectory(this.directory);
169
+ } else {
170
+ const [exists] = await tryFn(async () => {
171
+ const stats = await stat(this.directory);
172
+ return stats.isDirectory();
173
+ });
174
+
175
+ if (!exists) {
176
+ throw new CacheError(`Cache directory "${this.directory}" does not exist and createDirectory is disabled`, {
177
+ driver: 'filesystem',
178
+ operation: 'init',
179
+ statusCode: 500,
180
+ retriable: false,
181
+ suggestion: 'Create the cache directory manually or enable createDirectory in the FilesystemCache configuration.',
182
+ directory: this.directory
183
+ });
184
+ }
158
185
  }
159
186
 
160
187
  // Start cleanup timer if enabled
@@ -168,12 +195,39 @@ export class FilesystemCache extends Cache {
168
195
  }
169
196
 
170
197
  async _ensureDirectory(dir) {
198
+ if (!this.createDirectory) {
199
+ const [exists] = await tryFn(async () => {
200
+ const stats = await stat(dir);
201
+ return stats.isDirectory();
202
+ });
203
+
204
+ if (!exists) {
205
+ throw new CacheError(`Cache directory "${dir}" is missing (createDirectory disabled)`, {
206
+ driver: 'filesystem',
207
+ operation: 'ensureDirectory',
208
+ statusCode: 500,
209
+ retriable: false,
210
+ suggestion: 'Create the directory before writing cache entries or enable createDirectory.',
211
+ directory: dir
212
+ });
213
+ }
214
+ return;
215
+ }
216
+
171
217
  const [ok, err] = await tryFn(async () => {
172
218
  await mkdir(dir, { recursive: true });
173
219
  });
174
220
 
175
221
  if (!ok && err.code !== 'EEXIST') {
176
- throw new Error(`Failed to create cache directory: ${err.message}`);
222
+ throw new CacheError(`Failed to create cache directory: ${err.message}`, {
223
+ driver: 'filesystem',
224
+ operation: 'ensureDirectory',
225
+ statusCode: 500,
226
+ retriable: false,
227
+ suggestion: 'Check filesystem permissions and ensure the process can create directories.',
228
+ directory: dir,
229
+ original: err
230
+ });
177
231
  }
178
232
  }
179
233
 
@@ -190,43 +244,56 @@ export class FilesystemCache extends Cache {
190
244
 
191
245
  async _set(key, data) {
192
246
  const filePath = this._getFilePath(key);
193
-
247
+
194
248
  try {
195
249
  // Prepare data
196
250
  let serialized = JSON.stringify(data);
197
251
  const originalSize = Buffer.byteLength(serialized, this.encoding);
198
-
252
+
199
253
  // Check size limit
200
254
  if (originalSize > this.maxFileSize) {
201
- throw new Error(`Cache data exceeds maximum file size: ${originalSize} > ${this.maxFileSize}`);
255
+ throw new CacheError('Cache data exceeds maximum file size', {
256
+ driver: 'filesystem',
257
+ operation: 'set',
258
+ statusCode: 413,
259
+ retriable: false,
260
+ suggestion: 'Increase maxFileSize or reduce the cached payload size.',
261
+ key,
262
+ size: originalSize,
263
+ maxFileSize: this.maxFileSize
264
+ });
202
265
  }
203
-
266
+
204
267
  let compressed = false;
205
268
  let finalData = serialized;
206
-
269
+
207
270
  // Compress if enabled and over threshold
208
271
  if (this.enableCompression && originalSize >= this.compressionThreshold) {
209
272
  const compressedBuffer = zlib.gzipSync(Buffer.from(serialized, this.encoding));
210
273
  finalData = compressedBuffer.toString('base64');
211
274
  compressed = true;
212
275
  }
213
-
276
+
277
+ // Ensure directory exists before writing
278
+ const dir = path.dirname(filePath);
279
+ await this._ensureDirectory(dir);
280
+
214
281
  // Create backup if enabled
215
282
  if (this.enableBackup && await this._fileExists(filePath)) {
216
283
  const backupPath = filePath + this.backupSuffix;
217
284
  await this._copyFile(filePath, backupPath);
218
285
  }
219
-
286
+
220
287
  // Acquire lock if enabled
221
288
  if (this.enableLocking) {
222
289
  await this._acquireLock(filePath);
223
290
  }
224
-
291
+
225
292
  try {
226
293
  // Write data
227
- await writeFile(filePath, finalData, {
294
+ await writeFile(filePath, finalData, {
228
295
  encoding: compressed ? 'utf8' : this.encoding,
229
- mode: this.fileMode
296
+ mode: this.fileMode
230
297
  });
231
298
 
232
299
  // Write metadata if enabled
@@ -270,7 +337,15 @@ export class FilesystemCache extends Cache {
270
337
  if (this.enableStats) {
271
338
  this.stats.errors++;
272
339
  }
273
- throw new Error(`Failed to set cache key '${key}': ${error.message}`);
340
+ throw new CacheError(`Failed to set cache key '${key}': ${error.message}`, {
341
+ driver: 'filesystem',
342
+ operation: 'set',
343
+ statusCode: 500,
344
+ retriable: false,
345
+ suggestion: 'Verify filesystem permissions and available disk space.',
346
+ key,
347
+ original: error
348
+ });
274
349
  }
275
350
  }
276
351
 
@@ -422,7 +497,15 @@ export class FilesystemCache extends Cache {
422
497
  if (this.enableStats) {
423
498
  this.stats.errors++;
424
499
  }
425
- throw new Error(`Failed to delete cache key '${key}': ${error.message}`);
500
+ throw new CacheError(`Failed to delete cache key '${key}': ${error.message}`, {
501
+ driver: 'filesystem',
502
+ operation: 'delete',
503
+ statusCode: 500,
504
+ retriable: false,
505
+ suggestion: 'Ensure cache files are writable and not locked by another process.',
506
+ key,
507
+ original: error
508
+ });
426
509
  }
427
510
  }
428
511
 
@@ -522,7 +605,14 @@ export class FilesystemCache extends Cache {
522
605
  if (this.enableStats) {
523
606
  this.stats.errors++;
524
607
  }
525
- throw new Error(`Failed to clear cache: ${error.message}`);
608
+ throw new CacheError(`Failed to clear cache: ${error.message}`, {
609
+ driver: 'filesystem',
610
+ operation: 'clear',
611
+ statusCode: 500,
612
+ retriable: false,
613
+ suggestion: 'Verify the cache directory is accessible and not in use by another process.',
614
+ original: error
615
+ });
526
616
  }
527
617
  }
528
618
 
@@ -633,7 +723,14 @@ export class FilesystemCache extends Cache {
633
723
 
634
724
  while (this.locks.has(lockKey)) {
635
725
  if (Date.now() - startTime > this.lockTimeout) {
636
- throw new Error(`Lock timeout for file: ${filePath}`);
726
+ throw new CacheError(`Lock timeout for file: ${filePath}`, {
727
+ driver: 'filesystem',
728
+ operation: 'acquireLock',
729
+ statusCode: 408,
730
+ retriable: true,
731
+ suggestion: 'Increase lockTimeout or investigate long-running cache writes holding the lock.',
732
+ key: lockKey
733
+ });
637
734
  }
638
735
  await new Promise(resolve => setTimeout(resolve, 10));
639
736
  }
@@ -689,4 +786,4 @@ export class FilesystemCache extends Cache {
689
786
  }
690
787
  }
691
788
 
692
- export default FilesystemCache;
789
+ export default FilesystemCache;