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
@@ -37,6 +37,7 @@ import { requirePluginDependency } from '../concerns/plugin-dependencies.js';
37
37
  import tryFn from '../../concerns/try-fn.js';
38
38
  import { ApiServer } from './server.js';
39
39
  import { idGenerator } from '../../concerns/id.js';
40
+ import { resolveResourceName } from '../concerns/resource-names.js';
40
41
 
41
42
  const AUTH_DRIVER_KEYS = ['jwt', 'apiKey', 'basic', 'oidc', 'oauth2'];
42
43
 
@@ -61,9 +62,10 @@ function normalizeAuthConfig(authOptions = {}) {
61
62
  pathAuth: authOptions.pathAuth,
62
63
  strategy: authOptions.strategy || 'any',
63
64
  priorities: authOptions.priorities || {},
64
- resource: authOptions.resource || 'users',
65
+ resource: authOptions.resource,
65
66
  usernameField: authOptions.usernameField || 'email',
66
- passwordField: authOptions.passwordField || 'password'
67
+ passwordField: authOptions.passwordField || 'password',
68
+ createResource: authOptions.createResource !== false
67
69
  };
68
70
 
69
71
  const seen = new Set();
@@ -130,7 +132,29 @@ export class ApiPlugin extends Plugin {
130
132
  constructor(options = {}) {
131
133
  super(options);
132
134
 
135
+ const resourceNamesOption = options.resourceNames || {};
136
+ this._usersResourceDescriptor = {
137
+ defaultName: 'plg_api_users',
138
+ override: resourceNamesOption.authUsers || options.auth?.resource
139
+ };
133
140
  const normalizedAuth = normalizeAuthConfig(options.auth);
141
+ normalizedAuth.registration = {
142
+ enabled: options.auth?.registration?.enabled === true,
143
+ allowedFields: Array.isArray(options.auth?.registration?.allowedFields)
144
+ ? options.auth.registration.allowedFields
145
+ : [],
146
+ defaultRole: options.auth?.registration?.defaultRole || 'user'
147
+ };
148
+ normalizedAuth.loginThrottle = {
149
+ enabled: options.auth?.loginThrottle?.enabled !== false,
150
+ maxAttempts: options.auth?.loginThrottle?.maxAttempts || 5,
151
+ windowMs: options.auth?.loginThrottle?.windowMs || 60_000,
152
+ blockDurationMs: options.auth?.loginThrottle?.blockDurationMs || 300_000,
153
+ maxEntries: options.auth?.loginThrottle?.maxEntries || 10_000
154
+ };
155
+ this.usersResourceName = this._resolveUsersResourceName();
156
+ normalizedAuth.resource = this.usersResourceName;
157
+ normalizedAuth.createResource = options.auth?.createResource !== false;
134
158
 
135
159
  this.config = {
136
160
  // Server configuration
@@ -182,7 +206,8 @@ export class ApiPlugin extends Plugin {
182
206
  enabled: options.rateLimit?.enabled || false,
183
207
  windowMs: options.rateLimit?.windowMs || 60000, // 1 minute
184
208
  maxRequests: options.rateLimit?.maxRequests || 100,
185
- keyGenerator: options.rateLimit?.keyGenerator || null
209
+ keyGenerator: options.rateLimit?.keyGenerator || null,
210
+ maxUniqueKeys: options.rateLimit?.maxUniqueKeys || 1000
186
211
  },
187
212
 
188
213
  // Logging configuration
@@ -288,7 +313,22 @@ export class ApiPlugin extends Plugin {
288
313
  },
289
314
 
290
315
  // Custom global middlewares
291
- middlewares: options.middlewares || []
316
+ middlewares: options.middlewares || [],
317
+
318
+ requestId: options.requestId || { enabled: false },
319
+ sessionTracking: options.sessionTracking || { enabled: false },
320
+ events: options.events || { enabled: false },
321
+ metrics: options.metrics || { enabled: false },
322
+ failban: {
323
+ ...(options.failban || {}),
324
+ enabled: options.failban?.enabled === true,
325
+ resourceNames: resourceNamesOption.failban || options.failban?.resourceNames || {}
326
+ },
327
+ static: Array.isArray(options.static) ? options.static : [],
328
+ health: typeof options.health === 'object'
329
+ ? options.health
330
+ : { enabled: options.health !== false },
331
+ maxBodySize: options.maxBodySize || 10 * 1024 * 1024
292
332
  };
293
333
 
294
334
  this.config.resources = this._normalizeResourcesConfig(options.resources);
@@ -410,18 +450,43 @@ export class ApiPlugin extends Plugin {
410
450
  * @private
411
451
  */
412
452
  async _createUsersResource() {
453
+ const existingResource = this._findExistingUsersResource();
454
+
455
+ if (!this.config.auth.createResource) {
456
+ if (!existingResource) {
457
+ throw new Error(
458
+ `[API Plugin] Auth resource "${this.usersResourceName}" not found and auth.createResource is false`
459
+ );
460
+ }
461
+ this.usersResource = existingResource;
462
+ this.config.auth.resource = existingResource.name;
463
+ if (this.config.verbose) {
464
+ console.log(`[API Plugin] Using existing ${existingResource.name} resource for authentication`);
465
+ }
466
+ return;
467
+ }
468
+
469
+ if (existingResource) {
470
+ this.usersResource = existingResource;
471
+ this.config.auth.resource = existingResource.name;
472
+ if (this.config.verbose) {
473
+ console.log(`[API Plugin] Reusing existing ${existingResource.name} resource for authentication`);
474
+ }
475
+ return;
476
+ }
477
+
413
478
  const [ok, err, resource] = await tryFn(() =>
414
479
  this.database.createResource({
415
- name: 'plg_users',
480
+ name: this.usersResourceName,
416
481
  attributes: {
417
482
  id: 'string|required',
418
483
  username: 'string|required|minlength:3',
419
- email: 'string|required|email', // Required to support email-based auth
484
+ email: 'string|required|email',
420
485
  password: 'secret|required|minlength:8',
421
486
  apiKey: 'string|optional',
422
487
  jwtSecret: 'string|optional',
423
488
  role: 'string|default:user',
424
- scopes: 'array|items:string|optional', // Authorization scopes (e.g., ['read:users', 'write:cars'])
489
+ scopes: 'array|items:string|optional',
425
490
  active: 'boolean|default:true',
426
491
  createdAt: 'string|optional',
427
492
  lastLoginAt: 'string|optional',
@@ -433,20 +498,40 @@ export class ApiPlugin extends Plugin {
433
498
  })
434
499
  );
435
500
 
436
- if (ok) {
437
- this.usersResource = resource;
438
- if (this.config.verbose) {
439
- console.log('[API Plugin] Created plg_users resource for authentication');
501
+ if (!ok) {
502
+ throw err;
503
+ }
504
+
505
+ this.usersResource = resource;
506
+ this.config.auth.resource = resource.name;
507
+ if (this.config.verbose) {
508
+ console.log(`[API Plugin] Created ${this.usersResourceName} resource for authentication`);
509
+ }
510
+ }
511
+
512
+ _findExistingUsersResource() {
513
+ const candidates = new Set([this.usersResourceName]);
514
+
515
+ const identityPlugin = this.database?.plugins?.identity || this.database?.plugins?.Identity;
516
+ if (identityPlugin) {
517
+ const identityNames = [
518
+ identityPlugin.usersResource?.name,
519
+ identityPlugin.config?.resources?.users?.mergedConfig?.name,
520
+ identityPlugin.config?.resources?.users?.userConfig?.name
521
+ ].filter(Boolean);
522
+ for (const name of identityNames) {
523
+ candidates.add(name);
440
524
  }
441
- } else if (this.database.resources.plg_users) {
442
- // Resource already exists
443
- this.usersResource = this.database.resources.plg_users;
444
- if (this.config.verbose) {
445
- console.log('[API Plugin] Using existing plg_users resource');
525
+ }
526
+
527
+ for (const name of candidates) {
528
+ if (!name) continue;
529
+ const resource = this.database.resources?.[name];
530
+ if (resource) {
531
+ return resource;
446
532
  }
447
- } else {
448
- throw err;
449
533
  }
534
+ return null;
450
535
  }
451
536
  /**
452
537
  * Setup middlewares
@@ -584,25 +669,41 @@ export class ApiPlugin extends Plugin {
584
669
  */
585
670
  async _createRateLimitMiddleware() {
586
671
  const requests = new Map();
587
- const { windowMs, maxRequests, keyGenerator } = this.config.rateLimit;
672
+ const { windowMs, maxRequests, keyGenerator, maxUniqueKeys } = this.config.rateLimit;
673
+
674
+ const getClientIp = (c) => {
675
+ const forwarded = c.req.header('x-forwarded-for');
676
+ if (forwarded) {
677
+ return forwarded.split(',')[0].trim();
678
+ }
679
+ const cfConnecting = c.req.header('cf-connecting-ip');
680
+ if (cfConnecting) {
681
+ return cfConnecting;
682
+ }
683
+ return c.req.raw?.socket?.remoteAddress || 'unknown';
684
+ };
588
685
 
589
686
  return async (c, next) => {
590
687
  // Generate key (IP or custom)
591
- const key = keyGenerator
592
- ? keyGenerator(c)
593
- : c.req.header('x-forwarded-for') || c.req.header('cf-connecting-ip') || 'unknown';
688
+ const key = keyGenerator ? keyGenerator(c) : getClientIp(c) || 'unknown';
594
689
 
595
- // Get or create request count
596
- if (!requests.has(key)) {
597
- requests.set(key, { count: 0, resetAt: Date.now() + windowMs });
598
- }
690
+ let record = requests.get(key);
599
691
 
600
- const record = requests.get(key);
692
+ // Reset expired records to prevent unbounded memory growth
693
+ if (record && Date.now() > record.resetAt) {
694
+ requests.delete(key);
695
+ record = null;
696
+ }
601
697
 
602
- // Reset if window expired
603
- if (Date.now() > record.resetAt) {
604
- record.count = 0;
605
- record.resetAt = Date.now() + windowMs;
698
+ if (!record) {
699
+ record = { count: 0, resetAt: Date.now() + windowMs };
700
+ requests.set(key, record);
701
+ if (requests.size > maxUniqueKeys) {
702
+ const oldestKey = requests.keys().next().value;
703
+ if (oldestKey) {
704
+ requests.delete(oldestKey);
705
+ }
706
+ }
606
707
  }
607
708
 
608
709
  // Check limit
@@ -706,14 +807,15 @@ export class ApiPlugin extends Plugin {
706
807
  return;
707
808
  }
708
809
 
709
- // Skip if already compressed
710
- if (c.res.headers.has('content-encoding')) {
810
+ // Skip if already compressed or body consumed
811
+ if (c.res.headers.has('content-encoding') || c.res.bodyUsed) {
711
812
  return;
712
813
  }
713
814
 
714
815
  // Skip if content-type should not be compressed
715
816
  const contentType = c.res.headers.get('content-type') || '';
716
- if (skipContentTypes.some(type => contentType.startsWith(type))) {
817
+ const isTextLike = contentType.startsWith('text/') || contentType.includes('json');
818
+ if (skipContentTypes.some(type => contentType.startsWith(type)) || !isTextLike) {
717
819
  return;
718
820
  }
719
821
 
@@ -909,11 +1011,22 @@ export class ApiPlugin extends Plugin {
909
1011
  port: this.config.port,
910
1012
  host: this.config.host,
911
1013
  database: this.database,
1014
+ namespace: this.namespace,
912
1015
  versionPrefix: this.config.versionPrefix,
913
1016
  resources: this.config.resources,
914
1017
  routes: this.config.routes,
915
1018
  templates: this.config.templates,
916
1019
  middlewares: this.compiledMiddlewares,
1020
+ cors: this.config.cors,
1021
+ security: this.config.security,
1022
+ requestId: this.config.requestId,
1023
+ sessionTracking: this.config.sessionTracking,
1024
+ events: this.config.events,
1025
+ metrics: this.config.metrics,
1026
+ failban: this.config.failban,
1027
+ static: this.config.static,
1028
+ health: this.config.health,
1029
+ maxBodySize: this.config.maxBodySize,
917
1030
  verbose: this.config.verbose,
918
1031
  auth: this.config.auth,
919
1032
  docsEnabled: this.config.docs.enabled,
@@ -942,10 +1055,24 @@ export class ApiPlugin extends Plugin {
942
1055
 
943
1056
  if (this.server) {
944
1057
  await this.server.stop();
945
- this.server = null;
946
1058
  }
1059
+ this.server = null;
1060
+ }
1061
+
1062
+ _resolveUsersResourceName() {
1063
+ return resolveResourceName('api', this._usersResourceDescriptor, {
1064
+ namespace: this.namespace
1065
+ });
1066
+ }
947
1067
 
948
- this.emit('plugin.stopped');
1068
+ onNamespaceChanged() {
1069
+ this.usersResourceName = this._resolveUsersResourceName();
1070
+ if (this.config?.auth) {
1071
+ this.config.auth.resource = this.usersResourceName;
1072
+ }
1073
+ if (this.server?.failban) {
1074
+ this.server.failban.setNamespace(this.namespace);
1075
+ }
949
1076
  }
950
1077
 
951
1078
  /**
@@ -959,11 +1086,9 @@ export class ApiPlugin extends Plugin {
959
1086
 
960
1087
  // Optionally delete users resource
961
1088
  if (purgeData && this.usersResource) {
962
- // Delete all users (plugin data cleanup happens automatically via base Plugin class)
963
- const [ok] = await tryFn(() => this.database.deleteResource('plg_users'));
964
-
1089
+ const [ok] = await tryFn(() => this.database.deleteResource(this.usersResourceName));
965
1090
  if (ok && this.config.verbose) {
966
- console.log('[API Plugin] Deleted plg_users resource');
1091
+ console.log(`[API Plugin] Deleted ${this.usersResourceName} resource`);
967
1092
  }
968
1093
  }
969
1094
 
@@ -1006,3 +1131,6 @@ export {
1006
1131
  createNotificationStateMachine,
1007
1132
  createAttemptStateMachine
1008
1133
  } from './concerns/state-machine.js';
1134
+
1135
+ // Export route context utilities (NEW!)
1136
+ export { RouteContext, withContext } from './concerns/route-context.js';
@@ -9,8 +9,8 @@ import { asyncHandler } from '../utils/error-handler.js';
9
9
  import * as formatter from '../utils/response-formatter.js';
10
10
  import { createToken } from '../auth/jwt-auth.js';
11
11
  import { generateApiKey } from '../auth/api-key-auth.js';
12
- import { encrypt } from '../../../concerns/crypto.js';
13
12
  import tryFn from '../../../concerns/try-fn.js';
13
+ import { hashPassword } from '../../../concerns/password-hashing.js';
14
14
 
15
15
  /**
16
16
  * Create authentication routes
@@ -27,16 +27,133 @@ export function createAuthRoutes(authResource, config = {}) {
27
27
  jwtSecret,
28
28
  jwtExpiresIn = '7d',
29
29
  passphrase = 'secret',
30
- allowRegistration = true
30
+ registration = {},
31
+ loginThrottle = {}
31
32
  } = config;
32
33
 
34
+ const registrationConfig = {
35
+ enabled: registration.enabled === true,
36
+ allowedFields: Array.isArray(registration.allowedFields) ? registration.allowedFields : [],
37
+ defaultRole: registration.defaultRole || 'user'
38
+ };
39
+
40
+ const schemaAttributes = authResource.schema?.attributes || {};
41
+ const passwordAttribute = schemaAttributes?.[passwordField];
42
+ const isPasswordType = typeof passwordAttribute === 'string'
43
+ ? passwordAttribute.includes('password')
44
+ : passwordAttribute?.type === 'password';
45
+
46
+ const allowedRegistrationFields = new Set([usernameField, passwordField]);
47
+ for (const field of registrationConfig.allowedFields) {
48
+ if (typeof field === 'string' && field && field !== passwordField) {
49
+ allowedRegistrationFields.add(field);
50
+ }
51
+ }
52
+
53
+ const blockedRegistrationFields = new Set([
54
+ 'role',
55
+ 'active',
56
+ 'apiKey',
57
+ 'jwtSecret',
58
+ 'scopes',
59
+ 'createdAt',
60
+ 'updatedAt',
61
+ 'metadata',
62
+ 'id'
63
+ ]);
64
+
65
+ const loginThrottleConfig = {
66
+ enabled: loginThrottle?.enabled !== false,
67
+ maxAttempts: loginThrottle?.maxAttempts ?? 5,
68
+ windowMs: loginThrottle?.windowMs ?? 60_000,
69
+ blockDurationMs: loginThrottle?.blockDurationMs ?? 300_000,
70
+ maxEntries: loginThrottle?.maxEntries ?? 10_000
71
+ };
72
+
73
+ const loginAttempts = new Map();
74
+
75
+ const getClientIp = (c) => {
76
+ const forwarded = c.req.header('x-forwarded-for');
77
+ if (forwarded) {
78
+ return forwarded.split(',')[0].trim();
79
+ }
80
+ const cfConnecting = c.req.header('cf-connecting-ip');
81
+ if (cfConnecting) {
82
+ return cfConnecting;
83
+ }
84
+ return c.req.raw?.socket?.remoteAddress || 'unknown';
85
+ };
86
+
87
+ const cleanupLoginAttempts = () => {
88
+ if (loginAttempts.size <= loginThrottleConfig.maxEntries) {
89
+ return;
90
+ }
91
+ const oldestKey = loginAttempts.keys().next().value;
92
+ if (oldestKey) {
93
+ loginAttempts.delete(oldestKey);
94
+ }
95
+ };
96
+
97
+ const getThrottleRecord = (key, now) => {
98
+ if (!loginThrottleConfig.enabled) return null;
99
+ let record = loginAttempts.get(key);
100
+ if (record && record.blockedUntil && now > record.blockedUntil) {
101
+ loginAttempts.delete(key);
102
+ record = null;
103
+ }
104
+ if (!record || now - record.firstAttemptAt > loginThrottleConfig.windowMs) {
105
+ record = { attempts: 0, firstAttemptAt: now, blockedUntil: null };
106
+ loginAttempts.set(key, record);
107
+ cleanupLoginAttempts();
108
+ }
109
+ return record;
110
+ };
111
+
112
+ const registerFailedAttempt = (record, now) => {
113
+ if (!loginThrottleConfig.enabled || !record) {
114
+ return { blocked: false };
115
+ }
116
+ record.attempts += 1;
117
+ record.lastAttemptAt = now;
118
+ if (record.attempts >= loginThrottleConfig.maxAttempts) {
119
+ record.blockedUntil = now + loginThrottleConfig.blockDurationMs;
120
+ const retryAfter = Math.ceil((record.blockedUntil - now) / 1000);
121
+ return { blocked: true, retryAfter };
122
+ }
123
+ return { blocked: false };
124
+ };
125
+
126
+ const buildPublicUser = (user) => {
127
+ const publicUser = { id: user.id };
128
+ const identifier = user[usernameField] ?? user.email ?? user.username;
129
+ if (identifier !== undefined) {
130
+ publicUser[usernameField] = identifier;
131
+ }
132
+
133
+ for (const field of allowedRegistrationFields) {
134
+ if (field === usernameField || field === passwordField) continue;
135
+ if (user[field] !== undefined) {
136
+ publicUser[field] = user[field];
137
+ }
138
+ }
139
+
140
+ if (schemaAttributes.role !== undefined && user.role !== undefined) {
141
+ publicUser.role = user.role;
142
+ }
143
+
144
+ if (schemaAttributes.active !== undefined && user.active !== undefined) {
145
+ publicUser.active = user.active;
146
+ }
147
+
148
+ return publicUser;
149
+ };
150
+
33
151
  // POST /auth/register - Register new user
34
- if (allowRegistration) {
152
+ if (registrationConfig.enabled) {
35
153
  app.post('/register', asyncHandler(async (c) => {
36
154
  const data = await c.req.json();
37
155
  const username = data[usernameField];
38
156
  const password = data[passwordField];
39
- const role = data.role || 'user';
40
157
 
41
158
  // Validate input
42
159
  if (!username || !password) {
@@ -68,17 +185,28 @@ export function createAuthRoutes(authResource, config = {}) {
68
185
  // Create user with dynamic fields
69
186
  // Only include fields from request + required auth fields
70
187
  const { id, ...dataWithoutId } = data; // Exclude id from request data
71
- const userData = {
72
- ...dataWithoutId, // Include all fields from request except id
73
- [usernameField]: username, // Override to ensure correct value
74
- [passwordField]: password // Will be auto-encrypted by schema (secret field)
75
- };
76
-
77
- // Add optional fields only if not provided
78
- if (!userData.role) {
79
- userData.role = role;
188
+ const userData = {};
189
+
190
+ for (const [key, value] of Object.entries(dataWithoutId)) {
191
+ if (!allowedRegistrationFields.has(key)) continue;
192
+ if (blockedRegistrationFields.has(key)) continue;
193
+ if (key === usernameField || key === passwordField) continue;
194
+ userData[key] = value;
80
195
  }
81
- if (userData.active === undefined) {
196
+
197
+ userData[usernameField] = username;
198
+
199
+ if (isPasswordType) {
200
+ userData[passwordField] = password;
201
+ } else {
202
+ userData[passwordField] = await hashPassword(password);
203
+ }
204
+
205
+ if (schemaAttributes.role !== undefined) {
206
+ userData.role = registrationConfig.defaultRole;
207
+ }
208
+
209
+ if (schemaAttributes.active !== undefined) {
82
210
  userData.active = true;
83
211
  }
84
212
 
@@ -98,11 +226,8 @@ export function createAuthRoutes(authResource, config = {}) {
98
226
  );
99
227
  }
100
228
 
101
- // Remove sensitive data from response
102
- const { [passwordField]: _, ...userWithoutPassword } = user;
103
-
104
229
  const response = formatter.created({
105
- user: userWithoutPassword,
230
+ user: buildPublicUser(user),
106
231
  ...(token && { token }) // Only include token if JWT driver
107
232
  }, `/auth/users/${user.id}`);
108
233
 
@@ -127,6 +252,25 @@ export function createAuthRoutes(authResource, config = {}) {
127
252
  const queryFilter = { [usernameField]: username };
128
253
  const users = await authResource.query(queryFilter);
129
254
  if (!users || users.length === 0) {
255
+ const now = Date.now();
256
+ let throttleRecord = null;
257
+ let throttleKey = null;
258
+ if (loginThrottleConfig.enabled) {
259
+ const ip = getClientIp(c);
260
+ throttleKey = `${ip}:${username}`;
261
+ throttleRecord = getThrottleRecord(throttleKey, now);
262
+ const throttleResult = registerFailedAttempt(throttleRecord, now);
263
+ if (throttleResult.blocked) {
264
+ c.header('Retry-After', throttleResult.retryAfter.toString());
265
+ const response = formatter.error('Too many login attempts. Try again later.', {
266
+ status: 429,
267
+ code: 'TOO_MANY_ATTEMPTS',
268
+ details: { retryAfter: throttleResult.retryAfter }
269
+ });
270
+ return c.json(response, response._status);
271
+ }
272
+ }
273
+
130
274
  const response = formatter.unauthorized('Invalid credentials');
131
275
  return c.json(response, response._status);
132
276
  }
@@ -139,6 +283,25 @@ export function createAuthRoutes(authResource, config = {}) {
139
283
  return c.json(response, response._status);
140
284
  }
141
285
 
286
+ const now = Date.now();
287
+ let throttleRecord = null;
288
+ let throttleKey = null;
289
+ if (loginThrottleConfig.enabled) {
290
+ const ip = getClientIp(c);
291
+ throttleKey = `${ip}:${username}`;
292
+ throttleRecord = getThrottleRecord(throttleKey, now);
293
+ if (throttleRecord && throttleRecord.blockedUntil && now < throttleRecord.blockedUntil) {
294
+ const retryAfter = Math.ceil((throttleRecord.blockedUntil - now) / 1000);
295
+ c.header('Retry-After', retryAfter.toString());
296
+ const response = formatter.error('Too many login attempts. Try again later.', {
297
+ status: 429,
298
+ code: 'TOO_MANY_ATTEMPTS',
299
+ details: { retryAfter }
300
+ });
301
+ return c.json(response, response._status);
302
+ }
303
+ }
304
+
142
305
  // Verify password (compare with password field)
143
306
  // For 'password' field type (bcrypt hash), use verifyPassword
144
307
  // For 'secret' field type (AES encryption), compare directly
@@ -163,6 +326,17 @@ export function createAuthRoutes(authResource, config = {}) {
163
326
  }
164
327
 
165
328
  if (!isValid) {
329
+ const throttleResult = registerFailedAttempt(throttleRecord, now);
330
+ if (throttleResult.blocked) {
331
+ c.header('Retry-After', throttleResult.retryAfter.toString());
332
+ const response = formatter.error('Too many login attempts. Try again later.', {
333
+ status: 429,
334
+ code: 'TOO_MANY_ATTEMPTS',
335
+ details: { retryAfter: throttleResult.retryAfter }
336
+ });
337
+ return c.json(response, response._status);
338
+ }
339
+
166
340
  const response = formatter.unauthorized('Invalid credentials');
167
341
  return c.json(response, response._status);
168
342
  }
@@ -189,10 +363,12 @@ export function createAuthRoutes(authResource, config = {}) {
189
363
  }
190
364
 
191
365
  // Remove sensitive data from response
192
- const { [passwordField]: _, ...userWithoutPassword } = user;
366
+ if (loginThrottleConfig.enabled && throttleKey) {
367
+ loginAttempts.delete(throttleKey);
368
+ }
193
369
 
194
370
  const response = formatter.success({
195
- user: userWithoutPassword,
371
+ user: buildPublicUser(user),
196
372
  token,
197
373
  expiresIn: jwtExpiresIn
198
374
  });
@@ -237,15 +413,7 @@ export function createAuthRoutes(authResource, config = {}) {
237
413
  }
238
414
 
239
415
  // If user is from JWT payload (no password field), return as is
240
- if (!user.password) {
241
- const response = formatter.success(user);
242
- return c.json(response, response._status);
243
- }
244
-
245
- // Remove sensitive data
246
- const { password: _, ...userWithoutPassword } = user;
247
-
248
- const response = formatter.success(userWithoutPassword);
416
+ const response = formatter.success(buildPublicUser(user));
249
417
  return c.json(response, response._status);
250
418
  }));
251
419
 
@@ -262,7 +430,7 @@ export function createAuthRoutes(authResource, config = {}) {
262
430
  const newApiKey = generateApiKey();
263
431
 
264
432
  // Update user
265
- await usersResource.update(user.id, {
433
+ await authResource.update(user.id, {
266
434
  apiKey: newApiKey
267
435
  });
268
436
 
@@ -141,17 +141,30 @@ export function createResourceRoutes(resource, version, config = {}, Hono) {
141
141
  }
142
142
  }
143
143
 
144
+ // ✅ NEW: Check for partition filters from guards (set via ctx.setPartition())
145
+ const guardPartitionFilters = c.get('partitionFilters') || [];
146
+
144
147
  let items;
145
148
  let total;
146
149
 
147
- // Use query if filters are present
148
- if (Object.keys(filters).length > 0) {
150
+ // Priority 1: Partition filters from guards (tenant isolation)
151
+ if (guardPartitionFilters.length > 0) {
152
+ const { partitionName, partitionFields } = guardPartitionFilters[0];
153
+
154
+ // Use partition query for O(1) tenant isolation
155
+ items = await resource.listPartition(partitionName, partitionFields, { limit, offset });
156
+ total = items.length;
157
+ }
158
+ // Priority 2: Use query if filters are present
159
+ else if (Object.keys(filters).length > 0) {
149
160
  // Query with native offset support (efficient!)
150
161
  items = await resource.query(filters, { limit, offset });
151
162
  // Note: total is approximate (length of returned items)
152
163
  // For exact total count with filters, would need separate count query
153
164
  total = items.length;
154
- } else if (partition && partitionValues) {
165
+ }
166
+ // Priority 3: Partition from query params
167
+ else if (partition && partitionValues) {
155
168
  // Query specific partition
156
169
  items = await resource.listPartition({
157
170
  partition,
@@ -160,7 +173,9 @@ export function createResourceRoutes(resource, version, config = {}, Hono) {
160
173
  offset
161
174
  });
162
175
  total = items.length;
163
- } else {
176
+ }
177
+ // Priority 4: Regular list (full scan)
178
+ else {
164
179
  // Regular list
165
180
  items = await resource.list({ limit, offset });
166
181
  total = items.length;