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.
- package/README.md +56 -15
- package/dist/s3db.cjs +72446 -39022
- package/dist/s3db.cjs.map +1 -1
- package/dist/s3db.es.js +72172 -38790
- package/dist/s3db.es.js.map +1 -1
- package/mcp/lib/base-handler.js +157 -0
- package/mcp/lib/handlers/connection-handler.js +280 -0
- package/mcp/lib/handlers/query-handler.js +533 -0
- package/mcp/lib/handlers/resource-handler.js +428 -0
- package/mcp/lib/tool-registry.js +336 -0
- package/mcp/lib/tools/connection-tools.js +161 -0
- package/mcp/lib/tools/query-tools.js +267 -0
- package/mcp/lib/tools/resource-tools.js +404 -0
- package/package.json +85 -50
- package/src/clients/memory-client.class.js +346 -191
- package/src/clients/memory-storage.class.js +300 -84
- package/src/clients/s3-client.class.js +7 -6
- package/src/concerns/geo-encoding.js +19 -2
- package/src/concerns/ip.js +59 -9
- package/src/concerns/money.js +8 -1
- package/src/concerns/password-hashing.js +49 -8
- package/src/concerns/plugin-storage.js +186 -18
- package/src/concerns/storage-drivers/filesystem-driver.js +284 -0
- package/src/database.class.js +139 -29
- package/src/errors.js +332 -42
- package/src/plugins/api/auth/oidc-auth.js +66 -17
- package/src/plugins/api/auth/strategies/base-strategy.class.js +74 -0
- package/src/plugins/api/auth/strategies/factory.class.js +63 -0
- package/src/plugins/api/auth/strategies/global-strategy.class.js +44 -0
- package/src/plugins/api/auth/strategies/path-based-strategy.class.js +83 -0
- package/src/plugins/api/auth/strategies/path-rules-strategy.class.js +118 -0
- package/src/plugins/api/concerns/failban-manager.js +106 -57
- package/src/plugins/api/concerns/route-context.js +601 -0
- package/src/plugins/api/index.js +168 -40
- package/src/plugins/api/routes/auth-routes.js +198 -30
- package/src/plugins/api/routes/resource-routes.js +19 -4
- package/src/plugins/api/server/health-manager.class.js +163 -0
- package/src/plugins/api/server/middleware-chain.class.js +310 -0
- package/src/plugins/api/server/router.class.js +472 -0
- package/src/plugins/api/server.js +280 -1303
- package/src/plugins/api/utils/custom-routes.js +17 -5
- package/src/plugins/api/utils/guards.js +76 -17
- package/src/plugins/api/utils/openapi-generator-cached.class.js +133 -0
- package/src/plugins/api/utils/openapi-generator.js +7 -6
- package/src/plugins/audit.plugin.js +30 -8
- package/src/plugins/backup.plugin.js +110 -14
- package/src/plugins/cache/cache.class.js +22 -5
- package/src/plugins/cache/filesystem-cache.class.js +116 -19
- package/src/plugins/cache/memory-cache.class.js +211 -57
- package/src/plugins/cache/multi-tier-cache.class.js +371 -0
- package/src/plugins/cache/partition-aware-filesystem-cache.class.js +168 -47
- package/src/plugins/cache/redis-cache.class.js +552 -0
- package/src/plugins/cache/s3-cache.class.js +17 -8
- package/src/plugins/cache.plugin.js +176 -61
- package/src/plugins/cloud-inventory/drivers/alibaba-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/aws-driver.js +60 -29
- package/src/plugins/cloud-inventory/drivers/azure-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/base-driver.js +16 -2
- package/src/plugins/cloud-inventory/drivers/cloudflare-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/digitalocean-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/hetzner-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/linode-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/mongodb-atlas-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/vultr-driver.js +8 -1
- package/src/plugins/cloud-inventory/index.js +29 -8
- package/src/plugins/cloud-inventory/registry.js +64 -42
- package/src/plugins/cloud-inventory.plugin.js +240 -138
- package/src/plugins/concerns/plugin-dependencies.js +54 -0
- package/src/plugins/concerns/resource-names.js +100 -0
- package/src/plugins/consumers/index.js +10 -2
- package/src/plugins/consumers/sqs-consumer.js +12 -2
- package/src/plugins/cookie-farm-suite.plugin.js +278 -0
- package/src/plugins/cookie-farm.errors.js +73 -0
- package/src/plugins/cookie-farm.plugin.js +869 -0
- package/src/plugins/costs.plugin.js +7 -1
- package/src/plugins/eventual-consistency/analytics.js +94 -19
- package/src/plugins/eventual-consistency/config.js +15 -7
- package/src/plugins/eventual-consistency/consolidation.js +29 -11
- package/src/plugins/eventual-consistency/garbage-collection.js +3 -1
- package/src/plugins/eventual-consistency/helpers.js +39 -14
- package/src/plugins/eventual-consistency/install.js +21 -2
- package/src/plugins/eventual-consistency/utils.js +32 -10
- package/src/plugins/fulltext.plugin.js +38 -11
- package/src/plugins/geo.plugin.js +61 -9
- package/src/plugins/identity/concerns/config.js +61 -0
- package/src/plugins/identity/concerns/mfa-manager.js +15 -2
- package/src/plugins/identity/concerns/rate-limit.js +124 -0
- package/src/plugins/identity/concerns/resource-schemas.js +9 -1
- package/src/plugins/identity/concerns/token-generator.js +29 -4
- package/src/plugins/identity/drivers/auth-driver.interface.js +76 -0
- package/src/plugins/identity/drivers/client-credentials-driver.js +127 -0
- package/src/plugins/identity/drivers/index.js +18 -0
- package/src/plugins/identity/drivers/password-driver.js +122 -0
- package/src/plugins/identity/email-service.js +17 -2
- package/src/plugins/identity/index.js +413 -69
- package/src/plugins/identity/oauth2-server.js +413 -30
- package/src/plugins/identity/oidc-discovery.js +16 -8
- package/src/plugins/identity/rsa-keys.js +115 -35
- package/src/plugins/identity/server.js +166 -45
- package/src/plugins/identity/session-manager.js +53 -7
- package/src/plugins/identity/ui/pages/mfa-verification.js +17 -15
- package/src/plugins/identity/ui/routes.js +363 -255
- package/src/plugins/importer/index.js +153 -20
- package/src/plugins/index.js +9 -2
- package/src/plugins/kubernetes-inventory/index.js +6 -0
- package/src/plugins/kubernetes-inventory/k8s-driver.js +867 -0
- package/src/plugins/kubernetes-inventory/resource-types.js +274 -0
- package/src/plugins/kubernetes-inventory.plugin.js +980 -0
- package/src/plugins/metrics.plugin.js +64 -16
- package/src/plugins/ml/base-model.class.js +25 -15
- package/src/plugins/ml/regression-model.class.js +1 -1
- package/src/plugins/ml.errors.js +57 -25
- package/src/plugins/ml.plugin.js +28 -4
- package/src/plugins/namespace.js +210 -0
- package/src/plugins/plugin.class.js +180 -8
- package/src/plugins/puppeteer/console-monitor.js +729 -0
- package/src/plugins/puppeteer/cookie-manager.js +492 -0
- package/src/plugins/puppeteer/network-monitor.js +816 -0
- package/src/plugins/puppeteer/performance-manager.js +746 -0
- package/src/plugins/puppeteer/proxy-manager.js +478 -0
- package/src/plugins/puppeteer/stealth-manager.js +556 -0
- package/src/plugins/puppeteer.errors.js +81 -0
- package/src/plugins/puppeteer.plugin.js +1327 -0
- package/src/plugins/queue-consumer.plugin.js +69 -14
- package/src/plugins/recon/behaviors/uptime-behavior.js +691 -0
- package/src/plugins/recon/concerns/command-runner.js +148 -0
- package/src/plugins/recon/concerns/diff-detector.js +372 -0
- package/src/plugins/recon/concerns/fingerprint-builder.js +307 -0
- package/src/plugins/recon/concerns/process-manager.js +338 -0
- package/src/plugins/recon/concerns/report-generator.js +478 -0
- package/src/plugins/recon/concerns/security-analyzer.js +571 -0
- package/src/plugins/recon/concerns/target-normalizer.js +68 -0
- package/src/plugins/recon/config/defaults.js +321 -0
- package/src/plugins/recon/config/resources.js +370 -0
- package/src/plugins/recon/index.js +778 -0
- package/src/plugins/recon/managers/dependency-manager.js +174 -0
- package/src/plugins/recon/managers/scheduler-manager.js +179 -0
- package/src/plugins/recon/managers/storage-manager.js +745 -0
- package/src/plugins/recon/managers/target-manager.js +274 -0
- package/src/plugins/recon/stages/asn-stage.js +314 -0
- package/src/plugins/recon/stages/certificate-stage.js +84 -0
- package/src/plugins/recon/stages/dns-stage.js +107 -0
- package/src/plugins/recon/stages/dnsdumpster-stage.js +362 -0
- package/src/plugins/recon/stages/fingerprint-stage.js +71 -0
- package/src/plugins/recon/stages/google-dorks-stage.js +440 -0
- package/src/plugins/recon/stages/http-stage.js +89 -0
- package/src/plugins/recon/stages/latency-stage.js +148 -0
- package/src/plugins/recon/stages/massdns-stage.js +302 -0
- package/src/plugins/recon/stages/osint-stage.js +1373 -0
- package/src/plugins/recon/stages/ports-stage.js +169 -0
- package/src/plugins/recon/stages/screenshot-stage.js +94 -0
- package/src/plugins/recon/stages/secrets-stage.js +514 -0
- package/src/plugins/recon/stages/subdomains-stage.js +295 -0
- package/src/plugins/recon/stages/tls-audit-stage.js +78 -0
- package/src/plugins/recon/stages/vulnerability-stage.js +78 -0
- package/src/plugins/recon/stages/web-discovery-stage.js +113 -0
- package/src/plugins/recon/stages/whois-stage.js +349 -0
- package/src/plugins/recon.plugin.js +75 -0
- package/src/plugins/recon.plugin.js.backup +2635 -0
- package/src/plugins/relation.errors.js +87 -14
- package/src/plugins/replicator.plugin.js +514 -137
- package/src/plugins/replicators/base-replicator.class.js +89 -1
- package/src/plugins/replicators/bigquery-replicator.class.js +66 -22
- package/src/plugins/replicators/dynamodb-replicator.class.js +22 -15
- package/src/plugins/replicators/mongodb-replicator.class.js +22 -15
- package/src/plugins/replicators/mysql-replicator.class.js +52 -17
- package/src/plugins/replicators/planetscale-replicator.class.js +30 -4
- package/src/plugins/replicators/postgres-replicator.class.js +62 -27
- package/src/plugins/replicators/s3db-replicator.class.js +25 -18
- package/src/plugins/replicators/schema-sync.helper.js +3 -3
- package/src/plugins/replicators/sqs-replicator.class.js +8 -2
- package/src/plugins/replicators/turso-replicator.class.js +23 -3
- package/src/plugins/replicators/webhook-replicator.class.js +42 -4
- package/src/plugins/s3-queue.plugin.js +464 -65
- package/src/plugins/scheduler.plugin.js +20 -6
- package/src/plugins/state-machine.plugin.js +40 -9
- package/src/plugins/tfstate/base-driver.js +28 -4
- package/src/plugins/tfstate/errors.js +65 -10
- package/src/plugins/tfstate/filesystem-driver.js +52 -8
- package/src/plugins/tfstate/index.js +163 -90
- package/src/plugins/tfstate/s3-driver.js +64 -6
- package/src/plugins/ttl.plugin.js +72 -17
- package/src/plugins/vector/distances.js +18 -12
- package/src/plugins/vector/kmeans.js +26 -4
- package/src/resource.class.js +115 -19
- package/src/testing/factory.class.js +20 -3
- package/src/testing/seeder.class.js +7 -1
- package/src/clients/memory-client.md +0 -917
- 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 || {},
|
|
54
|
-
templates: options.templates || { enabled: false, engine: 'jsx' },
|
|
30
|
+
routes: options.routes || {},
|
|
31
|
+
templates: options.templates || { enabled: false, engine: 'jsx' },
|
|
55
32
|
middlewares: options.middlewares || [],
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
events: options.events || { enabled: false },
|
|
61
|
-
metrics: options.metrics || { enabled: false },
|
|
62
|
-
failban: options.failban || { enabled: false },
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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;
|
|
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
|
-
|
|
85
|
-
this.
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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.
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
this.
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
method: data.allowedMethods?.[0] || 'unknown'
|
|
167
|
-
});
|
|
168
|
-
});
|
|
140
|
+
if (this.failban) {
|
|
141
|
+
await this.failban.initialize();
|
|
142
|
+
}
|
|
169
143
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
186
|
-
this.metrics.recordResourceOperation({
|
|
187
|
-
action: 'deleted',
|
|
188
|
-
resource: data.resource
|
|
189
|
-
});
|
|
190
|
-
});
|
|
167
|
+
const authMiddleware = this._createAuthMiddleware();
|
|
191
168
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
206
|
-
console.log('[API Server] Metrics event listeners configured');
|
|
209
|
+
this.initialized = true;
|
|
207
210
|
}
|
|
208
|
-
}
|
|
209
211
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
245
|
+
async stop() {
|
|
246
|
+
if (!this.isRunning) {
|
|
247
|
+
console.warn('[API Plugin] Server is not running');
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
224
250
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
235
|
-
this.
|
|
264
|
+
if (this.metrics) {
|
|
265
|
+
this.metrics.stop();
|
|
266
|
+
}
|
|
236
267
|
|
|
237
|
-
|
|
238
|
-
|
|
268
|
+
if (this.failban) {
|
|
269
|
+
await this.failban.cleanup();
|
|
270
|
+
}
|
|
271
|
+
}
|
|
239
272
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
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
|
-
|
|
324
|
-
|
|
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
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
-
|
|
354
|
+
});
|
|
359
355
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
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
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
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
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
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
|
-
|
|
581
|
-
|
|
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
|
-
|
|
603
|
-
|
|
402
|
+
this.events.on('user:login', () => {
|
|
403
|
+
this.metrics.recordUserEvent({ action: 'login' });
|
|
604
404
|
});
|
|
605
405
|
|
|
606
|
-
|
|
607
|
-
|
|
406
|
+
if (this.options.verbose) {
|
|
407
|
+
console.log('[API Server] Metrics event listeners configured');
|
|
408
|
+
}
|
|
409
|
+
}
|
|
608
410
|
|
|
609
|
-
|
|
411
|
+
_setupDocumentationRoutes() {
|
|
610
412
|
if (this.options.docsEnabled) {
|
|
611
413
|
this.app.get('/openapi.json', (c) => {
|
|
612
|
-
|
|
613
|
-
|
|
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.
|
|
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
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
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
|
-
|
|
656
|
-
|
|
657
|
-
|
|
444
|
+
if (this.options.docsEnabled) {
|
|
445
|
+
return c.redirect('/docs', 302);
|
|
446
|
+
}
|
|
658
447
|
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
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
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
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
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
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
|
-
|
|
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
|
|