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
@@ -5,6 +5,7 @@
5
5
  * and comprehensive optimal K analysis using multiple metrics.
6
6
  */
7
7
 
8
+ import { ValidationError } from '../../errors.js';
8
9
  import { euclideanDistance } from './distances.js';
9
10
 
10
11
  /**
@@ -31,15 +32,27 @@ export function kmeans(vectors, k, options = {}) {
31
32
  } = options;
32
33
 
33
34
  if (vectors.length === 0) {
34
- throw new Error('Cannot cluster empty vector array');
35
+ throw new ValidationError('Cannot cluster empty vector array', {
36
+ operation: 'kmeans',
37
+ retriable: false,
38
+ suggestion: 'Provide at least one vector before invoking k-means.'
39
+ });
35
40
  }
36
41
 
37
42
  if (k < 1) {
38
- throw new Error(`k must be at least 1, got ${k}`);
43
+ throw new ValidationError(`k must be at least 1, got ${k}`, {
44
+ operation: 'kmeans',
45
+ retriable: false,
46
+ suggestion: 'Use a positive integer for the number of clusters.'
47
+ });
39
48
  }
40
49
 
41
50
  if (k > vectors.length) {
42
- throw new Error(`k (${k}) cannot be greater than number of vectors (${vectors.length})`);
51
+ throw new ValidationError(`k (${k}) cannot be greater than number of vectors (${vectors.length})`, {
52
+ operation: 'kmeans',
53
+ retriable: false,
54
+ suggestion: 'Reduce k or provide more vectors before clustering.'
55
+ });
43
56
  }
44
57
 
45
58
  const dimensions = vectors[0].length;
@@ -47,7 +60,16 @@ export function kmeans(vectors, k, options = {}) {
47
60
  // Validate all vectors have same dimensions
48
61
  for (let i = 1; i < vectors.length; i++) {
49
62
  if (vectors[i].length !== dimensions) {
50
- throw new Error(`All vectors must have same dimensions. Expected ${dimensions}, got ${vectors[i].length} at index ${i}`);
63
+ throw new ValidationError('All vectors must have same dimensions.', {
64
+ operation: 'kmeans',
65
+ retriable: false,
66
+ suggestion: 'Pad or trim vectors so every row has identical dimensionality.',
67
+ metadata: {
68
+ expectedDimensions: dimensions,
69
+ actualDimensions: vectors[i].length,
70
+ index: i
71
+ }
72
+ });
51
73
  }
52
74
  }
53
75
 
@@ -191,6 +191,15 @@ export class Resource extends AsyncEventEmitter {
191
191
  createdBy,
192
192
  };
193
193
 
194
+ // Store raw schema definition (accessible as resource.$schema)
195
+ // This is the LITERAL object passed to createResource()
196
+ // Useful for plugins, documentation, and introspection
197
+ this.$schema = cloneDeep(config);
198
+
199
+ // Add metadata timestamps
200
+ this.$schema._createdAt = Date.now();
201
+ this.$schema._updatedAt = Date.now();
202
+
194
203
  // Initialize hooks system - expanded to cover ALL methods
195
204
  this.hooks = {
196
205
  // Insert hooks
@@ -1175,7 +1184,14 @@ export class Resource extends AsyncEventEmitter {
1175
1184
  // LOG: body e contentType antes do putObject
1176
1185
  // Only throw if behavior is 'body-only' and body is empty
1177
1186
  if (this.behavior === 'body-only' && (!body || body === "")) {
1178
- throw new Error(`[Resource.insert] Attempt to save object without body! Data: id=${finalId}, resource=${this.name}`);
1187
+ throw new ResourceError('Body required for body-only behavior', {
1188
+ resourceName: this.name,
1189
+ operation: 'insert',
1190
+ id: finalId,
1191
+ statusCode: 400,
1192
+ retriable: false,
1193
+ suggestion: 'Include a request body when using behavior "body-only" or switch to "body-overflow".'
1194
+ });
1179
1195
  }
1180
1196
  // For other behaviors, allow empty body (all data in metadata)
1181
1197
 
@@ -1272,8 +1288,22 @@ export class Resource extends AsyncEventEmitter {
1272
1288
  * const user = await resource.get('user-123');
1273
1289
  */
1274
1290
  async get(id) {
1275
- if (isObject(id)) throw new Error(`id cannot be an object`);
1276
- if (isEmpty(id)) throw new Error('id cannot be empty');
1291
+ if (isObject(id)) {
1292
+ throw new ValidationError('Resource id must be a string', {
1293
+ field: 'id',
1294
+ statusCode: 400,
1295
+ retriable: false,
1296
+ suggestion: 'Pass the resource id as a string value (e.g. "user-123").'
1297
+ });
1298
+ }
1299
+ if (isEmpty(id)) {
1300
+ throw new ValidationError('Resource id cannot be empty', {
1301
+ field: 'id',
1302
+ statusCode: 400,
1303
+ retriable: false,
1304
+ suggestion: 'Provide a non-empty id when calling resource methods.'
1305
+ });
1306
+ }
1277
1307
 
1278
1308
  // Execute beforeGet hooks
1279
1309
  await this.executeHooks('beforeGet', { id });
@@ -1454,12 +1484,23 @@ export class Resource extends AsyncEventEmitter {
1454
1484
  */
1455
1485
  async update(id, attributes) {
1456
1486
  if (isEmpty(id)) {
1457
- throw new Error('id cannot be empty');
1487
+ throw new ValidationError('Resource id cannot be empty', {
1488
+ field: 'id',
1489
+ statusCode: 400,
1490
+ retriable: false,
1491
+ suggestion: 'Provide the target id when calling update().'
1492
+ });
1458
1493
  }
1459
1494
  // Garante que o recurso existe antes de atualizar
1460
1495
  const exists = await this.exists(id);
1461
1496
  if (!exists) {
1462
- throw new Error(`Resource with id '${id}' does not exist`);
1497
+ throw new ResourceError(`Resource with id '${id}' does not exist`, {
1498
+ resourceName: this.name,
1499
+ id,
1500
+ statusCode: 404,
1501
+ retriable: false,
1502
+ suggestion: 'Ensure the record exists or create it before attempting an update.'
1503
+ });
1463
1504
  }
1464
1505
  const originalData = await this.get(id);
1465
1506
  const attributesClone = cloneDeep(attributes);
@@ -1685,11 +1726,21 @@ export class Resource extends AsyncEventEmitter {
1685
1726
  */
1686
1727
  async patch(id, fields, options = {}) {
1687
1728
  if (isEmpty(id)) {
1688
- throw new Error('id cannot be empty');
1729
+ throw new ValidationError('Resource id cannot be empty', {
1730
+ field: 'id',
1731
+ statusCode: 400,
1732
+ retriable: false,
1733
+ suggestion: 'Provide the target id when calling patch().'
1734
+ });
1689
1735
  }
1690
1736
 
1691
1737
  if (!fields || typeof fields !== 'object') {
1692
- throw new Error('fields must be a non-empty object');
1738
+ throw new ValidationError('fields must be a non-empty object', {
1739
+ field: 'fields',
1740
+ statusCode: 400,
1741
+ retriable: false,
1742
+ suggestion: 'Pass a plain object with the fields to update (e.g. { status: "active" }).'
1743
+ });
1693
1744
  }
1694
1745
 
1695
1746
  // Execute beforePatch hooks
@@ -1843,11 +1894,21 @@ export class Resource extends AsyncEventEmitter {
1843
1894
  */
1844
1895
  async replace(id, fullData, options = {}) {
1845
1896
  if (isEmpty(id)) {
1846
- throw new Error('id cannot be empty');
1897
+ throw new ValidationError('Resource id cannot be empty', {
1898
+ field: 'id',
1899
+ statusCode: 400,
1900
+ retriable: false,
1901
+ suggestion: 'Provide the target id when calling replace().'
1902
+ });
1847
1903
  }
1848
1904
 
1849
1905
  if (!fullData || typeof fullData !== 'object') {
1850
- throw new Error('fullData must be a non-empty object');
1906
+ throw new ValidationError('fullData must be a non-empty object', {
1907
+ field: 'fullData',
1908
+ statusCode: 400,
1909
+ retriable: false,
1910
+ suggestion: 'Pass a plain object containing the full resource payload to replace().'
1911
+ });
1851
1912
  }
1852
1913
 
1853
1914
  // Execute beforeReplace hooks
@@ -1921,7 +1982,14 @@ export class Resource extends AsyncEventEmitter {
1921
1982
 
1922
1983
  // Only throw if behavior is 'body-only' and body is empty
1923
1984
  if (this.behavior === 'body-only' && (!body || body === "")) {
1924
- throw new Error(`[Resource.replace] Attempt to save object without body! Data: id=${id}, resource=${this.name}`);
1985
+ throw new ResourceError('Body required for body-only behavior', {
1986
+ resourceName: this.name,
1987
+ operation: 'replace',
1988
+ id,
1989
+ statusCode: 400,
1990
+ retriable: false,
1991
+ suggestion: 'Include a request body when using behavior "body-only" or switch to "body-overflow".'
1992
+ });
1925
1993
  }
1926
1994
 
1927
1995
  // Store to S3 (overwrites if exists, creates if not - true replace/upsert)
@@ -2000,12 +2068,22 @@ export class Resource extends AsyncEventEmitter {
2000
2068
  */
2001
2069
  async updateConditional(id, attributes, options = {}) {
2002
2070
  if (isEmpty(id)) {
2003
- throw new Error('id cannot be empty');
2071
+ throw new ValidationError('Resource id cannot be empty', {
2072
+ field: 'id',
2073
+ statusCode: 400,
2074
+ retriable: false,
2075
+ suggestion: 'Provide the target id when calling updateConditional().'
2076
+ });
2004
2077
  }
2005
2078
 
2006
2079
  const { ifMatch } = options;
2007
2080
  if (!ifMatch) {
2008
- throw new Error('updateConditional requires ifMatch option with ETag value');
2081
+ throw new ValidationError('updateConditional requires ifMatch option with ETag value', {
2082
+ field: 'ifMatch',
2083
+ statusCode: 428,
2084
+ retriable: false,
2085
+ suggestion: 'Pass the current object ETag in options.ifMatch to enable conditional updates.'
2086
+ });
2009
2087
  }
2010
2088
 
2011
2089
  // Check if resource exists
@@ -2214,7 +2292,12 @@ export class Resource extends AsyncEventEmitter {
2214
2292
  */
2215
2293
  async delete(id) {
2216
2294
  if (isEmpty(id)) {
2217
- throw new Error('id cannot be empty');
2295
+ throw new ValidationError('Resource id cannot be empty', {
2296
+ field: 'id',
2297
+ statusCode: 400,
2298
+ retriable: false,
2299
+ suggestion: 'Provide the target id when calling delete().'
2300
+ });
2218
2301
  }
2219
2302
 
2220
2303
  let objectData;
@@ -3006,7 +3089,8 @@ export class Resource extends AsyncEventEmitter {
3006
3089
  const key = this.getResourceKey(id);
3007
3090
  const [ok, err, response] = await tryFn(() => this.client.getObject(key));
3008
3091
  if (!ok) {
3009
- if (err.name === "NoSuchKey") {
3092
+ // Check multiple ways the error might indicate "not found"
3093
+ if (err.name === "NoSuchKey" || err.code === "NoSuchKey" || err.Code === "NoSuchKey" || err.statusCode === 404) {
3010
3094
  return {
3011
3095
  buffer: null,
3012
3096
  contentType: null
@@ -3801,7 +3885,15 @@ export class Resource extends AsyncEventEmitter {
3801
3885
  let idx = -1;
3802
3886
  const stack = this._middlewares.get(method);
3803
3887
  const dispatch = async (i) => {
3804
- if (i <= idx) throw new Error('next() called multiple times');
3888
+ if (i <= idx) {
3889
+ throw new ResourceError('Resource middleware next() called multiple times', {
3890
+ resourceName: this.name,
3891
+ operation: method,
3892
+ statusCode: 500,
3893
+ retriable: false,
3894
+ suggestion: 'Ensure each middleware awaits next() at most once.'
3895
+ });
3896
+ }
3805
3897
  idx = i;
3806
3898
  if (i < stack.length) {
3807
3899
  return await stack[i](ctx, () => dispatch(i + 1));
@@ -3866,10 +3958,14 @@ export class Resource extends AsyncEventEmitter {
3866
3958
 
3867
3959
  const throwIfNoStateMachine = () => {
3868
3960
  if (!resource._stateMachine) {
3869
- throw new Error(
3870
- `No state machine configured for resource '${resource.name}'. ` +
3871
- `Ensure StateMachinePlugin is installed and configured for this resource.`
3872
- );
3961
+ throw new ResourceError(`State machine not configured for resource '${resource.name}'`, {
3962
+ resourceName: resource.name,
3963
+ operation: 'stateMachine',
3964
+ statusCode: 400,
3965
+ retriable: false,
3966
+ suggestion: 'Install and configure the StateMachinePlugin before calling resource.state.* methods.',
3967
+ docs: 'https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/state-machine.md'
3968
+ });
3873
3969
  }
3874
3970
  };
3875
3971
 
@@ -19,6 +19,8 @@
19
19
  * const users = await UserFactory.createMany(10);
20
20
  */
21
21
 
22
+ import { ValidationError } from '../errors.js';
23
+
22
24
  export class Factory {
23
25
  /**
24
26
  * Global sequence counter
@@ -161,7 +163,13 @@ export class Factory {
161
163
  for (const traitName of traits) {
162
164
  const trait = this.traits.get(traitName);
163
165
  if (!trait) {
164
- throw new Error(`Trait '${traitName}' not found in factory '${this.resourceName}'`);
166
+ throw new ValidationError(`Trait '${traitName}' not found in factory '${this.resourceName}'`, {
167
+ field: 'trait',
168
+ value: traitName,
169
+ resourceName: this.resourceName,
170
+ retriable: false,
171
+ suggestion: `Define the trait with Factory.define('${this.resourceName}').trait('${traitName}', ...) before using it.`
172
+ });
165
173
  }
166
174
 
167
175
  const traitAttrs = typeof trait === 'function'
@@ -194,7 +202,11 @@ export class Factory {
194
202
  const { database = Factory._database } = options;
195
203
 
196
204
  if (!database) {
197
- throw new Error('Database not set. Use Factory.setDatabase(db) or pass database option');
205
+ throw new ValidationError('Database not set for factory', {
206
+ field: 'database',
207
+ retriable: false,
208
+ suggestion: 'Call Factory.setDatabase(db) globally or pass { database } when invoking create().'
209
+ });
198
210
  }
199
211
 
200
212
  // Build attributes
@@ -208,7 +220,12 @@ export class Factory {
208
220
  // Get resource
209
221
  const resource = database.resources[this.resourceName];
210
222
  if (!resource) {
211
- throw new Error(`Resource '${this.resourceName}' not found in database`);
223
+ throw new ValidationError(`Resource '${this.resourceName}' not found in database`, {
224
+ field: 'resourceName',
225
+ value: this.resourceName,
226
+ retriable: false,
227
+ suggestion: `Ensure the resource is created in the database before using Factory '${this.resourceName}'.`
228
+ });
212
229
  }
213
230
 
214
231
  // Create in database
@@ -16,6 +16,7 @@
16
16
  */
17
17
 
18
18
  import { Factory } from './factory.class.js';
19
+ import { ValidationError } from '../errors.js';
19
20
 
20
21
  export class Seeder {
21
22
  /**
@@ -59,7 +60,12 @@ export class Seeder {
59
60
 
60
61
  const factory = Factory.get(resourceName);
61
62
  if (!factory) {
62
- throw new Error(`Factory for '${resourceName}' not found. Define it with Factory.define()`);
63
+ throw new ValidationError(`Factory for '${resourceName}' not found`, {
64
+ field: 'resourceName',
65
+ value: resourceName,
66
+ retriable: false,
67
+ suggestion: `Register a factory with Factory.define('${resourceName}', ...) before seeding.`
68
+ });
63
69
  }
64
70
 
65
71
  created[resourceName] = await factory.createMany(count, {}, { database: this.database });