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
@@ -4,6 +4,7 @@ import { flatten } from 'flat';
4
4
  import isEqual from 'lodash-es/isEqual.js';
5
5
 
6
6
  import { Plugin } from './plugin.class.js';
7
+ import { PluginError } from '../errors.js';
7
8
  import tryFn from '../concerns/try-fn.js';
8
9
  import { requirePluginDependency } from './concerns/plugin-dependencies.js';
9
10
 
@@ -14,13 +15,7 @@ import {
14
15
  registerCloudDriver,
15
16
  BaseCloudDriver
16
17
  } from './cloud-inventory/index.js';
17
-
18
- const DEFAULT_RESOURCES = {
19
- snapshots: 'plg_cloud_inventory_snapshots',
20
- versions: 'plg_cloud_inventory_versions',
21
- changes: 'plg_cloud_inventory_changes',
22
- clouds: 'plg_cloud_inventory_clouds'
23
- };
18
+ import { resolveResourceNames } from './concerns/resource-names.js';
24
19
 
25
20
  const DEFAULT_DISCOVERY = {
26
21
  concurrency: 3,
@@ -77,16 +72,34 @@ export class CloudInventoryPlugin extends Plugin {
77
72
  (level, message, meta) => pendingLogs.push({ level, message, meta })
78
73
  );
79
74
 
75
+ this._internalResourceOverrides = options.resourceNames || {};
76
+ this._internalResourceDescriptors = {
77
+ snapshots: {
78
+ defaultName: 'plg_cloud_inventory_snapshots',
79
+ override: this._internalResourceOverrides.snapshots
80
+ },
81
+ versions: {
82
+ defaultName: 'plg_cloud_inventory_versions',
83
+ override: this._internalResourceOverrides.versions
84
+ },
85
+ changes: {
86
+ defaultName: 'plg_cloud_inventory_changes',
87
+ override: this._internalResourceOverrides.changes
88
+ },
89
+ clouds: {
90
+ defaultName: 'plg_cloud_inventory_clouds',
91
+ override: this._internalResourceOverrides.clouds
92
+ }
93
+ };
94
+ this.internalResourceNames = this._resolveInternalResourceNames();
95
+
80
96
  this.config = {
81
97
  clouds: normalizedClouds,
82
98
  discovery: {
83
99
  ...DEFAULT_DISCOVERY,
84
100
  ...(options.discovery || {})
85
101
  },
86
- resources: {
87
- ...DEFAULT_RESOURCES,
88
- ...(options.resources || {})
89
- },
102
+ resourceNames: this.internalResourceNames,
90
103
  logger: typeof options.logger === 'function' ? options.logger : null,
91
104
  verbose: options.verbose === true,
92
105
  scheduled: normalizeSchedule(options.scheduled),
@@ -108,6 +121,7 @@ export class CloudInventoryPlugin extends Plugin {
108
121
  this._resourceHandles = {};
109
122
  this._scheduledJobs = [];
110
123
  this._cron = null;
124
+ this.resourceNames = this.internalResourceNames;
111
125
 
112
126
  for (const entry of pendingLogs) {
113
127
  this._log(entry.level, entry.message, entry.meta);
@@ -138,6 +152,15 @@ export class CloudInventoryPlugin extends Plugin {
138
152
  await this._destroyDrivers();
139
153
  }
140
154
 
155
+ onNamespaceChanged() {
156
+ this.internalResourceNames = this._resolveInternalResourceNames();
157
+ if (this.config) {
158
+ this.config.resourceNames = this.internalResourceNames;
159
+ }
160
+ this.resourceNames = this.internalResourceNames;
161
+ this._resourceHandles = {};
162
+ }
163
+
141
164
  async syncAll(options = {}) {
142
165
  const results = [];
143
166
  for (const cloud of this.config.clouds) {
@@ -156,7 +179,14 @@ export class CloudInventoryPlugin extends Plugin {
156
179
  async syncCloud(cloudId, options = {}) {
157
180
  const driverEntry = this.cloudDrivers.get(cloudId);
158
181
  if (!driverEntry) {
159
- throw new Error(`Cloud "${cloudId}" is not registered. Available clouds: ${[...this.cloudDrivers.keys()].join(', ') || 'none'}`);
182
+ throw new PluginError(`Cloud "${cloudId}" is not registered`, {
183
+ pluginName: 'CloudInventoryPlugin',
184
+ operation: 'syncCloud',
185
+ statusCode: 404,
186
+ retriable: false,
187
+ suggestion: `Register the cloud definition in CloudInventoryPlugin configuration. Available: ${[...this.cloudDrivers.keys()].join(', ') || 'none'}.`,
188
+ cloudId
189
+ });
160
190
  }
161
191
 
162
192
  const { driver, definition } = driverEntry;
@@ -228,136 +258,144 @@ export class CloudInventoryPlugin extends Plugin {
228
258
  }
229
259
  };
230
260
 
231
- let items;
232
261
  try {
233
- items = await driver.listResources({
234
- discovery: this.config.discovery,
235
- checkpoint: runtimeContext.checkpoint,
236
- state: runtimeContext.state,
237
- runtime: runtimeContext,
238
- ...options
239
- });
240
- } catch (err) {
241
- await storage.releaseLock(lockKey).catch(() => {});
242
- await this._updateCloudSummary(cloudId, {
243
- status: 'error',
244
- lastErrorAt: new Date().toISOString(),
245
- lastError: err.message || 'Driver failure during listResources'
246
- });
247
- throw err;
248
- }
262
+ let items;
263
+ try {
264
+ items = await driver.listResources({
265
+ discovery: this.config.discovery,
266
+ checkpoint: runtimeContext.checkpoint,
267
+ state: runtimeContext.state,
268
+ runtime: runtimeContext,
269
+ ...options
270
+ });
271
+ } catch (err) {
272
+ await this._updateCloudSummary(cloudId, {
273
+ status: 'error',
274
+ lastErrorAt: new Date().toISOString(),
275
+ lastError: err.message || 'Driver failure during listResources'
276
+ });
277
+ throw err;
278
+ }
249
279
 
250
- let countCreated = 0;
251
- let countUpdated = 0;
252
- let countUnchanged = 0;
253
- let processed = 0;
254
- let errorDuringRun = null;
255
- const startMs = Date.now();
256
-
257
- const processItem = async (rawItem) => {
258
- const normalized = this._normalizeResource(definition, rawItem);
259
- if (!normalized) return;
260
-
261
- const persisted = await this._persistSnapshot(normalized, rawItem);
262
- processed += 1;
263
- if (persisted?.status === 'created') countCreated += 1;
264
- else if (persisted?.status === 'updated') countUpdated += 1;
265
- else countUnchanged += 1;
266
- };
280
+ let countCreated = 0;
281
+ let countUpdated = 0;
282
+ let countUnchanged = 0;
283
+ let processed = 0;
284
+ let errorDuringRun = null;
285
+ const startMs = Date.now();
286
+
287
+ const processItem = async (rawItem) => {
288
+ const normalized = this._normalizeResource(definition, rawItem);
289
+ if (!normalized) return;
290
+
291
+ const persisted = await this._persistSnapshot(normalized, rawItem);
292
+ processed += 1;
293
+ if (persisted?.status === 'created') countCreated += 1;
294
+ else if (persisted?.status === 'updated') countUpdated += 1;
295
+ else countUnchanged += 1;
296
+ };
267
297
 
268
- try {
269
- if (isAsyncIterable(items)) {
270
- for await (const item of items) {
271
- await processItem(item);
272
- }
273
- } else if (Array.isArray(items)) {
274
- for (const item of items) {
275
- await processItem(item);
298
+ try {
299
+ if (isAsyncIterable(items)) {
300
+ for await (const item of items) {
301
+ await processItem(item);
302
+ }
303
+ } else if (Array.isArray(items)) {
304
+ for (const item of items) {
305
+ await processItem(item);
306
+ }
307
+ } else if (items) {
308
+ await processItem(items);
276
309
  }
277
- } else if (items) {
278
- await processItem(items);
310
+ } catch (err) {
311
+ errorDuringRun = err;
279
312
  }
280
- } catch (err) {
281
- errorDuringRun = err;
282
- }
283
-
284
- const finishedAt = new Date().toISOString();
285
- const durationMs = Date.now() - startMs;
286
313
 
287
- const summaryPatch = {
288
- status: errorDuringRun ? 'error' : 'idle',
289
- lastRunAt: startedAt,
290
- lastRunId: runId,
291
- lastResult: {
292
- runId,
293
- startedAt,
294
- finishedAt,
295
- durationMs,
296
- counts: {
297
- created: countCreated,
298
- updated: countUpdated,
299
- unchanged: countUnchanged
314
+ const finishedAt = new Date().toISOString();
315
+ const durationMs = Date.now() - startMs;
316
+
317
+ const summaryPatch = {
318
+ status: errorDuringRun ? 'error' : 'idle',
319
+ lastRunAt: startedAt,
320
+ lastRunId: runId,
321
+ lastResult: {
322
+ runId,
323
+ startedAt,
324
+ finishedAt,
325
+ durationMs,
326
+ counts: {
327
+ created: countCreated,
328
+ updated: countUpdated,
329
+ unchanged: countUnchanged
330
+ },
331
+ processed,
332
+ checkpoint: pendingCheckpoint
300
333
  },
301
- processed,
302
- checkpoint: pendingCheckpoint
303
- },
304
- totalResources: Math.max(0, (summaryBefore?.totalResources ?? 0) + countCreated),
305
- totalVersions: Math.max(0, (summaryBefore?.totalVersions ?? 0) + countCreated + countUpdated),
306
- checkpoint: pendingCheckpoint,
307
- checkpointUpdatedAt: pendingCheckpoint !== summaryBefore?.checkpoint ? finishedAt : summaryBefore?.checkpointUpdatedAt,
308
- rateLimit: pendingRateLimit,
309
- rateLimitUpdatedAt: pendingRateLimit !== summaryBefore?.rateLimit ? finishedAt : summaryBefore?.rateLimitUpdatedAt,
310
- state: pendingState,
311
- stateUpdatedAt: pendingState !== summaryBefore?.state ? finishedAt : summaryBefore?.stateUpdatedAt,
312
- progress: null
313
- };
334
+ totalResources: Math.max(0, (summaryBefore?.totalResources ?? 0) + countCreated),
335
+ totalVersions: Math.max(0, (summaryBefore?.totalVersions ?? 0) + countCreated + countUpdated),
336
+ checkpoint: pendingCheckpoint,
337
+ checkpointUpdatedAt: pendingCheckpoint !== summaryBefore?.checkpoint ? finishedAt : summaryBefore?.checkpointUpdatedAt,
338
+ rateLimit: pendingRateLimit,
339
+ rateLimitUpdatedAt: pendingRateLimit !== summaryBefore?.rateLimit ? finishedAt : summaryBefore?.rateLimitUpdatedAt,
340
+ state: pendingState,
341
+ stateUpdatedAt: pendingState !== summaryBefore?.state ? finishedAt : summaryBefore?.stateUpdatedAt,
342
+ progress: null
343
+ };
314
344
 
315
- if (errorDuringRun) {
316
- summaryPatch.lastError = errorDuringRun.message;
317
- summaryPatch.lastErrorAt = finishedAt;
318
- } else {
319
- summaryPatch.lastError = null;
320
- summaryPatch.lastSuccessAt = finishedAt;
321
- }
345
+ if (errorDuringRun) {
346
+ summaryPatch.lastError = errorDuringRun.message;
347
+ summaryPatch.lastErrorAt = finishedAt;
348
+ } else {
349
+ summaryPatch.lastError = null;
350
+ summaryPatch.lastSuccessAt = finishedAt;
351
+ }
322
352
 
323
- await this._updateCloudSummary(cloudId, summaryPatch);
353
+ await this._updateCloudSummary(cloudId, summaryPatch);
324
354
 
325
- try {
326
- await storage.releaseLock(lockKey);
327
- } catch (releaseErr) {
328
- this._log('warn', 'Failed to release sync lock', { cloudId, error: releaseErr.message });
329
- }
355
+ if (errorDuringRun) {
356
+ throw errorDuringRun;
357
+ }
330
358
 
331
- if (errorDuringRun) {
332
- throw errorDuringRun;
333
- }
359
+ const summary = {
360
+ cloudId,
361
+ driver: definition.driver,
362
+ created: countCreated,
363
+ updated: countUpdated,
364
+ unchanged: countUnchanged,
365
+ processed,
366
+ durationMs
367
+ };
334
368
 
335
- const summary = {
336
- cloudId,
337
- driver: definition.driver,
338
- created: countCreated,
339
- updated: countUpdated,
340
- unchanged: countUnchanged,
341
- processed,
342
- durationMs
343
- };
369
+ this._log('info', 'Cloud sync finished', summary);
344
370
 
345
- this._log('info', 'Cloud sync finished', summary);
371
+ // Auto-export to Terraform if configured
372
+ if (this.config.terraform.enabled && this.config.terraform.autoExport) {
373
+ await this._autoExportTerraform(cloudId);
374
+ }
346
375
 
347
- // Auto-export to Terraform if configured
348
- if (this.config.terraform.enabled && this.config.terraform.autoExport) {
349
- await this._autoExportTerraform(cloudId);
376
+ return summary;
377
+ } finally {
378
+ try {
379
+ await storage.releaseLock(lock);
380
+ } catch (releaseErr) {
381
+ this._log('warn', 'Failed to release sync lock', {
382
+ cloudId,
383
+ lockName: lock?.name ?? lockKey,
384
+ error: releaseErr.message
385
+ });
386
+ }
350
387
  }
351
-
352
- return summary;
353
388
  }
354
389
 
355
390
  _validateConfiguration() {
356
391
  if (!Array.isArray(this.config.clouds) || this.config.clouds.length === 0) {
357
- throw new Error(
358
- 'CloudInventoryPlugin requires a "clouds" array in the configuration. ' +
359
- `Registered drivers: ${listCloudDrivers().join(', ') || 'none'}`
360
- );
392
+ throw new PluginError('CloudInventoryPlugin requires a "clouds" array in the configuration', {
393
+ pluginName: 'CloudInventoryPlugin',
394
+ operation: 'validateConfiguration',
395
+ statusCode: 400,
396
+ retriable: false,
397
+ suggestion: `Provide at least one cloud definition. Registered drivers: ${listCloudDrivers().join(', ') || 'none'}.`
398
+ });
361
399
  }
362
400
 
363
401
  for (const cloud of this.config.clouds) {
@@ -366,7 +404,15 @@ export class CloudInventoryPlugin extends Plugin {
366
404
  try {
367
405
  normalizeSchedule(cloud.scheduled);
368
406
  } catch (err) {
369
- throw new Error(`Cloud "${cloud.id}" has an invalid scheduled configuration: ${err.message}`);
407
+ throw new PluginError(`Cloud "${cloud.id}" has an invalid scheduled configuration`, {
408
+ pluginName: 'CloudInventoryPlugin',
409
+ operation: 'validateConfiguration',
410
+ statusCode: 400,
411
+ retriable: false,
412
+ suggestion: 'Provide a valid cron expression and timezone when enabling scheduled discovery.',
413
+ cloudId: cloud.id,
414
+ original: err
415
+ });
370
416
  }
371
417
  }
372
418
  }
@@ -410,7 +456,13 @@ export class CloudInventoryPlugin extends Plugin {
410
456
  // Get snapshots resource
411
457
  const snapshotsResource = this._resourceHandles.snapshots;
412
458
  if (!snapshotsResource) {
413
- throw new Error('Snapshots resource not initialized. Ensure plugin is installed.');
459
+ throw new PluginError('Snapshots resource not initialized', {
460
+ pluginName: 'CloudInventoryPlugin',
461
+ operation: 'exportToTerraformState',
462
+ statusCode: 500,
463
+ retriable: false,
464
+ suggestion: 'Call database.usePlugin(new CloudInventoryPlugin(...)) and ensure onInstall completed before exporting.'
465
+ });
414
466
  }
415
467
 
416
468
  // Build query filter
@@ -497,7 +549,13 @@ export class CloudInventoryPlugin extends Plugin {
497
549
  // Get S3 client from database
498
550
  const s3Client = this.database.client;
499
551
  if (!s3Client || typeof s3Client.putObject !== 'function') {
500
- throw new Error('S3 client not available. Database must use S3-compatible storage.');
552
+ throw new PluginError('S3 client not available. Database must use S3-compatible storage.', {
553
+ pluginName: 'CloudInventoryPlugin',
554
+ operation: 'exportToTerraformStateToS3',
555
+ statusCode: 500,
556
+ retriable: false,
557
+ suggestion: 'Initialize the database with an S3-compatible client before exporting Terraform state to S3.'
558
+ });
501
559
  }
502
560
 
503
561
  // Upload to S3
@@ -548,14 +606,27 @@ export class CloudInventoryPlugin extends Plugin {
548
606
  // Parse S3 URL: s3://bucket/path/to/file.tfstate
549
607
  const s3Match = terraform.output?.match(/^s3:\/\/([^/]+)\/(.+)$/);
550
608
  if (!s3Match) {
551
- throw new Error(`Invalid S3 URL format: ${terraform.output}. Expected: s3://bucket/path/file.tfstate`);
609
+ throw new PluginError(`Invalid S3 URL format: ${terraform.output}`, {
610
+ pluginName: 'CloudInventoryPlugin',
611
+ operation: '_autoExportTerraform',
612
+ statusCode: 400,
613
+ retriable: false,
614
+ suggestion: 'Provide a Terraform export destination using s3://bucket/path/file.tfstate.',
615
+ output: terraform.output
616
+ });
552
617
  }
553
618
  const [, bucket, key] = s3Match;
554
619
  result = await this.exportToTerraformStateToS3(bucket, key, exportOptions);
555
620
  } else if (terraform.outputType === 'file') {
556
621
  // File path
557
622
  if (!terraform.output) {
558
- throw new Error('Terraform output path not configured');
623
+ throw new PluginError('Terraform output path not configured', {
624
+ pluginName: 'CloudInventoryPlugin',
625
+ operation: '_autoExportTerraform',
626
+ statusCode: 400,
627
+ retriable: false,
628
+ suggestion: 'Set terraform.output to a file path (e.g., ./terraform/state.tfstate) when using outputType "file".'
629
+ });
559
630
  }
560
631
  result = await this.exportToTerraformStateFile(terraform.output, exportOptions);
561
632
  } else {
@@ -564,7 +635,14 @@ export class CloudInventoryPlugin extends Plugin {
564
635
  const stateData = await this.exportToTerraformState(exportOptions);
565
636
  result = await terraform.output(stateData);
566
637
  } else {
567
- throw new Error(`Unknown terraform.outputType: ${terraform.outputType}`);
638
+ throw new PluginError(`Unknown terraform.outputType: ${terraform.outputType}`, {
639
+ pluginName: 'CloudInventoryPlugin',
640
+ operation: '_autoExportTerraform',
641
+ statusCode: 400,
642
+ retriable: false,
643
+ suggestion: 'Use one of the supported output types: "file", "s3", or provide a custom function.',
644
+ outputType: terraform.outputType
645
+ });
568
646
  }
569
647
  }
570
648
 
@@ -579,12 +657,11 @@ export class CloudInventoryPlugin extends Plugin {
579
657
  }
580
658
 
581
659
  async _ensureResources() {
582
- const {
583
- snapshots,
584
- versions,
585
- changes,
586
- clouds
587
- } = this.config.resources;
660
+ const names = this.internalResourceNames;
661
+ const snapshots = names.snapshots;
662
+ const versions = names.versions;
663
+ const changes = names.changes;
664
+ const clouds = names.clouds;
588
665
 
589
666
  const resourceDefinitions = [
590
667
  {
@@ -740,6 +817,13 @@ export class CloudInventoryPlugin extends Plugin {
740
817
  this._resourceHandles.versions = this.database.resources[versions];
741
818
  this._resourceHandles.changes = this.database.resources[changes];
742
819
  this._resourceHandles.clouds = this.database.resources[clouds];
820
+ this.resourceNames = this.internalResourceNames;
821
+ }
822
+
823
+ _resolveInternalResourceNames() {
824
+ return resolveResourceNames('cloud_inventory', this._internalResourceDescriptors, {
825
+ namespace: this.namespace
826
+ });
743
827
  }
744
828
 
745
829
  async _initializeDrivers() {
@@ -1173,7 +1257,13 @@ function normalizeSchedule(input) {
1173
1257
  schedule.runOnStart = Boolean(schedule.runOnStart);
1174
1258
 
1175
1259
  if (schedule.enabled && !schedule.cron) {
1176
- throw new Error('Scheduled configuration requires a valid cron expression when enabled is true');
1260
+ throw new PluginError('Scheduled configuration requires a valid cron expression when enabled is true', {
1261
+ pluginName: 'CloudInventoryPlugin',
1262
+ operation: 'normalizeSchedule',
1263
+ statusCode: 400,
1264
+ retriable: false,
1265
+ suggestion: 'Set scheduled.cron to a valid cron expression (e.g., "0 * * * *") when enabling scheduled discovery.'
1266
+ });
1177
1267
  }
1178
1268
 
1179
1269
  return schedule;
@@ -1207,7 +1297,13 @@ function resolveDriverReference(driverInput, logFn) {
1207
1297
  return candidate;
1208
1298
  }
1209
1299
 
1210
- throw new Error('Cloud driver must be a string identifier or a class/factory that produces a BaseCloudDriver instance');
1300
+ throw new PluginError('Cloud driver must be a string identifier or a factory/class that produces a BaseCloudDriver instance', {
1301
+ pluginName: 'CloudInventoryPlugin',
1302
+ operation: 'resolveDriverReference',
1303
+ statusCode: 400,
1304
+ retriable: false,
1305
+ suggestion: 'Register the driver name via registerCloudDriver() or supply a factory/class returning BaseCloudDriver.'
1306
+ });
1211
1307
  }
1212
1308
 
1213
1309
  function instantiateInlineDriver(driverInput, options) {
@@ -1224,7 +1320,13 @@ function instantiateInlineDriver(driverInput, options) {
1224
1320
  return result;
1225
1321
  }
1226
1322
 
1227
- throw new Error('Inline driver factory must return an instance of BaseCloudDriver');
1323
+ throw new PluginError('Inline driver factory must return an instance of BaseCloudDriver', {
1324
+ pluginName: 'CloudInventoryPlugin',
1325
+ operation: 'instantiateInlineDriver',
1326
+ statusCode: 500,
1327
+ retriable: false,
1328
+ suggestion: 'Ensure the inline driver function returns a BaseCloudDriver instance or class.'
1329
+ });
1228
1330
  }
1229
1331
 
1230
1332
  function isSubclassOfBase(fn) {
@@ -175,6 +175,56 @@ export const PLUGIN_DEPENDENCIES = {
175
175
  npmUrl: 'https://www.npmjs.com/package/@tensorflow/tfjs-node'
176
176
  }
177
177
  }
178
+ },
179
+ 'puppeteer': {
180
+ name: 'Puppeteer Suite',
181
+ docsUrl: 'https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/puppeteer/README.md',
182
+ dependencies: {
183
+ 'puppeteer-extra': {
184
+ version: '^3.3.4',
185
+ description: 'Headless Chrome automation toolkit',
186
+ installCommand: 'pnpm add puppeteer-extra',
187
+ npmUrl: 'https://www.npmjs.com/package/puppeteer-extra'
188
+ },
189
+ 'puppeteer-extra-plugin-stealth': {
190
+ version: '^2.11.2',
191
+ description: 'Stealth plugin to evade bot detection',
192
+ installCommand: 'pnpm add puppeteer-extra-plugin-stealth',
193
+ npmUrl: 'https://www.npmjs.com/package/puppeteer-extra-plugin-stealth'
194
+ },
195
+ 'user-agents': {
196
+ version: '^2.0.0',
197
+ description: 'Randomized user agent generator',
198
+ installCommand: 'pnpm add user-agents',
199
+ npmUrl: 'https://www.npmjs.com/package/user-agents'
200
+ },
201
+ 'ghost-cursor': {
202
+ version: '^1.4.1',
203
+ description: 'Human-like mouse movement generator',
204
+ installCommand: 'pnpm add ghost-cursor',
205
+ npmUrl: 'https://www.npmjs.com/package/ghost-cursor'
206
+ }
207
+ }
208
+ },
209
+ 'puppeteer-extra': {
210
+ name: 'puppeteer-extra',
211
+ docsUrl: 'https://github.com/berstend/puppeteer-extra',
212
+ dependencies: {}
213
+ },
214
+ 'puppeteer-extra-plugin-stealth': {
215
+ name: 'puppeteer-extra-plugin-stealth',
216
+ docsUrl: 'https://github.com/berstend/puppeteer-extra/tree/master/packages/puppeteer-extra-plugin-stealth',
217
+ dependencies: {}
218
+ },
219
+ 'user-agents': {
220
+ name: 'user-agents',
221
+ docsUrl: 'https://github.com/intoli/user-agents',
222
+ dependencies: {}
223
+ },
224
+ 'ghost-cursor': {
225
+ name: 'ghost-cursor',
226
+ docsUrl: 'https://github.com/Xetera/ghost-cursor',
227
+ dependencies: {}
178
228
  }
179
229
  };
180
230
 
@@ -270,6 +320,10 @@ export async function requirePluginDependency(pluginId, options = {}) {
270
320
  return { valid: false, missing: [], incompatible: [], messages: [error.message] };
271
321
  }
272
322
 
323
+ if (process?.env?.S3DB_SKIP_PLUGIN_DEP_CHECK === '1') {
324
+ return { valid: true, missing: [], incompatible: [], messages: [] };
325
+ }
326
+
273
327
  const missing = [];
274
328
  const incompatible = [];
275
329
  const messages = [];