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,98 +1,72 @@
1
1
  /**
2
2
  * API Server - Hono-based HTTP server for s3db.js API Plugin
3
3
  *
4
- * Manages HTTP server lifecycle and routing
4
+ * Manages HTTP server lifecycle and delegates routing/middleware concerns
5
+ * to dedicated components (MiddlewareChain, Router, HealthManager).
5
6
  */
6
7
 
7
- import { createResourceRoutes, createRelationalRoutes } from './routes/resource-routes.js';
8
- import { createAuthRoutes } from './routes/auth-routes.js';
9
- import { mountCustomRoutes } from './utils/custom-routes.js';
10
8
  import { errorHandler } from '../shared/error-handler.js';
11
9
  import * as formatter from '../shared/response-formatter.js';
12
- import { generateOpenAPISpec } from './utils/openapi-generator.js';
13
- import { createAuthMiddleware } from './auth/index.js';
14
10
  import { createOIDCHandler } from './auth/oidc-auth.js';
15
- import { findBestMatch, validatePathAuth } from './utils/path-matcher.js';
16
- import { createFilesystemHandler, validateFilesystemConfig } from './utils/static-filesystem.js';
17
- import { createS3Handler, validateS3Config } from './utils/static-s3.js';
18
- import { setupTemplateEngine } from './utils/template-engine.js';
19
- import { createPathBasedAuthMiddleware, findAuthRule } from './auth/path-auth-matcher.js';
20
- import { jwtAuth } from './auth/jwt-auth.js';
21
- import { apiKeyAuth } from './auth/api-key-auth.js';
22
- import { basicAuth } from './auth/basic-auth.js';
23
- import { createOAuth2Handler } from './auth/oauth2-auth.js';
24
- import { createRequestIdMiddleware } from './middlewares/request-id.js';
25
- import { createSecurityHeadersMiddleware } from './middlewares/security-headers.js';
26
- import { createSessionTrackingMiddleware } from './middlewares/session-tracking.js';
27
- import { createAuthDriverRateLimiter } from './middlewares/rate-limit.js';
28
- import { createFailbanMiddleware, setupFailbanViolationListener, createFailbanAdminRoutes } from './middlewares/failban.js';
29
11
  import { FailbanManager } from './concerns/failban-manager.js';
12
+ import { validatePathAuth } from './utils/path-matcher.js';
30
13
  import { ApiEventEmitter } from './concerns/event-emitter.js';
31
14
  import { MetricsCollector } from './concerns/metrics-collector.js';
15
+ import { MiddlewareChain } from './server/middleware-chain.class.js';
16
+ import { Router } from './server/router.class.js';
17
+ import { HealthManager } from './server/health-manager.class.js';
18
+ import { OpenAPIGeneratorCached } from './utils/openapi-generator-cached.class.js';
19
+ import { AuthStrategyFactory } from './auth/strategies/factory.class.js';
32
20
 
33
- /**
34
- * API Server class
35
- * @class
36
- */
37
21
  export class ApiServer {
38
- /**
39
- * Create API server
40
- * @param {Object} options - Server options
41
- * @param {number} options.port - Server port
42
- * @param {string} options.host - Server host
43
- * @param {Object} options.database - s3db.js database instance
44
- * @param {Object} options.resources - Resource configuration
45
- * @param {Array} options.middlewares - Global middlewares
46
- */
47
22
  constructor(options = {}) {
48
23
  this.options = {
49
24
  port: options.port || 3000,
50
25
  host: options.host || '0.0.0.0',
51
26
  database: options.database,
27
+ namespace: options.namespace || null,
28
+ versionPrefix: options.versionPrefix,
52
29
  resources: options.resources || {},
53
- routes: options.routes || {}, // Plugin-level custom routes
54
- templates: options.templates || { enabled: false, engine: 'jsx' }, // Template engine config
30
+ routes: options.routes || {},
31
+ templates: options.templates || { enabled: false, engine: 'jsx' },
55
32
  middlewares: options.middlewares || [],
56
- requestId: options.requestId || { enabled: false }, // Request ID tracking config
57
- cors: options.cors || { enabled: false }, // CORS configuration
58
- security: options.security || { enabled: false }, // Security headers config
59
- sessionTracking: options.sessionTracking || { enabled: false }, // Session tracking config
60
- events: options.events || { enabled: false }, // Event hooks config
61
- metrics: options.metrics || { enabled: false }, // Metrics collection config
62
- failban: options.failban || { enabled: false }, // Failban (fail2ban-style) config
33
+ cors: options.cors || { enabled: false },
34
+ security: options.security || { enabled: false },
35
+ sessionTracking: options.sessionTracking || { enabled: false },
36
+ requestId: options.requestId || { enabled: false },
37
+ events: options.events || { enabled: false },
38
+ metrics: options.metrics || { enabled: false },
39
+ failban: options.failban || { enabled: false },
40
+ static: Array.isArray(options.static) ? options.static : [],
41
+ health: options.health ?? { enabled: true },
63
42
  verbose: options.verbose || false,
64
43
  auth: options.auth || {},
65
- static: options.static || [], // Static file serving config
66
- docsEnabled: options.docsEnabled !== false, // Enable /docs by default
67
- docsUI: options.docsUI || 'redoc', // 'swagger' or 'redoc'
68
- maxBodySize: options.maxBodySize || 10 * 1024 * 1024, // 10MB default
69
- rootHandler: options.rootHandler, // Custom handler for root path, if not provided redirects to /docs
70
- versionPrefix: options.versionPrefix, // Global version prefix config
71
- apiInfo: {
72
- title: options.apiTitle || 's3db.js API',
73
- version: options.apiVersion || '1.0.0',
74
- description: options.apiDescription || 'Auto-generated REST API for s3db.js resources'
75
- }
44
+ docsEnabled: (options.docs?.enabled !== false) && (options.docsEnabled !== false),
45
+ docsUI: options.docs?.ui || options.docsUI || 'redoc',
46
+ apiTitle: options.docs?.title || options.apiTitle || 's3db.js API',
47
+ apiVersion: options.docs?.version || options.apiVersion || '1.0.0',
48
+ apiDescription: options.docs?.description || options.apiDescription || 'Auto-generated REST API for s3db.js resources',
49
+ maxBodySize: options.maxBodySize || 10 * 1024 * 1024
76
50
  };
77
51
 
78
- this.app = null; // Will be initialized in start() with dynamic import
52
+ this.app = null;
79
53
  this.server = null;
80
54
  this.isRunning = false;
81
- this.openAPISpec = null;
82
55
  this.initialized = false;
56
+ this.oidcMiddleware = null;
57
+ this.middlewareChain = null;
58
+ this.router = null;
59
+ this.healthManager = null;
83
60
 
84
- // Graceful shutdown tracking
85
- this.inFlightRequests = new Set(); // Track in-flight requests
86
- this.acceptingRequests = true; // Accept new requests flag
61
+ this.inFlightRequests = new Set();
62
+ this.acceptingRequests = true;
87
63
 
88
- // Event emitter
89
64
  this.events = new ApiEventEmitter({
90
65
  enabled: this.options.events?.enabled !== false,
91
66
  verbose: this.options.events?.verbose || this.options.verbose,
92
67
  maxListeners: this.options.events?.maxListeners
93
68
  });
94
69
 
95
- // Metrics collector
96
70
  this.metrics = new MetricsCollector({
97
71
  enabled: this.options.metrics?.enabled !== false,
98
72
  verbose: this.options.metrics?.verbose || this.options.verbose,
@@ -100,16 +74,15 @@ export class ApiServer {
100
74
  resetInterval: this.options.metrics?.resetInterval
101
75
  });
102
76
 
103
- // Wire up event listeners to metrics collector
104
77
  if (this.options.metrics?.enabled && this.options.events?.enabled !== false) {
105
78
  this._setupMetricsEventListeners();
106
79
  }
107
80
 
108
- // Failban manager (fail2ban-style automatic banning - internal feature)
109
81
  this.failban = null;
110
82
  if (this.options.failban?.enabled) {
111
83
  this.failban = new FailbanManager({
112
84
  database: this.options.database,
85
+ namespace: this.options.namespace,
113
86
  enabled: true,
114
87
  maxViolations: this.options.failban.maxViolations || 3,
115
88
  violationWindow: this.options.failban.violationWindow || 3600000,
@@ -118,151 +91,198 @@ export class ApiServer {
118
91
  blacklist: this.options.failban.blacklist || [],
119
92
  persistViolations: this.options.failban.persistViolations !== false,
120
93
  verbose: this.options.failban.verbose || this.options.verbose,
121
- geo: this.options.failban.geo || {}
94
+ geo: this.options.failban.geo || {},
95
+ resourceNames: this.options.failban.resourceNames || {}
122
96
  });
123
97
  }
124
98
 
125
- // Detect if RelationPlugin is installed
126
99
  this.relationsPlugin = this.options.database?.plugins?.relation ||
127
- this.options.database?.plugins?.RelationPlugin ||
128
- null;
129
-
130
- // Routes will be setup in start() after dynamic import
100
+ this.options.database?.plugins?.RelationPlugin ||
101
+ null;
102
+
103
+ const resolvedHost = (this.options.host || 'localhost') === '0.0.0.0'
104
+ ? 'localhost'
105
+ : (this.options.host || 'localhost');
106
+
107
+ this.openApiGenerator = new OpenAPIGeneratorCached({
108
+ database: this.options.database,
109
+ options: {
110
+ auth: this.options.auth,
111
+ resources: this.options.resources,
112
+ versionPrefix: this.options.versionPrefix,
113
+ title: this.options.apiTitle,
114
+ version: this.options.apiVersion,
115
+ description: this.options.apiDescription,
116
+ serverUrl: `http://${resolvedHost}:${this.options.port}`,
117
+ verbose: this.options.verbose
118
+ }
119
+ });
131
120
  }
132
121
 
133
- /**
134
- * Setup metrics event listeners
135
- * @private
136
- */
137
- _setupMetricsEventListeners() {
138
- // Request metrics
139
- this.events.on('request:end', (data) => {
140
- this.metrics.recordRequest({
141
- method: data.method,
142
- path: data.path,
143
- status: data.status,
144
- duration: data.duration
145
- });
146
- });
122
+ async start() {
123
+ if (this.isRunning) {
124
+ console.warn('[API Plugin] Server is already running');
125
+ return;
126
+ }
147
127
 
148
- this.events.on('request:error', (data) => {
149
- this.metrics.recordError({
150
- error: data.error,
151
- type: 'request'
152
- });
153
- });
128
+ if (!this.initialized) {
129
+ const { Hono } = await import('hono');
130
+ const { serve } = await import('@hono/node-server');
131
+ const { swaggerUI } = await import('@hono/swagger-ui');
132
+ const { cors } = await import('hono/cors');
154
133
 
155
- // Auth metrics
156
- this.events.on('auth:success', (data) => {
157
- this.metrics.recordAuth({
158
- success: true,
159
- method: data.method
160
- });
161
- });
134
+ this.Hono = Hono;
135
+ this.serve = serve;
136
+ this.swaggerUI = swaggerUI;
137
+ this.cors = cors;
138
+ this.app = new Hono();
162
139
 
163
- this.events.on('auth:failure', (data) => {
164
- this.metrics.recordAuth({
165
- success: false,
166
- method: data.allowedMethods?.[0] || 'unknown'
167
- });
168
- });
140
+ if (this.failban) {
141
+ await this.failban.initialize();
142
+ }
169
143
 
170
- // Resource metrics
171
- this.events.on('resource:created', (data) => {
172
- this.metrics.recordResourceOperation({
173
- action: 'created',
174
- resource: data.resource
144
+ this.middlewareChain = new MiddlewareChain({
145
+ requestId: this.options.requestId,
146
+ cors: this.options.cors,
147
+ security: this.options.security,
148
+ sessionTracking: this.options.sessionTracking,
149
+ middlewares: this.options.middlewares,
150
+ templates: this.options.templates,
151
+ maxBodySize: this.options.maxBodySize,
152
+ failban: this.failban,
153
+ events: this.events,
154
+ verbose: this.options.verbose,
155
+ database: this.options.database,
156
+ inFlightRequests: this.inFlightRequests,
157
+ acceptingRequests: () => this.acceptingRequests,
158
+ corsMiddleware: this.cors
175
159
  });
176
- });
160
+ this.middlewareChain.apply(this.app);
177
161
 
178
- this.events.on('resource:updated', (data) => {
179
- this.metrics.recordResourceOperation({
180
- action: 'updated',
181
- resource: data.resource
182
- });
183
- });
162
+ const oidcDriver = this.options.auth?.drivers?.find((d) => d.driver === 'oidc');
163
+ if (oidcDriver) {
164
+ this._setupOIDCRoutes(oidcDriver.config);
165
+ }
184
166
 
185
- this.events.on('resource:deleted', (data) => {
186
- this.metrics.recordResourceOperation({
187
- action: 'deleted',
188
- resource: data.resource
189
- });
190
- });
167
+ const authMiddleware = this._createAuthMiddleware();
191
168
 
192
- // User metrics
193
- this.events.on('user:created', (data) => {
194
- this.metrics.recordUserEvent({
195
- action: 'created'
169
+ this.router = new Router({
170
+ database: this.options.database,
171
+ resources: this.options.resources,
172
+ routes: this.options.routes,
173
+ versionPrefix: this.options.versionPrefix,
174
+ auth: this.options.auth,
175
+ static: this.options.static,
176
+ failban: this.failban,
177
+ metrics: this.metrics,
178
+ relationsPlugin: this.relationsPlugin,
179
+ authMiddleware,
180
+ verbose: this.options.verbose,
181
+ Hono: this.Hono
196
182
  });
197
- });
183
+ this.router.mount(this.app, this.events);
198
184
 
199
- this.events.on('user:login', (data) => {
200
- this.metrics.recordUserEvent({
201
- action: 'login'
185
+ if (this.options.health?.enabled !== false) {
186
+ this.healthManager = new HealthManager({
187
+ database: this.options.database,
188
+ healthConfig: this.options.health,
189
+ verbose: this.options.verbose
190
+ });
191
+ this.healthManager.register(this.app);
192
+ }
193
+
194
+ this._setupDocumentationRoutes();
195
+
196
+ this.app.onError((err, c) => errorHandler(err, c));
197
+ this.app.notFound((c) => {
198
+ const response = formatter.error('Route not found', {
199
+ status: 404,
200
+ code: 'NOT_FOUND',
201
+ details: {
202
+ path: c.req.path,
203
+ method: c.req.method
204
+ }
205
+ });
206
+ return c.json(response, 404);
202
207
  });
203
- });
204
208
 
205
- if (this.options.verbose) {
206
- console.log('[API Server] Metrics event listeners configured');
209
+ this.initialized = true;
207
210
  }
208
- }
209
211
 
210
- /**
211
- * Setup request tracking middleware for graceful shutdown
212
- * @private
213
- */
214
- _setupRequestTracking() {
215
- this.app.use('*', async (c, next) => {
216
- // Check if we're still accepting requests
217
- if (!this.acceptingRequests) {
218
- return c.json({ error: 'Server is shutting down' }, 503);
212
+ const { port, host } = this.options;
213
+
214
+ return new Promise((resolve, reject) => {
215
+ try {
216
+ this.server = this.serve(
217
+ { fetch: this.app.fetch, port, hostname: host },
218
+ (info) => {
219
+ this.isRunning = true;
220
+ console.log(`[API Plugin] Server listening on http://${info.address}:${info.port}`);
221
+
222
+ const shutdownHandler = async (signal) => {
223
+ console.log(`[API Server] Received ${signal}, initiating graceful shutdown...`);
224
+ try {
225
+ await this.shutdown({ timeout: 30000 });
226
+ process.exit(0);
227
+ } catch (err) {
228
+ console.error('[API Server] Error during shutdown:', err);
229
+ process.exit(1);
230
+ }
231
+ };
232
+
233
+ process.once('SIGTERM', () => shutdownHandler('SIGTERM'));
234
+ process.once('SIGINT', () => shutdownHandler('SIGINT'));
235
+
236
+ resolve();
237
+ }
238
+ );
239
+ } catch (err) {
240
+ reject(err);
219
241
  }
242
+ });
243
+ }
220
244
 
221
- // Track this request
222
- const requestId = Symbol('request');
223
- this.inFlightRequests.add(requestId);
245
+ async stop() {
246
+ if (!this.isRunning) {
247
+ console.warn('[API Plugin] Server is not running');
248
+ return;
249
+ }
224
250
 
225
- const startTime = Date.now();
226
- const requestInfo = {
227
- requestId: c.get('requestId') || requestId.toString(),
228
- method: c.req.method,
229
- path: c.req.path,
230
- userAgent: c.req.header('user-agent'),
231
- ip: c.req.header('x-forwarded-for') || c.req.header('x-real-ip')
232
- };
251
+ if (this.server && typeof this.server.close === 'function') {
252
+ await new Promise((resolve) => {
253
+ this.server.close(() => {
254
+ this.isRunning = false;
255
+ console.log('[API Plugin] Server stopped');
256
+ resolve();
257
+ });
258
+ });
259
+ } else {
260
+ this.isRunning = false;
261
+ console.log('[API Plugin] Server stopped');
262
+ }
233
263
 
234
- // Emit request:start
235
- this.events.emitRequestEvent('start', requestInfo);
264
+ if (this.metrics) {
265
+ this.metrics.stop();
266
+ }
236
267
 
237
- try {
238
- await next();
268
+ if (this.failban) {
269
+ await this.failban.cleanup();
270
+ }
271
+ }
239
272
 
240
- // Emit request:end
241
- this.events.emitRequestEvent('end', {
242
- ...requestInfo,
243
- duration: Date.now() - startTime,
244
- status: c.res.status
245
- });
246
- } catch (err) {
247
- // Emit request:error
248
- this.events.emitRequestEvent('error', {
249
- ...requestInfo,
250
- duration: Date.now() - startTime,
251
- error: err.message,
252
- stack: err.stack
253
- });
254
- throw err; // Re-throw for error handler
255
- } finally {
256
- // Remove from tracking when done
257
- this.inFlightRequests.delete(requestId);
258
- }
259
- });
273
+ getInfo() {
274
+ return {
275
+ isRunning: this.isRunning,
276
+ port: this.options.port,
277
+ host: this.options.host,
278
+ resources: Object.keys(this.options.database?.resources || {}).length
279
+ };
280
+ }
281
+
282
+ getApp() {
283
+ return this.app;
260
284
  }
261
285
 
262
- /**
263
- * Stop accepting new requests
264
- * @returns {void}
265
- */
266
286
  stopAcceptingRequests() {
267
287
  this.acceptingRequests = false;
268
288
  if (this.options.verbose) {
@@ -270,18 +290,11 @@ export class ApiServer {
270
290
  }
271
291
  }
272
292
 
273
- /**
274
- * Wait for all in-flight requests to finish
275
- * @param {Object} options - Options
276
- * @param {number} options.timeout - Max time to wait in ms (default: 30000)
277
- * @returns {Promise<boolean>} True if all requests finished, false if timeout
278
- */
279
293
  async waitForRequestsToFinish({ timeout = 30000 } = {}) {
280
294
  const startTime = Date.now();
281
295
 
282
296
  while (this.inFlightRequests.size > 0) {
283
297
  const elapsed = Date.now() - startTime;
284
-
285
298
  if (elapsed >= timeout) {
286
299
  if (this.options.verbose) {
287
300
  console.warn(`[API Server] Timeout waiting for ${this.inFlightRequests.size} in-flight requests`);
@@ -293,22 +306,16 @@ export class ApiServer {
293
306
  console.log(`[API Server] Waiting for ${this.inFlightRequests.size} in-flight requests...`);
294
307
  }
295
308
 
296
- // Wait 100ms before checking again
297
- await new Promise(resolve => setTimeout(resolve, 100));
309
+ await new Promise((resolve) => setTimeout(resolve, 100));
298
310
  }
299
311
 
300
312
  if (this.options.verbose) {
301
313
  console.log('[API Server] All requests finished');
302
314
  }
315
+
303
316
  return true;
304
317
  }
305
318
 
306
- /**
307
- * Graceful shutdown
308
- * @param {Object} options - Shutdown options
309
- * @param {number} options.timeout - Max time to wait for requests (default: 30000)
310
- * @returns {Promise<void>}
311
- */
312
319
  async shutdown({ timeout = 30000 } = {}) {
313
320
  if (!this.isRunning) {
314
321
  console.warn('[API Server] Server is not running');
@@ -316,18 +323,13 @@ export class ApiServer {
316
323
  }
317
324
 
318
325
  console.log('[API Server] Initiating graceful shutdown...');
319
-
320
- // Stop accepting new requests
321
326
  this.stopAcceptingRequests();
322
327
 
323
- // Wait for in-flight requests to finish
324
- const allFinished = await this.waitForRequestsToFinish({ timeout });
325
-
326
- if (!allFinished) {
328
+ const finished = await this.waitForRequestsToFinish({ timeout });
329
+ if (!finished) {
327
330
  console.warn('[API Server] Some requests did not finish in time');
328
331
  }
329
332
 
330
- // Close HTTP server
331
333
  if (this.server) {
332
334
  await new Promise((resolve, reject) => {
333
335
  this.server.close((err) => {
@@ -341,538 +343,115 @@ export class ApiServer {
341
343
  console.log('[API Server] Shutdown complete');
342
344
  }
343
345
 
344
- /**
345
- * Setup all routes
346
- * @private
347
- */
348
- _setupRoutes() {
349
- // Request tracking for graceful shutdown (must be first!)
350
- this._setupRequestTracking();
351
-
352
- // Failban middleware (check banned IPs early)
353
- if (this.failban) {
354
- const failbanMiddleware = createFailbanMiddleware({
355
- plugin: this.failban,
356
- events: this.events
346
+ _setupMetricsEventListeners() {
347
+ this.events.on('request:end', (data) => {
348
+ this.metrics.recordRequest({
349
+ method: data.method,
350
+ path: data.path,
351
+ status: data.status,
352
+ duration: data.duration
357
353
  });
358
- this.app.use('*', failbanMiddleware);
354
+ });
359
355
 
360
- // Setup violation listeners (connects events to failban)
361
- setupFailbanViolationListener({
362
- plugin: this.failban,
363
- events: this.events
356
+ this.events.on('request:error', (data) => {
357
+ this.metrics.recordError({
358
+ error: data.error,
359
+ type: 'request'
364
360
  });
365
-
366
- if (this.options.verbose) {
367
- console.log('[API Server] Failban protection enabled');
368
- }
369
- }
370
-
371
- // Request ID middleware (before all other middlewares)
372
- if (this.options.requestId?.enabled) {
373
- const requestIdMiddleware = createRequestIdMiddleware(this.options.requestId);
374
- this.app.use('*', requestIdMiddleware);
375
-
376
- if (this.options.verbose) {
377
- console.log(`[API Server] Request ID tracking enabled (header: ${this.options.requestId.headerName || 'X-Request-ID'})`);
378
- }
379
- }
380
-
381
- // CORS middleware
382
- if (this.options.cors?.enabled) {
383
- const corsConfig = this.options.cors;
384
- this.app.use('*', this.cors({
385
- origin: corsConfig.origin || '*',
386
- allowMethods: corsConfig.allowMethods || ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
387
- allowHeaders: corsConfig.allowHeaders || ['Content-Type', 'Authorization', 'X-Request-ID'],
388
- exposeHeaders: corsConfig.exposeHeaders || ['X-Request-ID'],
389
- credentials: corsConfig.credentials || false,
390
- maxAge: corsConfig.maxAge || 86400 // 24 hours cache by default
391
- }));
392
-
393
- if (this.options.verbose) {
394
- console.log(`[API Server] CORS enabled (maxAge: ${corsConfig.maxAge || 86400}s, origin: ${corsConfig.origin || '*'})`);
395
- }
396
- }
397
-
398
- // Security headers middleware
399
- if (this.options.security?.enabled) {
400
- const securityMiddleware = createSecurityHeadersMiddleware(this.options.security);
401
- this.app.use('*', securityMiddleware);
402
-
403
- if (this.options.verbose) {
404
- console.log('[API Server] Security headers enabled');
405
- }
406
- }
407
-
408
- // Session tracking middleware
409
- if (this.options.sessionTracking?.enabled) {
410
- const sessionMiddleware = createSessionTrackingMiddleware(
411
- this.options.sessionTracking,
412
- this.options.database
413
- );
414
- this.app.use('*', sessionMiddleware);
415
-
416
- if (this.options.verbose) {
417
- const resource = this.options.sessionTracking.resource ? ` (resource: ${this.options.sessionTracking.resource})` : ' (in-memory)';
418
- console.log(`[API Server] Session tracking enabled${resource}`);
419
- }
420
- }
421
-
422
- // Apply global middlewares
423
- this.options.middlewares.forEach(middleware => {
424
- this.app.use('*', middleware);
425
361
  });
426
362
 
427
- // Template engine middleware (if enabled)
428
- if (this.options.templates?.enabled) {
429
- const templateMiddleware = setupTemplateEngine(this.options.templates);
430
- this.app.use('*', templateMiddleware);
431
-
432
- if (this.options.verbose) {
433
- console.log(`[API Server] Template engine enabled: ${this.options.templates.engine}`);
434
- }
435
- }
436
-
437
- // Body size limit middleware (only for POST, PUT, PATCH)
438
- this.app.use('*', async (c, next) => {
439
- const method = c.req.method;
440
-
441
- if (['POST', 'PUT', 'PATCH'].includes(method)) {
442
- const contentLength = c.req.header('content-length');
443
-
444
- if (contentLength) {
445
- const size = parseInt(contentLength);
446
-
447
- if (size > this.options.maxBodySize) {
448
- const response = formatter.payloadTooLarge(size, this.options.maxBodySize);
449
- c.header('Connection', 'close'); // Close connection for large payloads
450
- return c.json(response, response._status);
451
- }
452
- }
453
- }
454
-
455
- await next();
363
+ this.events.on('auth:success', (data) => {
364
+ this.metrics.recordAuth({
365
+ success: true,
366
+ method: data.method
367
+ });
456
368
  });
457
369
 
458
- // Kubernetes Liveness Probe - checks if app is alive
459
- // If this fails, k8s will restart the pod
460
- this.app.get('/health/live', (c) => {
461
- // Simple check: if we can respond, we're alive
462
- const response = formatter.success({
463
- status: 'alive',
464
- timestamp: new Date().toISOString()
370
+ this.events.on('auth:failure', (data) => {
371
+ this.metrics.recordAuth({
372
+ success: false,
373
+ method: data.allowedMethods?.[0] || 'unknown'
465
374
  });
466
- return c.json(response);
467
375
  });
468
376
 
469
- // Kubernetes Readiness Probe - checks if app is ready to receive traffic
470
- // If this fails, k8s will remove pod from service endpoints
471
- this.app.get('/health/ready', async (c) => {
472
- const checks = {};
473
- let isHealthy = true;
474
-
475
- // Get custom checks configuration
476
- const healthConfig = this.options.health || {};
477
- const customChecks = healthConfig.readiness?.checks || [];
478
-
479
- // Built-in: Database check
480
- try {
481
- const startTime = Date.now();
482
- const isDbReady = this.options.database &&
483
- this.options.database.connected &&
484
- Object.keys(this.options.database.resources).length > 0;
485
- const latency = Date.now() - startTime;
486
-
487
- if (isDbReady) {
488
- checks.s3db = {
489
- status: 'healthy',
490
- latency_ms: latency,
491
- resources: Object.keys(this.options.database.resources).length
492
- };
493
- } else {
494
- checks.s3db = {
495
- status: 'unhealthy',
496
- connected: this.options.database?.connected || false,
497
- resources: Object.keys(this.options.database?.resources || {}).length
498
- };
499
- isHealthy = false;
500
- }
501
- } catch (err) {
502
- checks.s3db = {
503
- status: 'unhealthy',
504
- error: err.message
505
- };
506
- isHealthy = false;
507
- }
508
-
509
- // Execute custom checks
510
- for (const check of customChecks) {
511
- try {
512
- const startTime = Date.now();
513
- const timeout = check.timeout || 5000;
514
-
515
- // Run check with timeout
516
- const result = await Promise.race([
517
- check.check(),
518
- new Promise((_, reject) =>
519
- setTimeout(() => reject(new Error('Timeout')), timeout)
520
- )
521
- ]);
522
-
523
- const latency = Date.now() - startTime;
524
-
525
- checks[check.name] = {
526
- status: result.healthy ? 'healthy' : 'unhealthy',
527
- latency_ms: latency,
528
- ...result
529
- };
530
-
531
- // Only mark as unhealthy if check is not optional
532
- if (!result.healthy && !check.optional) {
533
- isHealthy = false;
534
- }
535
- } catch (err) {
536
- checks[check.name] = {
537
- status: 'unhealthy',
538
- error: err.message
539
- };
540
-
541
- // Only mark as unhealthy if check is not optional
542
- if (!check.optional) {
543
- isHealthy = false;
544
- }
545
- }
546
- }
547
-
548
- const status = isHealthy ? 200 : 503;
549
-
550
- return c.json({
551
- status: isHealthy ? 'healthy' : 'unhealthy',
552
- timestamp: new Date().toISOString(),
553
- uptime: process.uptime(),
554
- checks
555
- }, status);
377
+ this.events.on('resource:created', (data) => {
378
+ this.metrics.recordResourceOperation({
379
+ action: 'created',
380
+ resource: data.resource
381
+ });
556
382
  });
557
383
 
558
- // Generic Health Check endpoint
559
- this.app.get('/health', (c) => {
560
- const response = formatter.success({
561
- status: 'ok',
562
- uptime: process.uptime(),
563
- timestamp: new Date().toISOString(),
564
- checks: {
565
- liveness: '/health/live',
566
- readiness: '/health/ready'
567
- }
384
+ this.events.on('resource:updated', (data) => {
385
+ this.metrics.recordResourceOperation({
386
+ action: 'updated',
387
+ resource: data.resource
568
388
  });
569
- return c.json(response);
570
389
  });
571
390
 
572
- // Metrics endpoint
573
- if (this.options.metrics?.enabled) {
574
- this.app.get('/metrics', (c) => {
575
- const summary = this.metrics.getSummary();
576
- const response = formatter.success(summary);
577
- return c.json(response);
391
+ this.events.on('resource:deleted', (data) => {
392
+ this.metrics.recordResourceOperation({
393
+ action: 'deleted',
394
+ resource: data.resource
578
395
  });
396
+ });
579
397
 
580
- if (this.options.verbose) {
581
- console.log('[API Server] Metrics endpoint enabled at /metrics');
582
- }
583
- }
584
-
585
- // Failban admin endpoints
586
- if (this.failban) {
587
- const failbanAdminRoutes = createFailbanAdminRoutes(this.Hono, this.failban);
588
- this.app.route('/admin/security', failbanAdminRoutes);
589
-
590
- if (this.options.verbose) {
591
- console.log('[API Server] Failban admin endpoints enabled at /admin/security');
592
- }
593
- }
594
-
595
- // Root endpoint - custom handler or redirect to docs
596
- this.app.get('/', (c) => {
597
- // If user provided a custom root handler, use it
598
- if (this.options.rootHandler) {
599
- return this.options.rootHandler(c);
600
- }
398
+ this.events.on('user:created', () => {
399
+ this.metrics.recordUserEvent({ action: 'created' });
400
+ });
601
401
 
602
- // Otherwise, redirect to docs
603
- return c.redirect('/docs', 302);
402
+ this.events.on('user:login', () => {
403
+ this.metrics.recordUserEvent({ action: 'login' });
604
404
  });
605
405
 
606
- // Setup static file serving (before resource routes to give static files priority)
607
- this._setupStaticRoutes();
406
+ if (this.options.verbose) {
407
+ console.log('[API Server] Metrics event listeners configured');
408
+ }
409
+ }
608
410
 
609
- // OpenAPI spec endpoint
411
+ _setupDocumentationRoutes() {
610
412
  if (this.options.docsEnabled) {
611
413
  this.app.get('/openapi.json', (c) => {
612
- if (!this.openAPISpec) {
613
- this.openAPISpec = this._generateOpenAPISpec();
614
- }
615
- return c.json(this.openAPISpec);
414
+ const spec = this.openApiGenerator.generate();
415
+ return c.json(spec);
616
416
  });
617
417
 
618
- // API Documentation UI endpoint
619
418
  if (this.options.docsUI === 'swagger') {
620
- this.app.get('/docs', this.swaggerUI({
621
- url: '/openapi.json'
622
- }));
419
+ this.app.get('/docs', this.swaggerUI({ url: '/openapi.json' }));
623
420
  } else {
624
- this.app.get('/docs', (c) => {
625
- return c.html(`<!DOCTYPE html>
421
+ this.app.get('/docs', (c) => c.html(`<!DOCTYPE html>
626
422
  <html lang="en">
627
423
  <head>
628
424
  <meta charset="UTF-8">
629
425
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
630
- <title>${this.options.apiInfo.title} - API Documentation</title>
426
+ <title>${this.options.apiTitle} - API Documentation</title>
631
427
  <style>
632
- body {
633
- margin: 0;
634
- padding: 0;
635
- }
428
+ body { margin: 0; padding: 0; }
636
429
  </style>
637
430
  </head>
638
431
  <body>
639
432
  <redoc spec-url="/openapi.json"></redoc>
640
433
  <script src="https://cdn.redoc.ly/redoc/v2.5.1/bundles/redoc.standalone.js"></script>
641
434
  </body>
642
- </html>`);
643
- });
435
+ </html>`));
644
436
  }
645
437
  }
646
438
 
647
- // Setup resource routes
648
- this._setupResourceRoutes();
649
-
650
- // Setup authentication routes if JWT driver is configured
651
- const hasJwtDriver = Array.isArray(this.options.auth?.drivers)
652
- ? this.options.auth.drivers.some(d => d.driver === 'jwt')
653
- : false;
439
+ this.app.get('/', (c) => {
440
+ if (this.options.rootHandler) {
441
+ return this.options.rootHandler(c);
442
+ }
654
443
 
655
- if (this.options.auth?.driver || hasJwtDriver) {
656
- this._setupAuthRoutes();
657
- }
444
+ if (this.options.docsEnabled) {
445
+ return c.redirect('/docs', 302);
446
+ }
658
447
 
659
- // Setup OIDC routes if configured
660
- const oidcDriver = this.options.auth?.drivers?.find(d => d.driver === 'oidc');
661
- if (oidcDriver) {
662
- this._setupOIDCRoutes(oidcDriver.config);
663
- }
664
-
665
- // Setup relational routes if RelationPlugin is active
666
- if (this.relationsPlugin) {
667
- this._setupRelationalRoutes();
668
- }
669
-
670
- // Setup plugin-level custom routes
671
- this._setupPluginRoutes();
672
-
673
- // Global error handler
674
- this.app.onError((err, c) => {
675
- return errorHandler(err, c);
676
- });
677
-
678
- // 404 handler
679
- this.app.notFound((c) => {
680
- const response = formatter.error('Route not found', {
681
- status: 404,
682
- code: 'NOT_FOUND',
683
- details: {
684
- path: c.req.path,
685
- method: c.req.method
686
- }
687
- });
688
- return c.json(response, 404);
448
+ return c.json(formatter.success({
449
+ status: 'ok',
450
+ message: 's3db.js API is running'
451
+ }));
689
452
  });
690
453
  }
691
454
 
692
- /**
693
- * Setup routes for all resources
694
- * @private
695
- */
696
- _setupResourceRoutes() {
697
- const { database, resources: resourceConfigs = {} } = this.options;
698
-
699
- // Get all resources from database
700
- const resources = database.resources;
701
-
702
- // Create global auth middleware (applies to all resources, guards control access)
703
- const authMiddleware = this._createAuthMiddleware();
704
-
705
- for (const [name, resource] of Object.entries(resources)) {
706
- const resourceConfig = resourceConfigs[name];
707
- const isPluginResource = name.startsWith('plg_');
708
-
709
- // Internal plugin resources require explicit opt-in
710
- if (isPluginResource && !resourceConfig) {
711
- if (this.options.verbose) {
712
- console.log(`[API Plugin] Skipping internal resource '${name}' (not included in config.resources)`);
713
- }
714
- continue;
715
- }
716
-
717
- // Allow explicit disabling via config
718
- if (resourceConfig?.enabled === false) {
719
- if (this.options.verbose) {
720
- console.log(`[API Plugin] Resource '${name}' disabled via config.resources`);
721
- }
722
- continue;
723
- }
724
-
725
- // Determine version
726
- const version = resource.config?.currentVersion || resource.version || 'v1';
727
-
728
- // Determine version prefix (resource-level overrides global)
729
- let versionPrefixConfig;
730
- if (resourceConfig && resourceConfig.versionPrefix !== undefined) {
731
- versionPrefixConfig = resourceConfig.versionPrefix;
732
- } else if (resource.config && resource.config.versionPrefix !== undefined) {
733
- versionPrefixConfig = resource.config.versionPrefix;
734
- } else if (this.options.versionPrefix !== undefined) {
735
- versionPrefixConfig = this.options.versionPrefix;
736
- } else {
737
- versionPrefixConfig = false;
738
- }
739
-
740
- // Calculate the actual prefix to use
741
- let prefix = '';
742
- if (versionPrefixConfig === true) {
743
- // true: use resource version
744
- prefix = version;
745
- } else if (versionPrefixConfig === false) {
746
- // false: no prefix
747
- prefix = '';
748
- } else if (typeof versionPrefixConfig === 'string') {
749
- // string: custom prefix
750
- prefix = versionPrefixConfig;
751
- }
752
-
753
- // Prepare custom middleware
754
- const middlewares = [];
755
-
756
- // Add global authentication middleware unless explicitly disabled
757
- const authDisabled = resourceConfig?.auth === false;
758
-
759
- if (authMiddleware && !authDisabled) {
760
- middlewares.push(authMiddleware);
761
- }
762
-
763
- // Add resource-specific middleware from config (support single fn or array)
764
- const extraMiddleware = resourceConfig?.customMiddleware;
765
- if (extraMiddleware) {
766
- const toRegister = Array.isArray(extraMiddleware) ? extraMiddleware : [extraMiddleware];
767
-
768
- for (const middleware of toRegister) {
769
- if (typeof middleware === 'function') {
770
- middlewares.push(middleware);
771
- } else if (this.options.verbose) {
772
- console.warn(`[API Plugin] Ignoring non-function middleware for resource '${name}'`);
773
- }
774
- }
775
- }
776
-
777
- // Normalize HTTP methods (resource config > resource definition > defaults)
778
- let methods = resourceConfig?.methods || resource.config?.methods;
779
- if (!Array.isArray(methods) || methods.length === 0) {
780
- methods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'];
781
- } else {
782
- methods = methods
783
- .filter(Boolean)
784
- .map(method => typeof method === 'string' ? method.toUpperCase() : method);
785
- }
786
-
787
- // Determine validation toggle
788
- const enableValidation = resourceConfig?.validation !== undefined
789
- ? resourceConfig.validation !== false
790
- : resource.config?.validation !== false;
791
-
792
- // Create resource routes
793
- const resourceApp = createResourceRoutes(resource, version, {
794
- methods,
795
- customMiddleware: middlewares,
796
- enableValidation,
797
- versionPrefix: prefix,
798
- events: this.events
799
- }, this.Hono);
800
-
801
- // Mount resource routes (with or without prefix)
802
- const mountPath = prefix ? `/${prefix}/${name}` : `/${name}`;
803
- this.app.route(mountPath, resourceApp);
804
-
805
- if (this.options.verbose) {
806
- console.log(`[API Plugin] Mounted routes for resource '${name}' at ${mountPath}`);
807
- }
808
-
809
- // Mount custom routes for this resource
810
- if (resource.config?.routes) {
811
- const routeContext = {
812
- resource,
813
- database,
814
- resourceName: name,
815
- version
816
- };
817
-
818
- // Mount on the resourceApp (nested under resource path)
819
- mountCustomRoutes(resourceApp, resource.config.routes, routeContext, this.options.verbose);
820
- }
821
- }
822
- }
823
-
824
- /**
825
- * Setup authentication routes (when auth drivers are configured)
826
- * @private
827
- */
828
- _setupAuthRoutes() {
829
- const { database, auth } = this.options;
830
- const { drivers, resource: resourceName, usernameField, passwordField } = auth;
831
-
832
- // Find first JWT driver (for /auth/login endpoint)
833
- const jwtDriver = drivers.find(d => d.driver === 'jwt');
834
-
835
- if (!jwtDriver) {
836
- // No JWT driver = no /auth routes
837
- return;
838
- }
839
-
840
- // Get auth resource from database
841
- const authResource = database.resources[resourceName];
842
- if (!authResource) {
843
- console.error(`[API Plugin] Auth resource '${resourceName}' not found. Skipping auth routes.`);
844
- return;
845
- }
846
-
847
- const driverConfig = jwtDriver.config || {};
848
-
849
- // Prepare auth config for routes
850
- const authConfig = {
851
- driver: 'jwt',
852
- usernameField,
853
- passwordField,
854
- jwtSecret: driverConfig.jwtSecret || driverConfig.secret,
855
- jwtExpiresIn: driverConfig.jwtExpiresIn || driverConfig.expiresIn || '7d',
856
- passphrase: driverConfig.passphrase || 'secret',
857
- allowRegistration: driverConfig.allowRegistration !== false
858
- };
859
-
860
- // Create auth routes
861
- const authApp = createAuthRoutes(authResource, authConfig);
862
-
863
- // Mount auth routes at /auth
864
- this.app.route('/auth', authApp);
865
-
866
- if (this.options.verbose) {
867
- console.log('[API Plugin] Mounted auth routes (driver: jwt) at /auth');
868
- }
869
- }
870
-
871
- /**
872
- * Setup OIDC routes (when oidc driver is configured)
873
- * @private
874
- * @param {Object} config - OIDC driver configuration
875
- */
876
455
  _setupOIDCRoutes(config) {
877
456
  const { database, auth } = this.options;
878
457
  const authResource = database.resources[auth.resource];
@@ -882,10 +461,7 @@ export class ApiServer {
882
461
  return;
883
462
  }
884
463
 
885
- // Create OIDC handler (which creates routes + middleware)
886
464
  const oidcHandler = createOIDCHandler(config, this.app, authResource, this.events);
887
-
888
- // Store middleware for later use in _createAuthMiddleware
889
465
  this.oidcMiddleware = oidcHandler.middleware;
890
466
 
891
467
  if (this.options.verbose) {
@@ -896,33 +472,20 @@ export class ApiServer {
896
472
  }
897
473
  }
898
474
 
899
- /**
900
- * Create authentication middleware based on configured drivers
901
- * @private
902
- * @returns {Function|null} Hono middleware or null
903
- */
904
475
  _createAuthMiddleware() {
905
476
  const { database, auth } = this.options;
906
477
  const { drivers, resource: defaultResourceName, pathAuth, pathRules } = auth;
907
478
 
908
- // If no drivers configured, no auth
909
479
  if (!drivers || drivers.length === 0) {
910
480
  return null;
911
481
  }
912
482
 
913
- // Get auth resource
914
483
  const authResource = database.resources[defaultResourceName];
915
484
  if (!authResource) {
916
485
  console.error(`[API Plugin] Auth resource '${defaultResourceName}' not found for middleware`);
917
486
  return null;
918
487
  }
919
488
 
920
- // NEW: If pathRules configured, use new path-based auth system
921
- if (pathRules && pathRules.length > 0) {
922
- return this._createPathRulesAuthMiddleware(authResource, drivers, pathRules);
923
- }
924
-
925
- // Validate pathAuth config if provided
926
489
  if (pathAuth) {
927
490
  try {
928
491
  validatePathAuth(pathAuth);
@@ -932,612 +495,26 @@ export class ApiServer {
932
495
  }
933
496
  }
934
497
 
935
- // Helper: Extract driver configs from drivers array
936
- const extractDriverConfigs = (driverNames) => {
937
- const configs = {
938
- jwt: {},
939
- apiKey: {},
940
- basic: {},
941
- oauth2: {}
942
- };
943
-
944
- for (const driverDef of drivers) {
945
- const driverName = driverDef.driver;
946
- const driverConfig = driverDef.config || {};
947
-
948
- // Skip if not in requested drivers
949
- if (driverNames && !driverNames.includes(driverName)) {
950
- continue;
951
- }
952
-
953
- // Skip oauth2-server and oidc drivers (they're handled separately)
954
- if (driverName === 'oauth2-server' || driverName === 'oidc') {
955
- continue;
956
- }
957
-
958
- // Map driver configs
959
- if (driverName === 'jwt') {
960
- configs.jwt = {
961
- secret: driverConfig.jwtSecret || driverConfig.secret,
962
- expiresIn: driverConfig.jwtExpiresIn || driverConfig.expiresIn || '7d'
963
- };
964
- } else if (driverName === 'apiKey') {
965
- configs.apiKey = {
966
- headerName: driverConfig.headerName || 'X-API-Key'
967
- };
968
- } else if (driverName === 'basic') {
969
- configs.basic = {
970
- realm: driverConfig.realm || 'API Access',
971
- passphrase: driverConfig.passphrase || 'secret'
972
- };
973
- } else if (driverName === 'oauth2') {
974
- configs.oauth2 = driverConfig;
975
- }
976
- }
977
-
978
- return configs;
979
- };
980
-
981
- // If pathAuth is defined, create path-based conditional middleware
982
- if (pathAuth) {
983
- return async (c, next) => {
984
- const requestPath = c.req.path;
985
-
986
- // Find best matching rule for this path
987
- const matchedRule = findBestMatch(pathAuth, requestPath);
988
-
989
- if (this.options.verbose) {
990
- if (matchedRule) {
991
- console.log(`[API Plugin] Path ${requestPath} matched rule: ${matchedRule.pattern}`);
992
- } else {
993
- console.log(`[API Plugin] Path ${requestPath} no pathAuth rule matched (using global auth)`);
994
- }
995
- }
996
-
997
- // If no rule matched, use global auth (all drivers, optional)
998
- if (!matchedRule) {
999
- const methods = drivers
1000
- .map(d => d.driver)
1001
- .filter(d => d !== 'oauth2-server' && d !== 'oidc');
1002
-
1003
- const driverConfigs = extractDriverConfigs(null); // all drivers
1004
-
1005
- const globalAuth = createAuthMiddleware({
1006
- methods,
1007
- jwt: driverConfigs.jwt,
1008
- apiKey: driverConfigs.apiKey,
1009
- basic: driverConfigs.basic,
1010
- oauth2: driverConfigs.oauth2,
1011
- oidc: this.oidcMiddleware || null,
1012
- usersResource: authResource,
1013
- optional: true
1014
- });
1015
-
1016
- return await globalAuth(c, next);
1017
- }
1018
-
1019
- // Rule matched - check if auth is required
1020
- if (!matchedRule.required) {
1021
- // Public path - no auth required
1022
- return await next();
1023
- }
1024
-
1025
- // Auth required - apply with specific drivers from rule
1026
- const ruleMethods = matchedRule.drivers || [];
1027
- const driverConfigs = extractDriverConfigs(ruleMethods);
1028
-
1029
- const ruleAuth = createAuthMiddleware({
1030
- methods: ruleMethods,
1031
- jwt: driverConfigs.jwt,
1032
- apiKey: driverConfigs.apiKey,
1033
- basic: driverConfigs.basic,
1034
- oauth2: driverConfigs.oauth2,
1035
- oidc: this.oidcMiddleware || null,
1036
- usersResource: authResource,
1037
- optional: false // Auth is required for this path
1038
- });
1039
-
1040
- return await ruleAuth(c, next);
1041
- };
1042
- }
1043
-
1044
- // No pathAuth - use original behavior (global auth, all drivers)
1045
- const methods = [];
1046
- const driverConfigs = {
1047
- jwt: {},
1048
- apiKey: {},
1049
- basic: {},
1050
- oauth2: {}
1051
- };
1052
-
1053
- for (const driverDef of drivers) {
1054
- const driverName = driverDef.driver;
1055
- const driverConfig = driverDef.config || {};
1056
-
1057
- // Skip oauth2-server and oidc drivers (they're handled separately)
1058
- if (driverName === 'oauth2-server' || driverName === 'oidc') {
1059
- continue;
1060
- }
1061
-
1062
- if (!methods.includes(driverName)) {
1063
- methods.push(driverName);
1064
- }
1065
-
1066
- // Map driver configs
1067
- if (driverName === 'jwt') {
1068
- driverConfigs.jwt = {
1069
- secret: driverConfig.jwtSecret || driverConfig.secret,
1070
- expiresIn: driverConfig.jwtExpiresIn || driverConfig.expiresIn || '7d'
1071
- };
1072
- } else if (driverName === 'apiKey') {
1073
- driverConfigs.apiKey = {
1074
- headerName: driverConfig.headerName || 'X-API-Key'
1075
- };
1076
- } else if (driverName === 'basic') {
1077
- driverConfigs.basic = {
1078
- realm: driverConfig.realm || 'API Access',
1079
- passphrase: driverConfig.passphrase || 'secret'
1080
- };
1081
- } else if (driverName === 'oauth2') {
1082
- driverConfigs.oauth2 = driverConfig;
1083
- }
1084
- }
1085
-
1086
- // Create unified auth middleware
1087
- return createAuthMiddleware({
1088
- methods,
1089
- jwt: driverConfigs.jwt,
1090
- apiKey: driverConfigs.apiKey,
1091
- basic: driverConfigs.basic,
1092
- oauth2: driverConfigs.oauth2,
1093
- oidc: this.oidcMiddleware || null, // OIDC middleware (if configured)
1094
- usersResource: authResource,
1095
- optional: true // Let guards handle authorization
498
+ const strategy = AuthStrategyFactory.create({
499
+ drivers,
500
+ authResource,
501
+ oidcMiddleware: this.oidcMiddleware || null,
502
+ pathRules,
503
+ pathAuth,
504
+ events: this.events,
505
+ verbose: this.options.verbose
1096
506
  });
1097
- }
1098
507
 
1099
- /**
1100
- * Create path-based auth middleware using pathRules
1101
- * @private
1102
- * @param {Object} authResource - Users resource for authentication
1103
- * @param {Array} drivers - Auth driver configurations
1104
- * @param {Array} pathRules - Path-based auth rules
1105
- * @returns {Function} Hono middleware
1106
- */
1107
- _createPathRulesAuthMiddleware(authResource, drivers, pathRules) {
1108
- // Build auth middlewares map by driver type
1109
- const authMiddlewares = {};
1110
-
1111
- for (const driverDef of drivers) {
1112
- const driverType = driverDef.type || driverDef.driver;
1113
- const driverConfig = driverDef.config || driverDef;
1114
-
1115
- // Skip oauth2-server (not a request auth method)
1116
- if (driverType === 'oauth2-server') {
1117
- continue;
1118
- }
1119
-
1120
- // OIDC middleware (already configured)
1121
- if (driverType === 'oidc') {
1122
- if (this.oidcMiddleware) {
1123
- authMiddlewares.oidc = this.oidcMiddleware;
1124
- }
1125
- continue;
1126
- }
1127
-
1128
- // JWT
1129
- if (driverType === 'jwt') {
1130
- authMiddlewares.jwt = jwtAuth({
1131
- secret: driverConfig.jwtSecret || driverConfig.secret,
1132
- expiresIn: driverConfig.jwtExpiresIn || driverConfig.expiresIn || '7d',
1133
- usersResource: authResource,
1134
- optional: true
1135
- });
1136
- }
1137
-
1138
- // API Key
1139
- if (driverType === 'apiKey') {
1140
- authMiddlewares.apiKey = apiKeyAuth({
1141
- headerName: driverConfig.headerName || 'X-API-Key',
1142
- usersResource: authResource,
1143
- optional: true
1144
- });
1145
- }
1146
-
1147
- // Basic Auth
1148
- if (driverType === 'basic') {
1149
- authMiddlewares.basic = basicAuth({
1150
- authResource,
1151
- usernameField: driverConfig.usernameField || 'email',
1152
- passwordField: driverConfig.passwordField || 'password',
1153
- passphrase: driverConfig.passphrase || 'secret',
1154
- adminUser: driverConfig.adminUser || null,
1155
- optional: true
1156
- });
1157
- }
1158
-
1159
- // OAuth2
1160
- if (driverType === 'oauth2') {
1161
- const oauth2Handler = createOAuth2Handler(driverConfig, authResource);
1162
- authMiddlewares.oauth2 = async (c, next) => {
1163
- const user = await oauth2Handler(c);
1164
- if (user) {
1165
- c.set('user', user);
1166
- return await next();
1167
- }
1168
- };
1169
- }
1170
- }
1171
-
1172
- if (this.options.verbose) {
1173
- console.log(`[API Server] Path-based auth with ${pathRules.length} rules`);
1174
- console.log(`[API Server] Available auth methods: ${Object.keys(authMiddlewares).join(', ')}`);
508
+ try {
509
+ return strategy.createMiddleware();
510
+ } catch (err) {
511
+ console.error('[API Plugin] Failed to create auth middleware:', err.message);
512
+ throw err;
1175
513
  }
1176
-
1177
- // Create and return path-based auth middleware
1178
- return createPathBasedAuthMiddleware({
1179
- rules: pathRules,
1180
- authMiddlewares,
1181
- unauthorizedHandler: (c, message) => {
1182
- // Content negotiation
1183
- const acceptHeader = c.req.header('accept') || '';
1184
- const acceptsHtml = acceptHeader.includes('text/html');
1185
-
1186
- if (acceptsHtml) {
1187
- // Redirect to login if OIDC is available
1188
- if (authMiddlewares.oidc) {
1189
- return c.redirect('/auth/login', 302);
1190
- }
1191
- }
1192
-
1193
- return c.json({
1194
- error: 'Unauthorized',
1195
- message
1196
- }, 401);
1197
- },
1198
- events: this.events
1199
- });
1200
514
  }
1201
515
 
1202
- /**
1203
- * Setup relational routes (when RelationPlugin is active)
1204
- * @private
1205
- */
1206
- _setupRelationalRoutes() {
1207
- if (!this.relationsPlugin || !this.relationsPlugin.relations) {
1208
- return;
1209
- }
1210
-
1211
- const { database } = this.options;
1212
- const relations = this.relationsPlugin.relations;
1213
-
1214
- if (this.options.verbose) {
1215
- console.log('[API Plugin] Setting up relational routes...');
1216
- }
1217
-
1218
- for (const [resourceName, relationsDef] of Object.entries(relations)) {
1219
- const resource = database.resources[resourceName];
1220
- if (!resource) {
1221
- if (this.options.verbose) {
1222
- console.warn(`[API Plugin] Resource '${resourceName}' not found for relational routes`);
1223
- }
1224
- continue;
1225
- }
1226
-
1227
- // Skip plugin resources unless explicitly included
1228
- if (resourceName.startsWith('plg_') && !this.options.resources[resourceName]) {
1229
- continue;
1230
- }
1231
-
1232
- const version = resource.config?.currentVersion || resource.version || 'v1';
1233
-
1234
- for (const [relationName, relationConfig] of Object.entries(relationsDef)) {
1235
- // Only create routes for relations that should be exposed via API
1236
- // Skip belongsTo relations (they're just reverse lookups, not useful as endpoints)
1237
- if (relationConfig.type === 'belongsTo') {
1238
- continue;
1239
- }
1240
-
1241
- // Check if relation should be exposed (default: yes, unless explicitly disabled)
1242
- const resourceConfig = this.options.resources[resourceName];
1243
- const exposeRelation = resourceConfig?.relations?.[relationName]?.expose !== false;
1244
-
1245
- if (!exposeRelation) {
1246
- continue;
1247
- }
1248
-
1249
- // Create relational routes
1250
- const relationalApp = createRelationalRoutes(
1251
- resource,
1252
- relationName,
1253
- relationConfig,
1254
- version,
1255
- this.Hono
1256
- );
1257
-
1258
- // Mount relational routes at /{version}/{resource}/:id/{relation}
1259
- this.app.route(`/${version}/${resourceName}/:id/${relationName}`, relationalApp);
1260
-
1261
- if (this.options.verbose) {
1262
- console.log(
1263
- `[API Plugin] Mounted relational route: /${version}/${resourceName}/:id/${relationName} ` +
1264
- `(${relationConfig.type} -> ${relationConfig.resource})`
1265
- );
1266
- }
1267
- }
1268
- }
1269
- }
1270
-
1271
- /**
1272
- * Setup plugin-level custom routes
1273
- * @private
1274
- */
1275
- _setupPluginRoutes() {
1276
- const { routes, database } = this.options;
1277
-
1278
- if (!routes || Object.keys(routes).length === 0) {
1279
- return;
1280
- }
1281
-
1282
- // Plugin-level routes context
1283
- const context = {
1284
- database,
1285
- plugins: database?.plugins || {}
1286
- };
1287
-
1288
- // Mount plugin routes directly on main app (not nested)
1289
- mountCustomRoutes(this.app, routes, context, this.options.verbose);
1290
-
1291
- if (this.options.verbose) {
1292
- console.log(`[API Plugin] Mounted ${Object.keys(routes).length} plugin-level custom routes`);
1293
- }
1294
- }
1295
-
1296
- /**
1297
- * Setup static file serving routes
1298
- * @private
1299
- */
1300
- _setupStaticRoutes() {
1301
- const { static: staticConfigs, database } = this.options;
1302
-
1303
- if (!staticConfigs || staticConfigs.length === 0) {
1304
- return;
1305
- }
1306
-
1307
- if (!Array.isArray(staticConfigs)) {
1308
- throw new Error('Static config must be an array of mount points');
1309
- }
1310
-
1311
- for (const [index, config] of staticConfigs.entries()) {
1312
- try {
1313
- // Validate required fields
1314
- if (!config.driver) {
1315
- throw new Error(`static[${index}]: "driver" is required (filesystem or s3)`);
1316
- }
1317
-
1318
- if (!config.path) {
1319
- throw new Error(`static[${index}]: "path" is required (mount path)`);
1320
- }
1321
-
1322
- if (!config.path.startsWith('/')) {
1323
- throw new Error(`static[${index}]: "path" must start with / (got: ${config.path})`);
1324
- }
1325
-
1326
- const driverConfig = config.config || {};
1327
-
1328
- // Create handler based on driver
1329
- let handler;
1330
-
1331
- if (config.driver === 'filesystem') {
1332
- // Validate filesystem-specific config
1333
- validateFilesystemConfig({ ...config, ...driverConfig });
1334
-
1335
- handler = createFilesystemHandler({
1336
- root: config.root,
1337
- index: driverConfig.index,
1338
- fallback: driverConfig.fallback,
1339
- maxAge: driverConfig.maxAge,
1340
- dotfiles: driverConfig.dotfiles,
1341
- etag: driverConfig.etag,
1342
- cors: driverConfig.cors
1343
- });
1344
-
1345
- } else if (config.driver === 's3') {
1346
- // Validate S3-specific config
1347
- validateS3Config({ ...config, ...driverConfig });
1348
-
1349
- // Get S3 client from database
1350
- const s3Client = database?.client?.client; // S3Client instance
1351
-
1352
- if (!s3Client) {
1353
- throw new Error(`static[${index}]: S3 driver requires database with S3 client`);
1354
- }
1355
-
1356
- handler = createS3Handler({
1357
- s3Client,
1358
- bucket: config.bucket,
1359
- prefix: config.prefix,
1360
- streaming: driverConfig.streaming,
1361
- signedUrlExpiry: driverConfig.signedUrlExpiry,
1362
- maxAge: driverConfig.maxAge,
1363
- cacheControl: driverConfig.cacheControl,
1364
- contentDisposition: driverConfig.contentDisposition,
1365
- etag: driverConfig.etag,
1366
- cors: driverConfig.cors
1367
- });
1368
-
1369
- } else {
1370
- throw new Error(
1371
- `static[${index}]: invalid driver "${config.driver}". Valid drivers: filesystem, s3`
1372
- );
1373
- }
1374
-
1375
- // Mount handler at specified path
1376
- // Use wildcard to match all sub-paths
1377
- const mountPath = config.path === '/' ? '/*' : `${config.path}/*`;
1378
- this.app.get(mountPath, handler);
1379
- this.app.head(mountPath, handler);
1380
-
1381
- if (this.options.verbose) {
1382
- console.log(
1383
- `[API Plugin] Mounted static files (${config.driver}) at ${config.path}` +
1384
- (config.driver === 'filesystem' ? ` -> ${config.root}` : ` -> s3://${config.bucket}/${config.prefix || ''}`)
1385
- );
1386
- }
1387
-
1388
- } catch (err) {
1389
- console.error(`[API Plugin] Failed to setup static files for index ${index}:`, err.message);
1390
- throw err;
1391
- }
1392
- }
1393
- }
1394
-
1395
- /**
1396
- * Start the server
1397
- * @returns {Promise<void>}
1398
- */
1399
- async start() {
1400
- if (this.isRunning) {
1401
- console.warn('[API Plugin] Server is already running');
1402
- return;
1403
- }
1404
-
1405
- // Dynamic import of Hono dependencies (peer dependencies)
1406
- // This ensures hono is only loaded when server actually starts
1407
- if (!this.initialized) {
1408
- const { Hono } = await import('hono');
1409
- const { serve } = await import('@hono/node-server');
1410
- const { swaggerUI } = await import('@hono/swagger-ui');
1411
- const { cors } = await import('hono/cors');
1412
-
1413
- // Store for use in _setupRoutes
1414
- this.Hono = Hono;
1415
- this.serve = serve;
1416
- this.swaggerUI = swaggerUI;
1417
- this.cors = cors;
1418
-
1419
- // Initialize app
1420
- this.app = new Hono();
1421
-
1422
- // Initialize failban manager if enabled
1423
- if (this.failban) {
1424
- await this.failban.initialize();
1425
- }
1426
-
1427
- // Setup all routes
1428
- this._setupRoutes();
1429
-
1430
- this.initialized = true;
1431
- }
1432
-
1433
- const { port, host } = this.options;
1434
-
1435
- return new Promise((resolve, reject) => {
1436
- try {
1437
- this.server = this.serve({
1438
- fetch: this.app.fetch,
1439
- port,
1440
- hostname: host
1441
- }, (info) => {
1442
- this.isRunning = true;
1443
- console.log(`[API Plugin] Server listening on http://${info.address}:${info.port}`);
1444
-
1445
- // Setup graceful shutdown on SIGTERM/SIGINT
1446
- const shutdownHandler = async (signal) => {
1447
- console.log(`[API Server] Received ${signal}, initiating graceful shutdown...`);
1448
- try {
1449
- await this.shutdown({ timeout: 30000 });
1450
- process.exit(0);
1451
- } catch (err) {
1452
- console.error('[API Server] Error during shutdown:', err);
1453
- process.exit(1);
1454
- }
1455
- };
1456
-
1457
- process.once('SIGTERM', () => shutdownHandler('SIGTERM'));
1458
- process.once('SIGINT', () => shutdownHandler('SIGINT'));
1459
-
1460
- resolve();
1461
- });
1462
- } catch (err) {
1463
- reject(err);
1464
- }
1465
- });
1466
- }
1467
-
1468
- /**
1469
- * Stop the server
1470
- * @returns {Promise<void>}
1471
- */
1472
- async stop() {
1473
- if (!this.isRunning) {
1474
- console.warn('[API Plugin] Server is not running');
1475
- return;
1476
- }
1477
-
1478
- if (this.server && typeof this.server.close === 'function') {
1479
- await new Promise((resolve) => {
1480
- this.server.close(() => {
1481
- this.isRunning = false;
1482
- console.log('[API Plugin] Server stopped');
1483
- resolve();
1484
- });
1485
- });
1486
- } else {
1487
- // For some Hono adapters, server might not have close method
1488
- this.isRunning = false;
1489
- console.log('[API Plugin] Server stopped');
1490
- }
1491
-
1492
- // Cleanup metrics collector
1493
- if (this.metrics) {
1494
- this.metrics.stop();
1495
- }
1496
-
1497
- // Cleanup failban plugin
1498
- if (this.failban) {
1499
- await this.failban.cleanup();
1500
- }
1501
- }
1502
-
1503
- /**
1504
- * Get server info
1505
- * @returns {Object} Server information
1506
- */
1507
- getInfo() {
1508
- return {
1509
- isRunning: this.isRunning,
1510
- port: this.options.port,
1511
- host: this.options.host,
1512
- resources: Object.keys(this.options.database.resources).length
1513
- };
1514
- }
1515
-
1516
- /**
1517
- * Get Hono app instance
1518
- * @returns {Hono} Hono app
1519
- */
1520
- getApp() {
1521
- return this.app;
1522
- }
1523
-
1524
- /**
1525
- * Generate OpenAPI specification
1526
- * @private
1527
- * @returns {Object} OpenAPI spec
1528
- */
1529
516
  _generateOpenAPISpec() {
1530
- const { port, host, database, resources, auth, apiInfo, versionPrefix } = this.options;
1531
-
1532
- return generateOpenAPISpec(database, {
1533
- title: apiInfo.title,
1534
- version: apiInfo.version,
1535
- description: apiInfo.description,
1536
- serverUrl: `http://${host === '0.0.0.0' ? 'localhost' : host}:${port}`,
1537
- auth,
1538
- resources,
1539
- versionPrefix
1540
- });
517
+ return this.openApiGenerator.generate();
1541
518
  }
1542
519
  }
1543
520