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
@@ -1,5 +1,6 @@
1
1
  import { Plugin } from "./plugin.class.js";
2
2
  import tryFn from "../concerns/try-fn.js";
3
+ import { PluginError } from "../errors.js";
3
4
 
4
5
  /**
5
6
  * GeoPlugin - Geospatial Queries and Location-Based Features
@@ -137,9 +138,14 @@ export class GeoPlugin extends Plugin {
137
138
 
138
139
  // Validate configuration
139
140
  if (!config.latField || !config.lonField) {
140
- throw new Error(
141
- `[GeoPlugin] Resource "${resourceName}" must have "latField" and "lonField" configured`
142
- );
141
+ throw new PluginError(`[GeoPlugin] Resource "${resourceName}" must have "latField" and "lonField" configured`, {
142
+ pluginName: 'GeoPlugin',
143
+ operation: 'setupResource',
144
+ resourceName,
145
+ statusCode: 400,
146
+ retriable: false,
147
+ suggestion: 'Update GeoPlugin configuration with { latField: "...", lonField: "..." } for the resource.'
148
+ });
143
149
  }
144
150
 
145
151
  if (!config.precision || config.precision < 1 || config.precision > 12) {
@@ -327,7 +333,14 @@ export class GeoPlugin extends Plugin {
327
333
  */
328
334
  resource.findNearby = async function({ lat, lon, radius = 10, limit = 100 }) {
329
335
  if (lat === undefined || lon === undefined) {
330
- throw new Error('lat and lon are required for findNearby');
336
+ throw new PluginError('Latitude and longitude are required for findNearby()', {
337
+ pluginName: 'GeoPlugin',
338
+ operation: 'findNearby',
339
+ resourceName: resource.name,
340
+ statusCode: 400,
341
+ retriable: false,
342
+ suggestion: 'Call findNearby({ lat, lon, radius }) with both coordinates.'
343
+ });
331
344
  }
332
345
 
333
346
  const longitude = lon; // Alias for internal use
@@ -430,7 +443,14 @@ export class GeoPlugin extends Plugin {
430
443
  */
431
444
  resource.findInBounds = async function({ north, south, east, west, limit = 100 }) {
432
445
  if (north === undefined || south === undefined || east === undefined || west === undefined) {
433
- throw new Error('north, south, east, west are required for findInBounds');
446
+ throw new PluginError('Bounding box requires north, south, east, west coordinates', {
447
+ pluginName: 'GeoPlugin',
448
+ operation: 'findInBounds',
449
+ resourceName: resource.name,
450
+ statusCode: 400,
451
+ retriable: false,
452
+ suggestion: 'Call findInBounds({ north, south, east, west }) with all four boundaries.'
453
+ });
434
454
  }
435
455
 
436
456
  let allRecords = [];
@@ -536,13 +556,30 @@ export class GeoPlugin extends Plugin {
536
556
  ]);
537
557
  } catch (err) {
538
558
  if (err.name === 'NoSuchKey' || err.message?.includes('No such key')) {
539
- throw new Error('One or both records not found');
559
+ throw new PluginError('One or both records not found for distance calculation', {
560
+ pluginName: 'GeoPlugin',
561
+ operation: 'getDistance',
562
+ resourceName: resource.name,
563
+ statusCode: 404,
564
+ retriable: false,
565
+ suggestion: 'Ensure both record IDs exist before calling getDistance().',
566
+ ids: [id1, id2],
567
+ original: err
568
+ });
540
569
  }
541
570
  throw err;
542
571
  }
543
572
 
544
573
  if (!record1 || !record2) {
545
- throw new Error('One or both records not found');
574
+ throw new PluginError('One or both records not found for distance calculation', {
575
+ pluginName: 'GeoPlugin',
576
+ operation: 'getDistance',
577
+ resourceName: resource.name,
578
+ statusCode: 404,
579
+ retriable: false,
580
+ suggestion: 'Ensure both record IDs exist before calling getDistance().',
581
+ ids: [id1, id2]
582
+ });
546
583
  }
547
584
 
548
585
  const lat1 = record1[config.latField];
@@ -551,7 +588,15 @@ export class GeoPlugin extends Plugin {
551
588
  const lon2 = record2[config.lonField];
552
589
 
553
590
  if (lat1 === undefined || lon1 === undefined || lat2 === undefined || lon2 === undefined) {
554
- throw new Error('One or both records missing coordinates');
591
+ throw new PluginError('One or both records are missing coordinates', {
592
+ pluginName: 'GeoPlugin',
593
+ operation: 'getDistance',
594
+ resourceName: resource.name,
595
+ statusCode: 422,
596
+ retriable: false,
597
+ suggestion: `Check that both records contain ${config.latField} and ${config.lonField} before using geospatial helpers.`,
598
+ ids: [id1, id2]
599
+ });
555
600
  }
556
601
 
557
602
  const distance = plugin.calculateDistance(lat1, lon1, lat2, lon2);
@@ -635,7 +680,14 @@ export class GeoPlugin extends Plugin {
635
680
  const idx = this.base32.indexOf(chr);
636
681
 
637
682
  if (idx === -1) {
638
- throw new Error(`Invalid geohash character: ${chr}`);
683
+ throw new PluginError(`Invalid geohash character: ${chr}`, {
684
+ pluginName: 'GeoPlugin',
685
+ operation: 'decodeGeohash',
686
+ statusCode: 400,
687
+ retriable: false,
688
+ suggestion: 'Ensure geohash strings use the base32 alphabet 0123456789bcdefghjkmnpqrstuvwxyz.',
689
+ geohash
690
+ });
639
691
  }
640
692
 
641
693
  for (let n = 4; n >= 0; n--) {
@@ -0,0 +1,61 @@
1
+ import {
2
+ BASE_USER_ATTRIBUTES,
3
+ BASE_TENANT_ATTRIBUTES,
4
+ BASE_CLIENT_ATTRIBUTES,
5
+ validateResourcesConfig,
6
+ mergeResourceConfig
7
+ } from './resource-schemas.js';
8
+ import { PluginError } from '../../../errors.js';
9
+
10
+ /**
11
+ * Validate and normalize user-provided resource configurations.
12
+ *
13
+ * @param {Object} resourcesOptions
14
+ * @returns {{users: Object, tenants: Object, clients: Object}}
15
+ */
16
+ export function prepareResourceConfigs(resourcesOptions = {}) {
17
+ const resourcesValidation = validateResourcesConfig(resourcesOptions);
18
+ if (!resourcesValidation.valid) {
19
+ throw new PluginError('IdentityPlugin configuration error', {
20
+ pluginName: 'IdentityPlugin',
21
+ operation: 'prepareResourceConfigs',
22
+ statusCode: 400,
23
+ retriable: false,
24
+ suggestion: 'Review the resources configuration and fix the validation errors listed in the metadata.',
25
+ metadata: { validationErrors: resourcesValidation.errors }
26
+ });
27
+ }
28
+
29
+ mergeResourceConfig(
30
+ { attributes: BASE_USER_ATTRIBUTES },
31
+ resourcesOptions.users,
32
+ 'users'
33
+ );
34
+
35
+ mergeResourceConfig(
36
+ { attributes: BASE_TENANT_ATTRIBUTES },
37
+ resourcesOptions.tenants,
38
+ 'tenants'
39
+ );
40
+
41
+ mergeResourceConfig(
42
+ { attributes: BASE_CLIENT_ATTRIBUTES },
43
+ resourcesOptions.clients,
44
+ 'clients'
45
+ );
46
+
47
+ return {
48
+ users: {
49
+ userConfig: resourcesOptions.users,
50
+ mergedConfig: null
51
+ },
52
+ tenants: {
53
+ userConfig: resourcesOptions.tenants,
54
+ mergedConfig: null
55
+ },
56
+ clients: {
57
+ userConfig: resourcesOptions.clients,
58
+ mergedConfig: null
59
+ }
60
+ };
61
+ }
@@ -30,6 +30,7 @@
30
30
 
31
31
  import { requirePluginDependency } from '../../concerns/plugin-dependencies.js';
32
32
  import { idGenerator } from '../../../concerns/id.js';
33
+ import { PluginError } from '../../../errors.js';
33
34
 
34
35
  export class MFAManager {
35
36
  constructor(options = {}) {
@@ -64,7 +65,13 @@ export class MFAManager {
64
65
  */
65
66
  generateEnrollment(accountName) {
66
67
  if (!this.OTPAuth) {
67
- throw new Error('[MFA] OTPAuth library not initialized');
68
+ throw new PluginError('[MFA] OTPAuth library not initialized', {
69
+ pluginName: 'IdentityPlugin',
70
+ operation: 'mfaGenerateEnrollment',
71
+ statusCode: 500,
72
+ retriable: true,
73
+ suggestion: 'Call MFAManager.initialize() before generating enrollments or ensure otpauth dependency installs successfully.'
74
+ });
68
75
  }
69
76
 
70
77
  // Generate TOTP secret
@@ -103,7 +110,13 @@ export class MFAManager {
103
110
  */
104
111
  verifyTOTP(secret, token) {
105
112
  if (!this.OTPAuth) {
106
- throw new Error('[MFA] OTPAuth library not initialized');
113
+ throw new PluginError('[MFA] OTPAuth library not initialized', {
114
+ pluginName: 'IdentityPlugin',
115
+ operation: 'mfaVerify',
116
+ statusCode: 500,
117
+ retriable: true,
118
+ suggestion: 'Initialize MFAManager before verifying codes and confirm otpauth dependency is available.'
119
+ });
107
120
  }
108
121
 
109
122
  try {
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Sliding window rate limiter for IP-based throttling
3
+ */
4
+
5
+ export class RateLimiter {
6
+ /**
7
+ * @param {Object} options
8
+ * @param {number} options.windowMs - Window size in milliseconds
9
+ * @param {number} options.max - Maximum number of hits allowed per window
10
+ */
11
+ constructor(options = {}) {
12
+ this.windowMs = options.windowMs ?? 60000;
13
+ this.max = options.max ?? 10;
14
+ this.buckets = new Map(); // key -> { count, expiresAt }
15
+ }
16
+
17
+ /**
18
+ * Consume a token for the given key
19
+ * @param {string} key - Identifier (usually IP address)
20
+ * @returns {{allowed: boolean, remaining: number, retryAfter: number}}
21
+ */
22
+ consume(key) {
23
+ if (!this.enabled()) {
24
+ return { allowed: true, remaining: Infinity, retryAfter: 0 };
25
+ }
26
+
27
+ const now = Date.now();
28
+ const bucket = this.buckets.get(key);
29
+
30
+ if (!bucket || bucket.expiresAt <= now) {
31
+ const expiresAt = now + this.windowMs;
32
+ this.buckets.set(key, { count: 1, expiresAt });
33
+ this._prune(now);
34
+ return {
35
+ allowed: true,
36
+ remaining: Math.max(this.max - 1, 0),
37
+ retryAfter: 0
38
+ };
39
+ }
40
+
41
+ if (bucket.count < this.max) {
42
+ bucket.count += 1;
43
+ this._prune(now);
44
+ return {
45
+ allowed: true,
46
+ remaining: Math.max(this.max - bucket.count, 0),
47
+ retryAfter: 0
48
+ };
49
+ }
50
+
51
+ const retryAfterMs = bucket.expiresAt - now;
52
+ return {
53
+ allowed: false,
54
+ remaining: 0,
55
+ retryAfter: Math.max(Math.ceil(retryAfterMs / 1000), 1)
56
+ };
57
+ }
58
+
59
+ /**
60
+ * Whether the limiter is active
61
+ * @returns {boolean}
62
+ */
63
+ enabled() {
64
+ return this.max > 0 && this.windowMs > 0;
65
+ }
66
+
67
+ /**
68
+ * Periodically remove expired buckets
69
+ * @private
70
+ */
71
+ _prune(now) {
72
+ if (this.buckets.size > 5000) {
73
+ for (const [key, bucket] of this.buckets.entries()) {
74
+ if (bucket.expiresAt <= now) {
75
+ this.buckets.delete(key);
76
+ }
77
+ }
78
+ }
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Create Hono middleware for API-style responses
84
+ * @param {RateLimiter} limiter
85
+ * @param {(c: import('hono').Context) => string} getKey
86
+ */
87
+ export function createJsonRateLimitMiddleware(limiter, getKey) {
88
+ return async (c, next) => {
89
+ const key = getKey(c);
90
+ const result = limiter.consume(key);
91
+
92
+ if (result.allowed) {
93
+ return await next();
94
+ }
95
+
96
+ c.header('Retry-After', String(result.retryAfter));
97
+ return c.json({
98
+ error: 'too_many_requests',
99
+ error_description: `Too many requests. Try again in ${result.retryAfter} seconds.`
100
+ }, 429);
101
+ };
102
+ }
103
+
104
+ /**
105
+ * Create Hono middleware for browser redirect flows (login UI)
106
+ * @param {RateLimiter} limiter
107
+ * @param {(c: import('hono').Context) => string} getKey
108
+ * @param {(retryAfter: number) => string} buildRedirectUrl
109
+ */
110
+ export function createRedirectRateLimitMiddleware(limiter, getKey, buildRedirectUrl) {
111
+ return async (c, next) => {
112
+ const key = getKey(c);
113
+ const result = limiter.consume(key);
114
+
115
+ if (result.allowed) {
116
+ return await next();
117
+ }
118
+
119
+ const url = buildRedirectUrl(result.retryAfter);
120
+ return c.redirect(url, 302);
121
+ };
122
+ }
123
+
124
+ export default RateLimiter;
@@ -4,6 +4,7 @@
4
4
  * These are the REQUIRED attributes that the Identity Plugin needs to function.
5
5
  * Users can extend these with custom attributes, but cannot override base fields.
6
6
  */
7
+ import { PluginError } from '../../../errors.js';
7
8
 
8
9
  /**
9
10
  * Base attributes for Users resource
@@ -189,7 +190,14 @@ export function mergeResourceConfig(baseConfig, userConfig = {}, resourceType) {
189
190
  `Invalid extra attributes for ${resourceType} resource:`,
190
191
  ...validation.errors.map(err => ` - ${err}`)
191
192
  ].join('\n');
192
- throw new Error(errorMsg);
193
+ throw new PluginError('Invalid extra attributes for identity resource', {
194
+ pluginName: 'IdentityPlugin',
195
+ operation: 'mergeResourceConfig',
196
+ statusCode: 400,
197
+ retriable: false,
198
+ suggestion: 'Update the resource schema to match IdentityPlugin validation requirements.',
199
+ description: errorMsg
200
+ });
193
201
  }
194
202
  }
195
203
 
@@ -10,6 +10,7 @@
10
10
 
11
11
  import { randomBytes } from 'crypto';
12
12
  import { idGenerator } from '../../../concerns/id.js';
13
+ import { PluginError } from '../../../errors.js';
13
14
 
14
15
  /**
15
16
  * Generate a secure random token
@@ -31,7 +32,13 @@ export function generateToken(bytes = 32, encoding = 'hex') {
31
32
  return buffer.toString('base64url');
32
33
 
33
34
  default:
34
- throw new Error(`Invalid encoding: ${encoding}. Use 'hex', 'base64', or 'base64url'.`);
35
+ throw new PluginError(`Invalid encoding: ${encoding}. Use 'hex', 'base64', or 'base64url'.`, {
36
+ pluginName: 'IdentityPlugin',
37
+ operation: 'generateToken',
38
+ statusCode: 400,
39
+ retriable: false,
40
+ suggestion: 'Pass encoding as "hex", "base64", or "base64url" when calling generateToken.'
41
+ });
35
42
  }
36
43
  }
37
44
 
@@ -122,7 +129,13 @@ export function calculateExpiration(duration) {
122
129
  const match = duration.match(/^(\d+)([smhd])$/);
123
130
 
124
131
  if (!match) {
125
- throw new Error(`Invalid duration format: ${duration}. Use '15m', '1h', '7d', etc.`);
132
+ throw new PluginError(`Invalid duration format: ${duration}. Use '15m', '1h', '7d', etc.`, {
133
+ pluginName: 'IdentityPlugin',
134
+ operation: 'calculateExpiration',
135
+ statusCode: 400,
136
+ retriable: false,
137
+ suggestion: 'Provide durations using number + unit (s, m, h, d), for example "30m" or "1d".'
138
+ });
126
139
  }
127
140
 
128
141
  const value = parseInt(match[1], 10);
@@ -134,10 +147,22 @@ export function calculateExpiration(duration) {
134
147
  case 'h': ms = value * 60 * 60 * 1000; break; // hours
135
148
  case 'd': ms = value * 24 * 60 * 60 * 1000; break; // days
136
149
  default:
137
- throw new Error(`Invalid duration unit: ${unit}`);
150
+ throw new PluginError(`Invalid duration unit: ${unit}`, {
151
+ pluginName: 'IdentityPlugin',
152
+ operation: 'calculateExpiration',
153
+ statusCode: 400,
154
+ retriable: false,
155
+ suggestion: 'Use s, m, h, or d for seconds, minutes, hours, or days respectively.'
156
+ });
138
157
  }
139
158
  } else {
140
- throw new Error('Duration must be a string or number');
159
+ throw new PluginError('Duration must be a string or number', {
160
+ pluginName: 'IdentityPlugin',
161
+ operation: 'calculateExpiration',
162
+ statusCode: 400,
163
+ retriable: false,
164
+ suggestion: 'Pass durations as milliseconds (number) or a string like "15m".'
165
+ });
141
166
  }
142
167
 
143
168
  return Date.now() + ms;
@@ -0,0 +1,76 @@
1
+ import { PluginError } from '../../../errors.js';
2
+
3
+ export class AuthDriver {
4
+ constructor(name, supportedTypes = []) {
5
+ this.name = name;
6
+ this.supportedTypes = supportedTypes;
7
+ }
8
+
9
+ /**
10
+ * Called once during plugin initialization with database/config/resources
11
+ * @param {Object} context
12
+ */
13
+ // eslint-disable-next-line no-unused-vars
14
+ async initialize(context) {
15
+ throw new PluginError('AuthDriver.initialize(context) must be implemented by subclasses', {
16
+ pluginName: 'IdentityPlugin',
17
+ operation: 'initializeDriver',
18
+ statusCode: 500,
19
+ retriable: false,
20
+ suggestion: `Implement initialize(context) in ${this.constructor.name} or use one of the provided drivers.`
21
+ });
22
+ }
23
+
24
+ /**
25
+ * Authenticate a request (password, client credentials, etc)
26
+ * @param {Object} request
27
+ */
28
+ // eslint-disable-next-line no-unused-vars
29
+ async authenticate(request) {
30
+ throw new PluginError('AuthDriver.authenticate(request) must be implemented by subclasses', {
31
+ pluginName: 'IdentityPlugin',
32
+ operation: 'authenticateDriver',
33
+ statusCode: 500,
34
+ retriable: false,
35
+ suggestion: `Implement authenticate(request) in ${this.constructor.name} to support the configured grant type.`
36
+ });
37
+ }
38
+
39
+ supportsType(type) {
40
+ if (!type) return false;
41
+ return this.supportedTypes.includes(type);
42
+ }
43
+
44
+ /**
45
+ * Whether the driver supports issuing tokens for the given grant type
46
+ * @param {string} grantType
47
+ */
48
+ // eslint-disable-next-line no-unused-vars
49
+ supportsGrant(grantType) {
50
+ return false;
51
+ }
52
+
53
+ /**
54
+ * Optionally issue tokens (if driver is responsible for it)
55
+ * @param {Object} payload
56
+ */
57
+ // eslint-disable-next-line no-unused-vars
58
+ async issueTokens(payload) {
59
+ throw new PluginError(`AuthDriver ${this.name} does not implement issueTokens`, {
60
+ pluginName: 'IdentityPlugin',
61
+ operation: 'issueTokens',
62
+ statusCode: 500,
63
+ retriable: false,
64
+ suggestion: 'Provide an issueTokens implementation or delegate token issuance to the OAuth2 server.'
65
+ });
66
+ }
67
+
68
+ /**
69
+ * Optionally revoke tokens (if driver manages them)
70
+ * @param {Object} payload
71
+ */
72
+ // eslint-disable-next-line no-unused-vars
73
+ async revokeTokens(payload) {
74
+ return;
75
+ }
76
+ }
@@ -0,0 +1,127 @@
1
+ import { AuthDriver } from './auth-driver.interface.js';
2
+ import { PluginError } from '../../../errors.js';
3
+ import { tryFn } from '../../../concerns/try-fn.js';
4
+
5
+ function constantTimeEqual(a, b) {
6
+ const valueA = Buffer.from(String(a ?? ''), 'utf8');
7
+ const valueB = Buffer.from(String(b ?? ''), 'utf8');
8
+
9
+ if (valueA.length !== valueB.length) {
10
+ return false;
11
+ }
12
+
13
+ let mismatch = 0;
14
+ for (let i = 0; i < valueA.length; i += 1) {
15
+ mismatch |= valueA[i] ^ valueB[i];
16
+ }
17
+ return mismatch === 0;
18
+ }
19
+
20
+ export class ClientCredentialsAuthDriver extends AuthDriver {
21
+ constructor(options = {}) {
22
+ super('client-credentials', ['client_credentials']);
23
+ this.options = options;
24
+ this.clientResource = null;
25
+ this.passwordHelper = null;
26
+ }
27
+
28
+ async initialize(context) {
29
+ this.clientResource = context.resources?.clients;
30
+ if (!this.clientResource) {
31
+ throw new PluginError('ClientCredentialsAuthDriver requires clients resource', {
32
+ pluginName: 'IdentityPlugin',
33
+ operation: 'initializeClientCredentialsDriver',
34
+ statusCode: 500,
35
+ retriable: false,
36
+ suggestion: 'Map a clients resource in IdentityPlugin resources before enabling client credentials authentication.'
37
+ });
38
+ }
39
+ this.passwordHelper = context.helpers?.password || null;
40
+ }
41
+
42
+ supportsGrant(grantType) {
43
+ return grantType === 'client_credentials';
44
+ }
45
+
46
+ async authenticate(request) {
47
+ const { clientId, clientSecret } = request;
48
+ if (!clientId || !clientSecret) {
49
+ return { success: false, error: 'invalid_client', statusCode: 401 };
50
+ }
51
+
52
+ const [ok, err, clients] = await tryFn(() => this.clientResource.query({ clientId }));
53
+ if (!ok) {
54
+ return {
55
+ success: false,
56
+ error: err?.message || 'lookup_failed',
57
+ statusCode: 500
58
+ };
59
+ }
60
+
61
+ if (!clients || clients.length === 0) {
62
+ return { success: false, error: 'invalid_client', statusCode: 401 };
63
+ }
64
+
65
+ const client = clients[0];
66
+
67
+ if (client.active === false) {
68
+ return { success: false, error: 'inactive_client', statusCode: 403 };
69
+ }
70
+
71
+ const secrets = [];
72
+ if (Array.isArray(client.secrets)) {
73
+ secrets.push(...client.secrets);
74
+ }
75
+ if (client.clientSecret) {
76
+ secrets.push(client.clientSecret);
77
+ }
78
+ if (client.secret) {
79
+ secrets.push(client.secret);
80
+ }
81
+
82
+ if (!secrets.length) {
83
+ return { success: false, error: 'invalid_client', statusCode: 401 };
84
+ }
85
+
86
+ const secretMatches = await this._verifyAgainstSecrets(clientSecret, secrets);
87
+ if (!secretMatches) {
88
+ return { success: false, error: 'invalid_client', statusCode: 401 };
89
+ }
90
+
91
+ const sanitizedClient = { ...client };
92
+ if (sanitizedClient.clientSecret !== undefined) {
93
+ delete sanitizedClient.clientSecret;
94
+ }
95
+ if (sanitizedClient.secret !== undefined) {
96
+ delete sanitizedClient.secret;
97
+ }
98
+ if (sanitizedClient.secrets !== undefined) {
99
+ delete sanitizedClient.secrets;
100
+ }
101
+
102
+ return {
103
+ success: true,
104
+ client: sanitizedClient
105
+ };
106
+ }
107
+
108
+ async _verifyAgainstSecrets(providedSecret, storedSecrets) {
109
+ for (const storedSecret of storedSecrets) {
110
+ if (!storedSecret) continue;
111
+
112
+ if (typeof storedSecret === 'string' && storedSecret.startsWith('$') && this.passwordHelper?.verify) {
113
+ const ok = await this.passwordHelper.verify(providedSecret, storedSecret);
114
+ if (ok) {
115
+ return true;
116
+ }
117
+ continue;
118
+ }
119
+
120
+ if (constantTimeEqual(providedSecret, storedSecret)) {
121
+ return true;
122
+ }
123
+ }
124
+
125
+ return false;
126
+ }
127
+ }
@@ -0,0 +1,18 @@
1
+ import { PasswordAuthDriver } from './password-driver.js';
2
+ import { ClientCredentialsAuthDriver } from './client-credentials-driver.js';
3
+
4
+ export function createBuiltInAuthDrivers(options = {}) {
5
+ const drivers = [];
6
+
7
+ if (options.disablePassword !== true) {
8
+ drivers.push(new PasswordAuthDriver(options.password || {}));
9
+ }
10
+
11
+ if (options.disableClientCredentials !== true) {
12
+ drivers.push(new ClientCredentialsAuthDriver(options.clientCredentials || {}));
13
+ }
14
+
15
+ return drivers;
16
+ }
17
+
18
+ export { PasswordAuthDriver, ClientCredentialsAuthDriver };