s3db.js 13.6.1 → 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 (189) hide show
  1. package/README.md +56 -15
  2. package/dist/s3db.cjs +72446 -39022
  3. package/dist/s3db.cjs.map +1 -1
  4. package/dist/s3db.es.js +72172 -38790
  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 +85 -50
  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/route-context.js +601 -0
  34. package/src/plugins/api/index.js +168 -40
  35. package/src/plugins/api/routes/auth-routes.js +198 -30
  36. package/src/plugins/api/routes/resource-routes.js +19 -4
  37. package/src/plugins/api/server/health-manager.class.js +163 -0
  38. package/src/plugins/api/server/middleware-chain.class.js +310 -0
  39. package/src/plugins/api/server/router.class.js +472 -0
  40. package/src/plugins/api/server.js +280 -1303
  41. package/src/plugins/api/utils/custom-routes.js +17 -5
  42. package/src/plugins/api/utils/guards.js +76 -17
  43. package/src/plugins/api/utils/openapi-generator-cached.class.js +133 -0
  44. package/src/plugins/api/utils/openapi-generator.js +7 -6
  45. package/src/plugins/audit.plugin.js +30 -8
  46. package/src/plugins/backup.plugin.js +110 -14
  47. package/src/plugins/cache/cache.class.js +22 -5
  48. package/src/plugins/cache/filesystem-cache.class.js +116 -19
  49. package/src/plugins/cache/memory-cache.class.js +211 -57
  50. package/src/plugins/cache/multi-tier-cache.class.js +371 -0
  51. package/src/plugins/cache/partition-aware-filesystem-cache.class.js +168 -47
  52. package/src/plugins/cache/redis-cache.class.js +552 -0
  53. package/src/plugins/cache/s3-cache.class.js +17 -8
  54. package/src/plugins/cache.plugin.js +176 -61
  55. package/src/plugins/cloud-inventory/drivers/alibaba-driver.js +8 -1
  56. package/src/plugins/cloud-inventory/drivers/aws-driver.js +60 -29
  57. package/src/plugins/cloud-inventory/drivers/azure-driver.js +8 -1
  58. package/src/plugins/cloud-inventory/drivers/base-driver.js +16 -2
  59. package/src/plugins/cloud-inventory/drivers/cloudflare-driver.js +8 -1
  60. package/src/plugins/cloud-inventory/drivers/digitalocean-driver.js +8 -1
  61. package/src/plugins/cloud-inventory/drivers/hetzner-driver.js +8 -1
  62. package/src/plugins/cloud-inventory/drivers/linode-driver.js +8 -1
  63. package/src/plugins/cloud-inventory/drivers/mongodb-atlas-driver.js +8 -1
  64. package/src/plugins/cloud-inventory/drivers/vultr-driver.js +8 -1
  65. package/src/plugins/cloud-inventory/index.js +29 -8
  66. package/src/plugins/cloud-inventory/registry.js +64 -42
  67. package/src/plugins/cloud-inventory.plugin.js +240 -138
  68. package/src/plugins/concerns/plugin-dependencies.js +54 -0
  69. package/src/plugins/concerns/resource-names.js +100 -0
  70. package/src/plugins/consumers/index.js +10 -2
  71. package/src/plugins/consumers/sqs-consumer.js +12 -2
  72. package/src/plugins/cookie-farm-suite.plugin.js +278 -0
  73. package/src/plugins/cookie-farm.errors.js +73 -0
  74. package/src/plugins/cookie-farm.plugin.js +869 -0
  75. package/src/plugins/costs.plugin.js +7 -1
  76. package/src/plugins/eventual-consistency/analytics.js +94 -19
  77. package/src/plugins/eventual-consistency/config.js +15 -7
  78. package/src/plugins/eventual-consistency/consolidation.js +29 -11
  79. package/src/plugins/eventual-consistency/garbage-collection.js +3 -1
  80. package/src/plugins/eventual-consistency/helpers.js +39 -14
  81. package/src/plugins/eventual-consistency/install.js +21 -2
  82. package/src/plugins/eventual-consistency/utils.js +32 -10
  83. package/src/plugins/fulltext.plugin.js +38 -11
  84. package/src/plugins/geo.plugin.js +61 -9
  85. package/src/plugins/identity/concerns/config.js +61 -0
  86. package/src/plugins/identity/concerns/mfa-manager.js +15 -2
  87. package/src/plugins/identity/concerns/rate-limit.js +124 -0
  88. package/src/plugins/identity/concerns/resource-schemas.js +9 -1
  89. package/src/plugins/identity/concerns/token-generator.js +29 -4
  90. package/src/plugins/identity/drivers/auth-driver.interface.js +76 -0
  91. package/src/plugins/identity/drivers/client-credentials-driver.js +127 -0
  92. package/src/plugins/identity/drivers/index.js +18 -0
  93. package/src/plugins/identity/drivers/password-driver.js +122 -0
  94. package/src/plugins/identity/email-service.js +17 -2
  95. package/src/plugins/identity/index.js +413 -69
  96. package/src/plugins/identity/oauth2-server.js +413 -30
  97. package/src/plugins/identity/oidc-discovery.js +16 -8
  98. package/src/plugins/identity/rsa-keys.js +115 -35
  99. package/src/plugins/identity/server.js +166 -45
  100. package/src/plugins/identity/session-manager.js +53 -7
  101. package/src/plugins/identity/ui/pages/mfa-verification.js +17 -15
  102. package/src/plugins/identity/ui/routes.js +363 -255
  103. package/src/plugins/importer/index.js +153 -20
  104. package/src/plugins/index.js +9 -2
  105. package/src/plugins/kubernetes-inventory/index.js +6 -0
  106. package/src/plugins/kubernetes-inventory/k8s-driver.js +867 -0
  107. package/src/plugins/kubernetes-inventory/resource-types.js +274 -0
  108. package/src/plugins/kubernetes-inventory.plugin.js +980 -0
  109. package/src/plugins/metrics.plugin.js +64 -16
  110. package/src/plugins/ml/base-model.class.js +25 -15
  111. package/src/plugins/ml/regression-model.class.js +1 -1
  112. package/src/plugins/ml.errors.js +57 -25
  113. package/src/plugins/ml.plugin.js +28 -4
  114. package/src/plugins/namespace.js +210 -0
  115. package/src/plugins/plugin.class.js +180 -8
  116. package/src/plugins/puppeteer/console-monitor.js +729 -0
  117. package/src/plugins/puppeteer/cookie-manager.js +492 -0
  118. package/src/plugins/puppeteer/network-monitor.js +816 -0
  119. package/src/plugins/puppeteer/performance-manager.js +746 -0
  120. package/src/plugins/puppeteer/proxy-manager.js +478 -0
  121. package/src/plugins/puppeteer/stealth-manager.js +556 -0
  122. package/src/plugins/puppeteer.errors.js +81 -0
  123. package/src/plugins/puppeteer.plugin.js +1327 -0
  124. package/src/plugins/queue-consumer.plugin.js +69 -14
  125. package/src/plugins/recon/behaviors/uptime-behavior.js +691 -0
  126. package/src/plugins/recon/concerns/command-runner.js +148 -0
  127. package/src/plugins/recon/concerns/diff-detector.js +372 -0
  128. package/src/plugins/recon/concerns/fingerprint-builder.js +307 -0
  129. package/src/plugins/recon/concerns/process-manager.js +338 -0
  130. package/src/plugins/recon/concerns/report-generator.js +478 -0
  131. package/src/plugins/recon/concerns/security-analyzer.js +571 -0
  132. package/src/plugins/recon/concerns/target-normalizer.js +68 -0
  133. package/src/plugins/recon/config/defaults.js +321 -0
  134. package/src/plugins/recon/config/resources.js +370 -0
  135. package/src/plugins/recon/index.js +778 -0
  136. package/src/plugins/recon/managers/dependency-manager.js +174 -0
  137. package/src/plugins/recon/managers/scheduler-manager.js +179 -0
  138. package/src/plugins/recon/managers/storage-manager.js +745 -0
  139. package/src/plugins/recon/managers/target-manager.js +274 -0
  140. package/src/plugins/recon/stages/asn-stage.js +314 -0
  141. package/src/plugins/recon/stages/certificate-stage.js +84 -0
  142. package/src/plugins/recon/stages/dns-stage.js +107 -0
  143. package/src/plugins/recon/stages/dnsdumpster-stage.js +362 -0
  144. package/src/plugins/recon/stages/fingerprint-stage.js +71 -0
  145. package/src/plugins/recon/stages/google-dorks-stage.js +440 -0
  146. package/src/plugins/recon/stages/http-stage.js +89 -0
  147. package/src/plugins/recon/stages/latency-stage.js +148 -0
  148. package/src/plugins/recon/stages/massdns-stage.js +302 -0
  149. package/src/plugins/recon/stages/osint-stage.js +1373 -0
  150. package/src/plugins/recon/stages/ports-stage.js +169 -0
  151. package/src/plugins/recon/stages/screenshot-stage.js +94 -0
  152. package/src/plugins/recon/stages/secrets-stage.js +514 -0
  153. package/src/plugins/recon/stages/subdomains-stage.js +295 -0
  154. package/src/plugins/recon/stages/tls-audit-stage.js +78 -0
  155. package/src/plugins/recon/stages/vulnerability-stage.js +78 -0
  156. package/src/plugins/recon/stages/web-discovery-stage.js +113 -0
  157. package/src/plugins/recon/stages/whois-stage.js +349 -0
  158. package/src/plugins/recon.plugin.js +75 -0
  159. package/src/plugins/recon.plugin.js.backup +2635 -0
  160. package/src/plugins/relation.errors.js +87 -14
  161. package/src/plugins/replicator.plugin.js +514 -137
  162. package/src/plugins/replicators/base-replicator.class.js +89 -1
  163. package/src/plugins/replicators/bigquery-replicator.class.js +66 -22
  164. package/src/plugins/replicators/dynamodb-replicator.class.js +22 -15
  165. package/src/plugins/replicators/mongodb-replicator.class.js +22 -15
  166. package/src/plugins/replicators/mysql-replicator.class.js +52 -17
  167. package/src/plugins/replicators/planetscale-replicator.class.js +30 -4
  168. package/src/plugins/replicators/postgres-replicator.class.js +62 -27
  169. package/src/plugins/replicators/s3db-replicator.class.js +25 -18
  170. package/src/plugins/replicators/schema-sync.helper.js +3 -3
  171. package/src/plugins/replicators/sqs-replicator.class.js +8 -2
  172. package/src/plugins/replicators/turso-replicator.class.js +23 -3
  173. package/src/plugins/replicators/webhook-replicator.class.js +42 -4
  174. package/src/plugins/s3-queue.plugin.js +464 -65
  175. package/src/plugins/scheduler.plugin.js +20 -6
  176. package/src/plugins/state-machine.plugin.js +40 -9
  177. package/src/plugins/tfstate/base-driver.js +28 -4
  178. package/src/plugins/tfstate/errors.js +65 -10
  179. package/src/plugins/tfstate/filesystem-driver.js +52 -8
  180. package/src/plugins/tfstate/index.js +163 -90
  181. package/src/plugins/tfstate/s3-driver.js +64 -6
  182. package/src/plugins/ttl.plugin.js +72 -17
  183. package/src/plugins/vector/distances.js +18 -12
  184. package/src/plugins/vector/kmeans.js +26 -4
  185. package/src/resource.class.js +115 -19
  186. package/src/testing/factory.class.js +20 -3
  187. package/src/testing/seeder.class.js +7 -1
  188. package/src/clients/memory-client.md +0 -917
  189. package/src/plugins/cloud-inventory/drivers/mock-drivers.js +0 -449
@@ -575,10 +575,34 @@
575
575
 
576
576
  import { Plugin } from "./plugin.class.js";
577
577
  import tryFn from "../concerns/try-fn.js";
578
+ import { resolveResourceNames } from "./concerns/resource-names.js";
579
+ import { PluginError } from '../errors.js';
578
580
 
579
581
  export class MetricsPlugin extends Plugin {
580
582
  constructor(options = {}) {
581
583
  super();
584
+ const resourceNamesOption = options.resourceNames || {};
585
+ const legacyResourceOption = options.resources || {};
586
+ const resourceOverrides = {
587
+ metrics: resourceNamesOption.metrics ?? legacyResourceOption.metrics,
588
+ errors: resourceNamesOption.errors ?? legacyResourceOption.errors,
589
+ performance: resourceNamesOption.performance ?? legacyResourceOption.performance
590
+ };
591
+ this._resourceDescriptors = {
592
+ metrics: {
593
+ defaultName: 'plg_metrics',
594
+ override: resourceOverrides.metrics
595
+ },
596
+ errors: {
597
+ defaultName: 'plg_metrics_errors',
598
+ override: resourceOverrides.errors
599
+ },
600
+ performance: {
601
+ defaultName: 'plg_metrics_performance',
602
+ override: resourceOverrides.performance
603
+ }
604
+ };
605
+ this.resourceNames = this._resolveResourceNames();
582
606
  this.config = {
583
607
  collectPerformance: options.collectPerformance !== false,
584
608
  collectErrors: options.collectErrors !== false,
@@ -617,12 +641,22 @@ export class MetricsPlugin extends Plugin {
617
641
  this.metricsServer = null; // Standalone HTTP server (if needed)
618
642
  }
619
643
 
644
+ _resolveResourceNames() {
645
+ return resolveResourceNames('metrics', this._resourceDescriptors, {
646
+ namespace: this.namespace
647
+ });
648
+ }
649
+
650
+ onNamespaceChanged() {
651
+ this.resourceNames = this._resolveResourceNames();
652
+ }
653
+
620
654
  async onInstall() {
621
655
  if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') return;
622
656
 
623
657
  const [ok, err] = await tryFn(async () => {
624
658
  const [ok1, err1, metricsResource] = await tryFn(() => this.database.createResource({
625
- name: 'plg_metrics',
659
+ name: this.resourceNames.metrics,
626
660
  attributes: {
627
661
  id: 'string|required',
628
662
  type: 'string|required', // 'operation', 'error', 'performance'
@@ -641,10 +675,12 @@ export class MetricsPlugin extends Plugin {
641
675
  },
642
676
  behavior: 'body-overflow'
643
677
  }));
644
- this.metricsResource = ok1 ? metricsResource : this.database.resources.plg_metrics;
678
+ this.metricsResource = ok1
679
+ ? metricsResource
680
+ : this.database.resources[this.resourceNames.metrics];
645
681
 
646
682
  const [ok2, err2, errorsResource] = await tryFn(() => this.database.createResource({
647
- name: 'plg_error_logs',
683
+ name: this.resourceNames.errors,
648
684
  attributes: {
649
685
  id: 'string|required',
650
686
  resourceName: 'string|required',
@@ -659,10 +695,12 @@ export class MetricsPlugin extends Plugin {
659
695
  },
660
696
  behavior: 'body-overflow'
661
697
  }));
662
- this.errorsResource = ok2 ? errorsResource : this.database.resources.plg_error_logs;
698
+ this.errorsResource = ok2
699
+ ? errorsResource
700
+ : this.database.resources[this.resourceNames.errors];
663
701
 
664
702
  const [ok3, err3, performanceResource] = await tryFn(() => this.database.createResource({
665
- name: 'plg_performance_logs',
703
+ name: this.resourceNames.performance,
666
704
  attributes: {
667
705
  id: 'string|required',
668
706
  resourceName: 'string|required',
@@ -677,13 +715,15 @@ export class MetricsPlugin extends Plugin {
677
715
  },
678
716
  behavior: 'body-overflow'
679
717
  }));
680
- this.performanceResource = ok3 ? performanceResource : this.database.resources.plg_performance_logs;
718
+ this.performanceResource = ok3
719
+ ? performanceResource
720
+ : this.database.resources[this.resourceNames.performance];
681
721
  });
682
722
  if (!ok) {
683
723
  // Resources might already exist
684
- this.metricsResource = this.database.resources.plg_metrics;
685
- this.errorsResource = this.database.resources.plg_error_logs;
686
- this.performanceResource = this.database.resources.plg_performance_logs;
724
+ this.metricsResource = this.database.resources[this.resourceNames.metrics];
725
+ this.errorsResource = this.database.resources[this.resourceNames.errors];
726
+ this.performanceResource = this.database.resources[this.resourceNames.performance];
687
727
  }
688
728
 
689
729
  // Use database hooks for automatic resource discovery
@@ -728,7 +768,7 @@ export class MetricsPlugin extends Plugin {
728
768
  installDatabaseHooks() {
729
769
  // Use the new database hooks system for automatic resource discovery
730
770
  this.database.addHook('afterCreateResource', (resource) => {
731
- if (resource.name !== 'plg_metrics' && resource.name !== 'plg_error_logs' && resource.name !== 'plg_performance_logs') {
771
+ if (!this.isInternalResource(resource.name)) {
732
772
  this.installResourceHooks(resource);
733
773
  }
734
774
  });
@@ -739,10 +779,14 @@ export class MetricsPlugin extends Plugin {
739
779
  this.database.removeHook('afterCreateResource', this.installResourceHooks.bind(this));
740
780
  }
741
781
 
782
+ isInternalResource(resourceName) {
783
+ return Object.values(this.resourceNames).includes(resourceName);
784
+ }
785
+
742
786
  installMetricsHooks() {
743
787
  // Only hook into non-metrics resources
744
788
  for (const resource of Object.values(this.database.resources)) {
745
- if (['plg_metrics', 'plg_error_logs', 'plg_performance_logs'].includes(resource.name)) {
789
+ if (this.isInternalResource(resource.name)) {
746
790
  continue; // Skip metrics resources to avoid recursion
747
791
  }
748
792
 
@@ -753,7 +797,7 @@ export class MetricsPlugin extends Plugin {
753
797
  this.database._createResource = this.database.createResource;
754
798
  this.database.createResource = async function (...args) {
755
799
  const resource = await this._createResource(...args);
756
- if (this.plugins?.metrics && !['plg_metrics', 'plg_error_logs', 'plg_performance_logs'].includes(resource.name)) {
800
+ if (this.plugins?.metrics && !this.plugins.metrics.isInternalResource(resource.name)) {
757
801
  this.plugins.metrics.installResourceHooks(resource);
758
802
  }
759
803
  return resource;
@@ -1305,9 +1349,13 @@ export class MetricsPlugin extends Plugin {
1305
1349
  // INTEGRATED mode: requires API Plugin
1306
1350
  else if (mode === 'integrated') {
1307
1351
  if (!apiPlugin || !apiPlugin.server) {
1308
- throw new Error(
1309
- '[Metrics Plugin] prometheus.mode=integrated requires API Plugin to be active'
1310
- );
1352
+ throw new PluginError('[Metrics Plugin] prometheus.mode=integrated requires API Plugin to be active', {
1353
+ pluginName: 'MetricsPlugin',
1354
+ operation: '_setupPrometheusExporter',
1355
+ statusCode: 400,
1356
+ retriable: false,
1357
+ suggestion: 'Install and start the API plugin or switch prometheus.mode to "standalone" or "auto".'
1358
+ });
1311
1359
  }
1312
1360
  await this._setupIntegratedMetrics(apiPlugin);
1313
1361
  }
@@ -1406,4 +1454,4 @@ export class MetricsPlugin extends Plugin {
1406
1454
  console.error('[Metrics Plugin] Standalone metrics server error:', err);
1407
1455
  });
1408
1456
  }
1409
- }
1457
+ }
@@ -5,6 +5,7 @@
5
5
  * Provides common functionality for training, prediction, and persistence
6
6
  */
7
7
 
8
+ import { createRequire } from 'module';
8
9
  import {
9
10
  TrainingError,
10
11
  PredictionError,
@@ -13,11 +14,20 @@ import {
13
14
  InsufficientDataError,
14
15
  TensorFlowDependencyError
15
16
  } from '../ml.errors.js';
17
+ import { PluginError } from '../../errors.js';
18
+
19
+ const require = createRequire(import.meta.url);
16
20
 
17
21
  export class BaseModel {
18
22
  constructor(config = {}) {
19
23
  if (this.constructor === BaseModel) {
20
- throw new Error('BaseModel is an abstract class and cannot be instantiated directly');
24
+ throw new PluginError('BaseModel is an abstract class and cannot be instantiated directly', {
25
+ pluginName: 'MLPlugin',
26
+ operation: 'baseModel:constructor',
27
+ statusCode: 500,
28
+ retriable: false,
29
+ suggestion: 'Extend BaseModel and instantiate the concrete subclass instead.'
30
+ });
21
31
  }
22
32
 
23
33
  this.config = {
@@ -68,21 +78,15 @@ export class BaseModel {
68
78
  }
69
79
 
70
80
  try {
71
- // Try CommonJS require first (works in most environments)
81
+ // Use CommonJS require with createRequire (works reliably in both Node.js and Jest ESM mode)
82
+ // This avoids TensorFlow.js internal ESM compatibility issues in Jest
72
83
  this.tf = require('@tensorflow/tfjs-node');
73
84
  this._tfValidated = true;
74
- } catch (requireError) {
75
- // If require fails (e.g., Jest VM modules), try dynamic import
76
- try {
77
- const tfModule = await import('@tensorflow/tfjs-node');
78
- this.tf = tfModule.default || tfModule;
79
- this._tfValidated = true;
80
- } catch (importError) {
81
- throw new TensorFlowDependencyError(
82
- 'TensorFlow.js is not installed. Run: pnpm add @tensorflow/tfjs-node',
83
- { originalError: importError.message }
84
- );
85
- }
85
+ } catch (error) {
86
+ throw new TensorFlowDependencyError(
87
+ 'TensorFlow.js is not installed. Run: pnpm add @tensorflow/tfjs-node',
88
+ { originalError: error.message }
89
+ );
86
90
  }
87
91
  }
88
92
 
@@ -92,7 +96,13 @@ export class BaseModel {
92
96
  * @abstract
93
97
  */
94
98
  buildModel() {
95
- throw new Error('buildModel() must be implemented by subclass');
99
+ throw new PluginError('buildModel() must be implemented by subclass', {
100
+ pluginName: 'MLPlugin',
101
+ operation: 'baseModel:buildModel',
102
+ statusCode: 500,
103
+ retriable: false,
104
+ suggestion: 'Override buildModel() in your model subclass to define the underlying ML architecture.'
105
+ });
96
106
  }
97
107
 
98
108
  /**
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import { BaseModel } from './base-model.class.js';
9
- import { ModelConfigError } from '../ml.errors.js';
9
+ import { ModelConfigError, ModelNotTrainedError } from '../ml.errors.js';
10
10
 
11
11
  export class RegressionModel extends BaseModel {
12
12
  constructor(config = {}) {
@@ -1,3 +1,5 @@
1
+ import { PluginError } from '../errors.js';
2
+
1
3
  /**
2
4
  * Machine Learning Plugin Errors
3
5
  *
@@ -7,25 +9,18 @@
7
9
  /**
8
10
  * Base ML Error
9
11
  */
10
- export class MLError extends Error {
12
+ export class MLError extends PluginError {
11
13
  constructor(message, context = {}) {
12
- super(message);
13
- this.name = 'MLError';
14
- this.context = context;
15
-
16
- // Capture stack trace
17
- if (Error.captureStackTrace) {
18
- Error.captureStackTrace(this, this.constructor);
19
- }
20
- }
21
-
22
- toJSON() {
23
- return {
24
- name: this.name,
25
- message: this.message,
26
- context: this.context,
27
- stack: this.stack
14
+ const merged = {
15
+ pluginName: context.pluginName || 'MLPlugin',
16
+ operation: context.operation || 'unknown',
17
+ statusCode: context.statusCode ?? 500,
18
+ retriable: context.retriable ?? false,
19
+ suggestion: context.suggestion ?? 'Review ML plugin configuration and datasets before retrying.',
20
+ ...context
28
21
  };
22
+ super(message, merged);
23
+ this.name = 'MLError';
29
24
  }
30
25
  }
31
26
 
@@ -35,7 +30,12 @@ export class MLError extends Error {
35
30
  */
36
31
  export class ModelConfigError extends MLError {
37
32
  constructor(message, context = {}) {
38
- super(message, context);
33
+ super(message, {
34
+ statusCode: context.statusCode ?? 400,
35
+ retriable: context.retriable ?? false,
36
+ suggestion: context.suggestion ?? 'Validate layer definitions, optimizer, and loss function values.',
37
+ ...context
38
+ });
39
39
  this.name = 'ModelConfigError';
40
40
  }
41
41
  }
@@ -46,7 +46,11 @@ export class ModelConfigError extends MLError {
46
46
  */
47
47
  export class TrainingError extends MLError {
48
48
  constructor(message, context = {}) {
49
- super(message, context);
49
+ super(message, {
50
+ retriable: context.retriable ?? true,
51
+ suggestion: context.suggestion ?? 'Inspect training logs, data shapes, and GPU availability, then retry.',
52
+ ...context
53
+ });
50
54
  this.name = 'TrainingError';
51
55
  }
52
56
  }
@@ -57,7 +61,11 @@ export class TrainingError extends MLError {
57
61
  */
58
62
  export class PredictionError extends MLError {
59
63
  constructor(message, context = {}) {
60
- super(message, context);
64
+ super(message, {
65
+ retriable: context.retriable ?? true,
66
+ suggestion: context.suggestion ?? 'Verify the model is loaded and input tensors match the expected schema.',
67
+ ...context
68
+ });
61
69
  this.name = 'PredictionError';
62
70
  }
63
71
  }
@@ -68,7 +76,12 @@ export class PredictionError extends MLError {
68
76
  */
69
77
  export class ModelNotFoundError extends MLError {
70
78
  constructor(message, context = {}) {
71
- super(message, context);
79
+ super(message, {
80
+ statusCode: 404,
81
+ retriable: false,
82
+ suggestion: context.suggestion ?? 'Train the model or load it from storage before invoking inference.',
83
+ ...context
84
+ });
72
85
  this.name = 'ModelNotFoundError';
73
86
  }
74
87
  }
@@ -79,7 +92,12 @@ export class ModelNotFoundError extends MLError {
79
92
  */
80
93
  export class ModelNotTrainedError extends MLError {
81
94
  constructor(message, context = {}) {
82
- super(message, context);
95
+ super(message, {
96
+ statusCode: 409,
97
+ retriable: false,
98
+ suggestion: context.suggestion ?? 'Run train() for this model or load a trained checkpoint.',
99
+ ...context
100
+ });
83
101
  this.name = 'ModelNotTrainedError';
84
102
  }
85
103
  }
@@ -90,7 +108,12 @@ export class ModelNotTrainedError extends MLError {
90
108
  */
91
109
  export class DataValidationError extends MLError {
92
110
  constructor(message, context = {}) {
93
- super(message, context);
111
+ super(message, {
112
+ statusCode: 422,
113
+ retriable: false,
114
+ suggestion: context.suggestion ?? 'Normalize input data and ensure required features are provided.',
115
+ ...context
116
+ });
94
117
  this.name = 'DataValidationError';
95
118
  }
96
119
  }
@@ -101,7 +124,12 @@ export class DataValidationError extends MLError {
101
124
  */
102
125
  export class InsufficientDataError extends MLError {
103
126
  constructor(message, context = {}) {
104
- super(message, context);
127
+ super(message, {
128
+ statusCode: 400,
129
+ retriable: false,
130
+ suggestion: context.suggestion ?? 'Collect more samples, reduce batch size, or adjust minimumRecords configuration.',
131
+ ...context
132
+ });
105
133
  this.name = 'InsufficientDataError';
106
134
  }
107
135
  }
@@ -112,7 +140,11 @@ export class InsufficientDataError extends MLError {
112
140
  */
113
141
  export class TensorFlowDependencyError extends MLError {
114
142
  constructor(message = 'TensorFlow.js is not installed. Run: pnpm add @tensorflow/tfjs-node', context = {}) {
115
- super(message, context);
143
+ super(message, {
144
+ retriable: false,
145
+ suggestion: context.suggestion ?? 'Install @tensorflow/tfjs-node or @tensorflow/tfjs to enable ML features.',
146
+ ...context
147
+ });
116
148
  this.name = 'TensorFlowDependencyError';
117
149
  }
118
150
  }
@@ -284,7 +284,13 @@ export class MLPlugin extends Plugin {
284
284
  const mlPlugin = resource.database?._mlPlugin;
285
285
 
286
286
  if (!mlPlugin) {
287
- throw new Error('MLPlugin not installed');
287
+ throw new MLError('MLPlugin is not installed on this database instance', {
288
+ pluginName: 'MLPlugin',
289
+ operation: 'Resource.ml accessor',
290
+ statusCode: 400,
291
+ retriable: false,
292
+ suggestion: 'Install MLPlugin via db.usePlugin(new MLPlugin(...)) before calling resource.ml.* methods.'
293
+ });
288
294
  }
289
295
 
290
296
  return {
@@ -437,7 +443,13 @@ export class MLPlugin extends Plugin {
437
443
  Resource.prototype.predict = async function(input, targetAttribute) {
438
444
  const mlPlugin = this.database?._mlPlugin;
439
445
  if (!mlPlugin) {
440
- throw new Error('MLPlugin not installed');
446
+ throw new MLError('MLPlugin is not installed on this database instance', {
447
+ pluginName: 'MLPlugin',
448
+ operation: 'Resource.predict',
449
+ statusCode: 400,
450
+ retriable: false,
451
+ suggestion: 'Install MLPlugin via db.usePlugin(new MLPlugin(...)) before calling resource.predict().'
452
+ });
441
453
  }
442
454
 
443
455
  return await mlPlugin._resourcePredict(this.name, input, targetAttribute);
@@ -449,7 +461,13 @@ export class MLPlugin extends Plugin {
449
461
  Resource.prototype.trainModel = async function(targetAttribute, options = {}) {
450
462
  const mlPlugin = this.database?._mlPlugin;
451
463
  if (!mlPlugin) {
452
- throw new Error('MLPlugin not installed');
464
+ throw new MLError('MLPlugin is not installed on this database instance', {
465
+ pluginName: 'MLPlugin',
466
+ operation: 'Resource.trainModel',
467
+ statusCode: 400,
468
+ retriable: false,
469
+ suggestion: 'Install MLPlugin via db.usePlugin(new MLPlugin(...)) before calling resource.trainModel().'
470
+ });
453
471
  }
454
472
 
455
473
  return await mlPlugin._resourceTrainModel(this.name, targetAttribute, options);
@@ -461,7 +479,13 @@ export class MLPlugin extends Plugin {
461
479
  Resource.prototype.listModels = function() {
462
480
  const mlPlugin = this.database?._mlPlugin;
463
481
  if (!mlPlugin) {
464
- throw new Error('MLPlugin not installed');
482
+ throw new MLError('MLPlugin is not installed on this database instance', {
483
+ pluginName: 'MLPlugin',
484
+ operation: 'Resource.listModels',
485
+ statusCode: 400,
486
+ retriable: false,
487
+ suggestion: 'Install MLPlugin via db.usePlugin(new MLPlugin(...)) before calling resource.listModels().'
488
+ });
465
489
  }
466
490
 
467
491
  return mlPlugin._resourceListModels(this.name);
@@ -0,0 +1,210 @@
1
+ /**
2
+ * Plugin Namespace Detection and Logging
3
+ *
4
+ * Provides standardized namespace detection and console warnings for all plugins.
5
+ *
6
+ * @module concerns/plugin-namespace
7
+ */
8
+
9
+ /**
10
+ * List all existing namespaces for a plugin by scanning storage
11
+ *
12
+ * @param {Object} storage - Plugin storage instance
13
+ * @param {string} pluginPrefix - Plugin prefix (e.g., 'recon', 'scheduler', 'cache')
14
+ * @returns {Promise<string[]>} Array of namespace strings, sorted alphabetically
15
+ *
16
+ * @example
17
+ * const storage = plugin.getStorage();
18
+ * const namespaces = await listPluginNamespaces(storage, 'recon');
19
+ * // ['aggressive', 'default', 'stealth', 'uptime']
20
+ */
21
+ export async function listPluginNamespaces(storage, pluginPrefix) {
22
+ if (!storage) {
23
+ return [];
24
+ }
25
+
26
+ try {
27
+ // Get base key for plugin: plugin=<pluginPrefix>/
28
+ const baseKey = storage.getPluginKey(null);
29
+
30
+ // List all keys under plugin prefix
31
+ const allKeys = await storage.list(baseKey);
32
+
33
+ // Extract unique namespaces from keys like: plugin=<pluginPrefix>/<namespace>/...
34
+ const namespaces = new Set();
35
+ const prefix = baseKey.endsWith('/') ? baseKey : `${baseKey}/`;
36
+
37
+ for (const key of allKeys) {
38
+ // Remove prefix and extract first segment (namespace)
39
+ const relativePath = key.replace(prefix, '');
40
+ const parts = relativePath.split('/');
41
+
42
+ if (parts.length > 0 && parts[0]) {
43
+ namespaces.add(parts[0]);
44
+ }
45
+ }
46
+
47
+ return Array.from(namespaces).sort();
48
+ } catch (error) {
49
+ // If no keys exist yet or storage error, return empty array
50
+ return [];
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Emit console warnings about namespace detection and usage
56
+ *
57
+ * Standardized format for all plugins:
58
+ * - Lists detected namespaces (if any)
59
+ * - Warns which namespace is being used
60
+ *
61
+ * @param {string} pluginName - Plugin name for logging (e.g., 'ReconPlugin', 'SchedulerPlugin')
62
+ * @param {string} currentNamespace - The namespace being used by this instance
63
+ * @param {string[]} existingNamespaces - Array of detected namespaces
64
+ *
65
+ * @example
66
+ * warnNamespaceUsage('ReconPlugin', 'uptime', ['default', 'stealth']);
67
+ * // Console output:
68
+ * // [ReconPlugin] Detected 2 existing namespace(s): default, stealth
69
+ * // [ReconPlugin] Using namespace: "uptime"
70
+ */
71
+ export function warnNamespaceUsage(pluginName, currentNamespace, existingNamespaces = []) {
72
+ if (existingNamespaces.length > 0) {
73
+ console.warn(
74
+ `[${pluginName}] Detected ${existingNamespaces.length} existing namespace(s): ${existingNamespaces.join(', ')}`
75
+ );
76
+ }
77
+
78
+ const namespaceDisplay = currentNamespace === '' ? '(none)' : `"${currentNamespace}"`;
79
+ console.warn(`[${pluginName}] Using namespace: ${namespaceDisplay}`);
80
+ }
81
+
82
+ /**
83
+ * Complete namespace detection and warning flow
84
+ *
85
+ * Convenience method that combines listing and warning in one call.
86
+ *
87
+ * @param {Object} storage - Plugin storage instance
88
+ * @param {string} pluginName - Plugin name for logging
89
+ * @param {string} pluginPrefix - Plugin prefix for storage scanning
90
+ * @param {string} currentNamespace - The namespace being used
91
+ * @returns {Promise<string[]>} Array of detected namespaces
92
+ *
93
+ * @example
94
+ * const namespaces = await detectAndWarnNamespaces(
95
+ * plugin.getStorage(),
96
+ * 'ReconPlugin',
97
+ * 'recon',
98
+ * 'uptime'
99
+ * );
100
+ * // Console output:
101
+ * // [ReconPlugin] Detected 2 existing namespace(s): default, stealth
102
+ * // [ReconPlugin] Using namespace: "uptime"
103
+ * // Returns: ['default', 'stealth']
104
+ */
105
+ export async function detectAndWarnNamespaces(storage, pluginName, pluginPrefix, currentNamespace) {
106
+ const existingNamespaces = await listPluginNamespaces(storage, pluginPrefix);
107
+ warnNamespaceUsage(pluginName, currentNamespace, existingNamespaces);
108
+ return existingNamespaces;
109
+ }
110
+
111
+ /**
112
+ * Get namespaced resource name
113
+ *
114
+ * Generates consistent resource names across all plugins.
115
+ * Pattern: plg_<namespace>_<plugin>_<resource>
116
+ * Empty namespace: plg_<plugin>_<resource>
117
+ *
118
+ * @param {string} baseResourceName - Base resource name (e.g., 'plg_recon_hosts')
119
+ * @param {string} namespace - Namespace to apply (empty string = no namespace)
120
+ * @param {string} pluginPrefix - Plugin prefix (e.g., 'plg_recon')
121
+ * @returns {string} Namespaced resource name
122
+ *
123
+ * @example
124
+ * getNamespacedResourceName('plg_recon_hosts', '', 'plg_recon');
125
+ * // 'plg_recon_hosts'
126
+ *
127
+ * getNamespacedResourceName('plg_recon_hosts', 'uptime', 'plg_recon');
128
+ * // 'plg_uptime_recon_hosts'
129
+ *
130
+ * getNamespacedResourceName('plg_scheduler_jobs', 'prod', 'plg_scheduler');
131
+ * // 'plg_prod_scheduler_jobs'
132
+ */
133
+ export function getNamespacedResourceName(baseResourceName, namespace, pluginPrefix) {
134
+ if (!namespace) {
135
+ return baseResourceName;
136
+ }
137
+
138
+ // Insert namespace after 'plg_' prefix
139
+ // Example: 'plg_recon_hosts' → 'plg_uptime_recon_hosts'
140
+ return baseResourceName.replace('plg_', `plg_${namespace}_`);
141
+ }
142
+
143
+ /**
144
+ * Validate namespace string
145
+ *
146
+ * Ensures namespace follows naming conventions:
147
+ * - Alphanumeric + hyphens + underscores only
148
+ * - 1-50 characters
149
+ * - Empty string is allowed (no namespace)
150
+ *
151
+ * @param {string} namespace - Namespace to validate
152
+ * @throws {Error} If namespace is invalid
153
+ *
154
+ * @example
155
+ * validateNamespace('uptime'); // OK
156
+ * validateNamespace('client-acme'); // OK
157
+ * validateNamespace('prod_env_2'); // OK
158
+ * validateNamespace(''); // OK (no namespace)
159
+ * validateNamespace('invalid space'); // Throws
160
+ * validateNamespace('a'.repeat(51)); // Throws
161
+ */
162
+ export function validateNamespace(namespace) {
163
+ // Empty string is allowed (no namespace)
164
+ if (namespace === '') {
165
+ return;
166
+ }
167
+
168
+ if (!namespace || typeof namespace !== 'string') {
169
+ throw new Error('Namespace must be a string');
170
+ }
171
+
172
+ if (namespace.length > 50) {
173
+ throw new Error('Namespace must be 50 characters or less');
174
+ }
175
+
176
+ // Allow alphanumeric, hyphens, and underscores only
177
+ const validPattern = /^[a-zA-Z0-9_-]+$/;
178
+ if (!validPattern.test(namespace)) {
179
+ throw new Error(
180
+ 'Namespace can only contain alphanumeric characters, hyphens, and underscores'
181
+ );
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Get namespace from config with validation
187
+ *
188
+ * Extracts and validates namespace from plugin config.
189
+ * Defaults to empty string if not specified (no namespace).
190
+ *
191
+ * @param {Object} config - Plugin configuration
192
+ * @param {string} defaultNamespace - Default namespace if not specified (default: '')
193
+ * @returns {string} Validated namespace
194
+ * @throws {Error} If namespace is invalid
195
+ *
196
+ * @example
197
+ * getValidatedNamespace({ namespace: 'uptime' });
198
+ * // 'uptime'
199
+ *
200
+ * getValidatedNamespace({});
201
+ * // ''
202
+ *
203
+ * getValidatedNamespace({ namespace: 'invalid space' });
204
+ * // Throws Error
205
+ */
206
+ export function getValidatedNamespace(config = {}, defaultNamespace = '') {
207
+ const namespace = (config.namespace !== undefined && config.namespace !== null) ? config.namespace : defaultNamespace;
208
+ validateNamespace(namespace);
209
+ return namespace;
210
+ }