s3db.js 13.5.1 → 13.6.1
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 +89 -19
- package/dist/{s3db.cjs.js → s3db.cjs} +29780 -24384
- package/dist/s3db.cjs.map +1 -0
- package/dist/s3db.es.js +24263 -18860
- package/dist/s3db.es.js.map +1 -1
- package/package.json +227 -21
- package/src/concerns/id.js +90 -6
- package/src/concerns/index.js +2 -1
- package/src/concerns/password-hashing.js +150 -0
- package/src/database.class.js +4 -0
- package/src/plugins/api/auth/basic-auth.js +23 -1
- package/src/plugins/api/auth/index.js +49 -3
- package/src/plugins/api/auth/oauth2-auth.js +171 -0
- package/src/plugins/api/auth/oidc-auth.js +789 -0
- package/src/plugins/api/auth/oidc-client.js +462 -0
- package/src/plugins/api/auth/path-auth-matcher.js +284 -0
- package/src/plugins/api/concerns/event-emitter.js +134 -0
- package/src/plugins/api/concerns/failban-manager.js +651 -0
- package/src/plugins/api/concerns/guards-helpers.js +402 -0
- package/src/plugins/api/concerns/metrics-collector.js +346 -0
- package/src/plugins/api/concerns/opengraph-helper.js +116 -0
- package/src/plugins/api/concerns/state-machine.js +288 -0
- package/src/plugins/api/index.js +514 -54
- package/src/plugins/api/middlewares/failban.js +305 -0
- package/src/plugins/api/middlewares/rate-limit.js +301 -0
- package/src/plugins/api/middlewares/request-id.js +74 -0
- package/src/plugins/api/middlewares/security-headers.js +120 -0
- package/src/plugins/api/middlewares/session-tracking.js +194 -0
- package/src/plugins/api/routes/auth-routes.js +23 -3
- package/src/plugins/api/routes/resource-routes.js +71 -29
- package/src/plugins/api/server.js +1017 -94
- package/src/plugins/api/utils/guards.js +213 -0
- package/src/plugins/api/utils/mime-types.js +154 -0
- package/src/plugins/api/utils/openapi-generator.js +44 -11
- package/src/plugins/api/utils/path-matcher.js +173 -0
- package/src/plugins/api/utils/static-filesystem.js +262 -0
- package/src/plugins/api/utils/static-s3.js +231 -0
- package/src/plugins/api/utils/template-engine.js +262 -0
- package/src/plugins/cloud-inventory/drivers/alibaba-driver.js +853 -0
- package/src/plugins/cloud-inventory/drivers/aws-driver.js +2554 -0
- package/src/plugins/cloud-inventory/drivers/azure-driver.js +637 -0
- package/src/plugins/cloud-inventory/drivers/base-driver.js +99 -0
- package/src/plugins/cloud-inventory/drivers/cloudflare-driver.js +620 -0
- package/src/plugins/cloud-inventory/drivers/digitalocean-driver.js +698 -0
- package/src/plugins/cloud-inventory/drivers/gcp-driver.js +645 -0
- package/src/plugins/cloud-inventory/drivers/hetzner-driver.js +559 -0
- package/src/plugins/cloud-inventory/drivers/linode-driver.js +614 -0
- package/src/plugins/cloud-inventory/drivers/mock-drivers.js +449 -0
- package/src/plugins/cloud-inventory/drivers/mongodb-atlas-driver.js +771 -0
- package/src/plugins/cloud-inventory/drivers/oracle-driver.js +768 -0
- package/src/plugins/cloud-inventory/drivers/vultr-driver.js +636 -0
- package/src/plugins/cloud-inventory/index.js +20 -0
- package/src/plugins/cloud-inventory/registry.js +146 -0
- package/src/plugins/cloud-inventory/terraform-exporter.js +362 -0
- package/src/plugins/cloud-inventory.plugin.js +1333 -0
- package/src/plugins/concerns/plugin-dependencies.js +61 -1
- package/src/plugins/eventual-consistency/analytics.js +1 -0
- package/src/plugins/identity/README.md +335 -0
- package/src/plugins/identity/concerns/mfa-manager.js +204 -0
- package/src/plugins/identity/concerns/password.js +138 -0
- package/src/plugins/identity/concerns/resource-schemas.js +273 -0
- package/src/plugins/identity/concerns/token-generator.js +172 -0
- package/src/plugins/identity/email-service.js +422 -0
- package/src/plugins/identity/index.js +1052 -0
- package/src/plugins/identity/oauth2-server.js +1033 -0
- package/src/plugins/identity/oidc-discovery.js +285 -0
- package/src/plugins/identity/rsa-keys.js +323 -0
- package/src/plugins/identity/server.js +500 -0
- package/src/plugins/identity/session-manager.js +453 -0
- package/src/plugins/identity/ui/layouts/base.js +251 -0
- package/src/plugins/identity/ui/middleware.js +135 -0
- package/src/plugins/identity/ui/pages/admin/client-form.js +247 -0
- package/src/plugins/identity/ui/pages/admin/clients.js +179 -0
- package/src/plugins/identity/ui/pages/admin/dashboard.js +181 -0
- package/src/plugins/identity/ui/pages/admin/user-form.js +283 -0
- package/src/plugins/identity/ui/pages/admin/users.js +263 -0
- package/src/plugins/identity/ui/pages/consent.js +262 -0
- package/src/plugins/identity/ui/pages/forgot-password.js +104 -0
- package/src/plugins/identity/ui/pages/login.js +144 -0
- package/src/plugins/identity/ui/pages/mfa-backup-codes.js +180 -0
- package/src/plugins/identity/ui/pages/mfa-enrollment.js +187 -0
- package/src/plugins/identity/ui/pages/mfa-verification.js +178 -0
- package/src/plugins/identity/ui/pages/oauth-error.js +225 -0
- package/src/plugins/identity/ui/pages/profile.js +361 -0
- package/src/plugins/identity/ui/pages/register.js +226 -0
- package/src/plugins/identity/ui/pages/reset-password.js +128 -0
- package/src/plugins/identity/ui/pages/verify-email.js +172 -0
- package/src/plugins/identity/ui/routes.js +2541 -0
- package/src/plugins/identity/ui/styles/main.css +465 -0
- package/src/plugins/index.js +4 -1
- package/src/plugins/ml/base-model.class.js +32 -7
- package/src/plugins/ml/classification-model.class.js +1 -1
- package/src/plugins/ml/timeseries-model.class.js +3 -1
- package/src/plugins/ml.plugin.js +124 -32
- package/src/plugins/shared/error-handler.js +147 -0
- package/src/plugins/shared/index.js +9 -0
- package/src/plugins/shared/middlewares/compression.js +117 -0
- package/src/plugins/shared/middlewares/cors.js +49 -0
- package/src/plugins/shared/middlewares/index.js +11 -0
- package/src/plugins/shared/middlewares/logging.js +54 -0
- package/src/plugins/shared/middlewares/rate-limit.js +73 -0
- package/src/plugins/shared/middlewares/security.js +158 -0
- package/src/plugins/shared/response-formatter.js +264 -0
- package/src/plugins/tfstate/README.md +126 -126
- package/src/resource.class.js +140 -12
- package/src/schema.class.js +30 -1
- package/src/validator.class.js +57 -6
- package/dist/s3db.cjs.js.map +0 -1
|
@@ -7,11 +7,28 @@
|
|
|
7
7
|
import { createResourceRoutes, createRelationalRoutes } from './routes/resource-routes.js';
|
|
8
8
|
import { createAuthRoutes } from './routes/auth-routes.js';
|
|
9
9
|
import { mountCustomRoutes } from './utils/custom-routes.js';
|
|
10
|
-
import { errorHandler } from '
|
|
11
|
-
import * as formatter from '
|
|
10
|
+
import { errorHandler } from '../shared/error-handler.js';
|
|
11
|
+
import * as formatter from '../shared/response-formatter.js';
|
|
12
12
|
import { generateOpenAPISpec } from './utils/openapi-generator.js';
|
|
13
|
+
import { createAuthMiddleware } from './auth/index.js';
|
|
14
|
+
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';
|
|
13
20
|
import { jwtAuth } from './auth/jwt-auth.js';
|
|
21
|
+
import { apiKeyAuth } from './auth/api-key-auth.js';
|
|
14
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
|
+
import { FailbanManager } from './concerns/failban-manager.js';
|
|
30
|
+
import { ApiEventEmitter } from './concerns/event-emitter.js';
|
|
31
|
+
import { MetricsCollector } from './concerns/metrics-collector.js';
|
|
15
32
|
|
|
16
33
|
/**
|
|
17
34
|
* API Server class
|
|
@@ -34,9 +51,18 @@ export class ApiServer {
|
|
|
34
51
|
database: options.database,
|
|
35
52
|
resources: options.resources || {},
|
|
36
53
|
routes: options.routes || {}, // Plugin-level custom routes
|
|
54
|
+
templates: options.templates || { enabled: false, engine: 'jsx' }, // Template engine config
|
|
37
55
|
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
|
|
38
63
|
verbose: options.verbose || false,
|
|
39
64
|
auth: options.auth || {},
|
|
65
|
+
static: options.static || [], // Static file serving config
|
|
40
66
|
docsEnabled: options.docsEnabled !== false, // Enable /docs by default
|
|
41
67
|
docsUI: options.docsUI || 'redoc', // 'swagger' or 'redoc'
|
|
42
68
|
maxBodySize: options.maxBodySize || 10 * 1024 * 1024, // 10MB default
|
|
@@ -55,6 +81,47 @@ export class ApiServer {
|
|
|
55
81
|
this.openAPISpec = null;
|
|
56
82
|
this.initialized = false;
|
|
57
83
|
|
|
84
|
+
// Graceful shutdown tracking
|
|
85
|
+
this.inFlightRequests = new Set(); // Track in-flight requests
|
|
86
|
+
this.acceptingRequests = true; // Accept new requests flag
|
|
87
|
+
|
|
88
|
+
// Event emitter
|
|
89
|
+
this.events = new ApiEventEmitter({
|
|
90
|
+
enabled: this.options.events?.enabled !== false,
|
|
91
|
+
verbose: this.options.events?.verbose || this.options.verbose,
|
|
92
|
+
maxListeners: this.options.events?.maxListeners
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Metrics collector
|
|
96
|
+
this.metrics = new MetricsCollector({
|
|
97
|
+
enabled: this.options.metrics?.enabled !== false,
|
|
98
|
+
verbose: this.options.metrics?.verbose || this.options.verbose,
|
|
99
|
+
maxPathsTracked: this.options.metrics?.maxPathsTracked,
|
|
100
|
+
resetInterval: this.options.metrics?.resetInterval
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// Wire up event listeners to metrics collector
|
|
104
|
+
if (this.options.metrics?.enabled && this.options.events?.enabled !== false) {
|
|
105
|
+
this._setupMetricsEventListeners();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Failban manager (fail2ban-style automatic banning - internal feature)
|
|
109
|
+
this.failban = null;
|
|
110
|
+
if (this.options.failban?.enabled) {
|
|
111
|
+
this.failban = new FailbanManager({
|
|
112
|
+
database: this.options.database,
|
|
113
|
+
enabled: true,
|
|
114
|
+
maxViolations: this.options.failban.maxViolations || 3,
|
|
115
|
+
violationWindow: this.options.failban.violationWindow || 3600000,
|
|
116
|
+
banDuration: this.options.failban.banDuration || 86400000,
|
|
117
|
+
whitelist: this.options.failban.whitelist || ['127.0.0.1', '::1'],
|
|
118
|
+
blacklist: this.options.failban.blacklist || [],
|
|
119
|
+
persistViolations: this.options.failban.persistViolations !== false,
|
|
120
|
+
verbose: this.options.failban.verbose || this.options.verbose,
|
|
121
|
+
geo: this.options.failban.geo || {}
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
58
125
|
// Detect if RelationPlugin is installed
|
|
59
126
|
this.relationsPlugin = this.options.database?.plugins?.relation ||
|
|
60
127
|
this.options.database?.plugins?.RelationPlugin ||
|
|
@@ -63,16 +130,310 @@ export class ApiServer {
|
|
|
63
130
|
// Routes will be setup in start() after dynamic import
|
|
64
131
|
}
|
|
65
132
|
|
|
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
|
+
});
|
|
147
|
+
|
|
148
|
+
this.events.on('request:error', (data) => {
|
|
149
|
+
this.metrics.recordError({
|
|
150
|
+
error: data.error,
|
|
151
|
+
type: 'request'
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Auth metrics
|
|
156
|
+
this.events.on('auth:success', (data) => {
|
|
157
|
+
this.metrics.recordAuth({
|
|
158
|
+
success: true,
|
|
159
|
+
method: data.method
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
this.events.on('auth:failure', (data) => {
|
|
164
|
+
this.metrics.recordAuth({
|
|
165
|
+
success: false,
|
|
166
|
+
method: data.allowedMethods?.[0] || 'unknown'
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// Resource metrics
|
|
171
|
+
this.events.on('resource:created', (data) => {
|
|
172
|
+
this.metrics.recordResourceOperation({
|
|
173
|
+
action: 'created',
|
|
174
|
+
resource: data.resource
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
this.events.on('resource:updated', (data) => {
|
|
179
|
+
this.metrics.recordResourceOperation({
|
|
180
|
+
action: 'updated',
|
|
181
|
+
resource: data.resource
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
this.events.on('resource:deleted', (data) => {
|
|
186
|
+
this.metrics.recordResourceOperation({
|
|
187
|
+
action: 'deleted',
|
|
188
|
+
resource: data.resource
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// User metrics
|
|
193
|
+
this.events.on('user:created', (data) => {
|
|
194
|
+
this.metrics.recordUserEvent({
|
|
195
|
+
action: 'created'
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
this.events.on('user:login', (data) => {
|
|
200
|
+
this.metrics.recordUserEvent({
|
|
201
|
+
action: 'login'
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
if (this.options.verbose) {
|
|
206
|
+
console.log('[API Server] Metrics event listeners configured');
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
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);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Track this request
|
|
222
|
+
const requestId = Symbol('request');
|
|
223
|
+
this.inFlightRequests.add(requestId);
|
|
224
|
+
|
|
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
|
+
};
|
|
233
|
+
|
|
234
|
+
// Emit request:start
|
|
235
|
+
this.events.emitRequestEvent('start', requestInfo);
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
await next();
|
|
239
|
+
|
|
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
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Stop accepting new requests
|
|
264
|
+
* @returns {void}
|
|
265
|
+
*/
|
|
266
|
+
stopAcceptingRequests() {
|
|
267
|
+
this.acceptingRequests = false;
|
|
268
|
+
if (this.options.verbose) {
|
|
269
|
+
console.log('[API Server] Stopped accepting new requests');
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
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
|
+
async waitForRequestsToFinish({ timeout = 30000 } = {}) {
|
|
280
|
+
const startTime = Date.now();
|
|
281
|
+
|
|
282
|
+
while (this.inFlightRequests.size > 0) {
|
|
283
|
+
const elapsed = Date.now() - startTime;
|
|
284
|
+
|
|
285
|
+
if (elapsed >= timeout) {
|
|
286
|
+
if (this.options.verbose) {
|
|
287
|
+
console.warn(`[API Server] Timeout waiting for ${this.inFlightRequests.size} in-flight requests`);
|
|
288
|
+
}
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (this.options.verbose) {
|
|
293
|
+
console.log(`[API Server] Waiting for ${this.inFlightRequests.size} in-flight requests...`);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Wait 100ms before checking again
|
|
297
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (this.options.verbose) {
|
|
301
|
+
console.log('[API Server] All requests finished');
|
|
302
|
+
}
|
|
303
|
+
return true;
|
|
304
|
+
}
|
|
305
|
+
|
|
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
|
+
async shutdown({ timeout = 30000 } = {}) {
|
|
313
|
+
if (!this.isRunning) {
|
|
314
|
+
console.warn('[API Server] Server is not running');
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
console.log('[API Server] Initiating graceful shutdown...');
|
|
319
|
+
|
|
320
|
+
// Stop accepting new requests
|
|
321
|
+
this.stopAcceptingRequests();
|
|
322
|
+
|
|
323
|
+
// Wait for in-flight requests to finish
|
|
324
|
+
const allFinished = await this.waitForRequestsToFinish({ timeout });
|
|
325
|
+
|
|
326
|
+
if (!allFinished) {
|
|
327
|
+
console.warn('[API Server] Some requests did not finish in time');
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Close HTTP server
|
|
331
|
+
if (this.server) {
|
|
332
|
+
await new Promise((resolve, reject) => {
|
|
333
|
+
this.server.close((err) => {
|
|
334
|
+
if (err) reject(err);
|
|
335
|
+
else resolve();
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
this.isRunning = false;
|
|
341
|
+
console.log('[API Server] Shutdown complete');
|
|
342
|
+
}
|
|
343
|
+
|
|
66
344
|
/**
|
|
67
345
|
* Setup all routes
|
|
68
346
|
* @private
|
|
69
347
|
*/
|
|
70
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
|
|
357
|
+
});
|
|
358
|
+
this.app.use('*', failbanMiddleware);
|
|
359
|
+
|
|
360
|
+
// Setup violation listeners (connects events to failban)
|
|
361
|
+
setupFailbanViolationListener({
|
|
362
|
+
plugin: this.failban,
|
|
363
|
+
events: this.events
|
|
364
|
+
});
|
|
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
|
+
|
|
71
422
|
// Apply global middlewares
|
|
72
423
|
this.options.middlewares.forEach(middleware => {
|
|
73
424
|
this.app.use('*', middleware);
|
|
74
425
|
});
|
|
75
426
|
|
|
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
|
+
|
|
76
437
|
// Body size limit middleware (only for POST, PUT, PATCH)
|
|
77
438
|
this.app.use('*', async (c, next) => {
|
|
78
439
|
const method = c.req.method;
|
|
@@ -107,35 +468,91 @@ export class ApiServer {
|
|
|
107
468
|
|
|
108
469
|
// Kubernetes Readiness Probe - checks if app is ready to receive traffic
|
|
109
470
|
// If this fails, k8s will remove pod from service endpoints
|
|
110
|
-
this.app.get('/health/ready', (c) => {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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;
|
|
125
534
|
}
|
|
126
|
-
})
|
|
127
|
-
|
|
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
|
+
}
|
|
128
546
|
}
|
|
129
547
|
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
});
|
|
138
|
-
return c.json(response);
|
|
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);
|
|
139
556
|
});
|
|
140
557
|
|
|
141
558
|
// Generic Health Check endpoint
|
|
@@ -152,6 +569,29 @@ export class ApiServer {
|
|
|
152
569
|
return c.json(response);
|
|
153
570
|
});
|
|
154
571
|
|
|
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);
|
|
578
|
+
});
|
|
579
|
+
|
|
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
|
+
|
|
155
595
|
// Root endpoint - custom handler or redirect to docs
|
|
156
596
|
this.app.get('/', (c) => {
|
|
157
597
|
// If user provided a custom root handler, use it
|
|
@@ -163,6 +603,9 @@ export class ApiServer {
|
|
|
163
603
|
return c.redirect('/docs', 302);
|
|
164
604
|
});
|
|
165
605
|
|
|
606
|
+
// Setup static file serving (before resource routes to give static files priority)
|
|
607
|
+
this._setupStaticRoutes();
|
|
608
|
+
|
|
166
609
|
// OpenAPI spec endpoint
|
|
167
610
|
if (this.options.docsEnabled) {
|
|
168
611
|
this.app.get('/openapi.json', (c) => {
|
|
@@ -204,11 +647,21 @@ export class ApiServer {
|
|
|
204
647
|
// Setup resource routes
|
|
205
648
|
this._setupResourceRoutes();
|
|
206
649
|
|
|
207
|
-
// Setup authentication routes if driver is configured
|
|
208
|
-
|
|
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;
|
|
654
|
+
|
|
655
|
+
if (this.options.auth?.driver || hasJwtDriver) {
|
|
209
656
|
this._setupAuthRoutes();
|
|
210
657
|
}
|
|
211
658
|
|
|
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
|
+
|
|
212
665
|
// Setup relational routes if RelationPlugin is active
|
|
213
666
|
if (this.relationsPlugin) {
|
|
214
667
|
this._setupRelationalRoutes();
|
|
@@ -241,33 +694,48 @@ export class ApiServer {
|
|
|
241
694
|
* @private
|
|
242
695
|
*/
|
|
243
696
|
_setupResourceRoutes() {
|
|
244
|
-
const { database, resources: resourceConfigs } = this.options;
|
|
697
|
+
const { database, resources: resourceConfigs = {} } = this.options;
|
|
245
698
|
|
|
246
699
|
// Get all resources from database
|
|
247
700
|
const resources = database.resources;
|
|
248
701
|
|
|
702
|
+
// Create global auth middleware (applies to all resources, guards control access)
|
|
703
|
+
const authMiddleware = this._createAuthMiddleware();
|
|
704
|
+
|
|
249
705
|
for (const [name, resource] of Object.entries(resources)) {
|
|
250
|
-
|
|
251
|
-
|
|
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
|
+
}
|
|
252
714
|
continue;
|
|
253
715
|
}
|
|
254
716
|
|
|
255
|
-
//
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
+
}
|
|
260
724
|
|
|
261
725
|
// Determine version
|
|
262
726
|
const version = resource.config?.currentVersion || resource.version || 'v1';
|
|
263
727
|
|
|
264
728
|
// Determine version prefix (resource-level overrides global)
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
+
}
|
|
271
739
|
|
|
272
740
|
// Calculate the actual prefix to use
|
|
273
741
|
let prefix = '';
|
|
@@ -283,22 +751,51 @@ export class ApiServer {
|
|
|
283
751
|
}
|
|
284
752
|
|
|
285
753
|
// Prepare custom middleware
|
|
286
|
-
const middlewares = [
|
|
754
|
+
const middlewares = [];
|
|
755
|
+
|
|
756
|
+
// Add global authentication middleware unless explicitly disabled
|
|
757
|
+
const authDisabled = resourceConfig?.auth === false;
|
|
287
758
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
+
}
|
|
293
774
|
}
|
|
294
775
|
}
|
|
295
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
|
+
|
|
296
792
|
// Create resource routes
|
|
297
793
|
const resourceApp = createResourceRoutes(resource, version, {
|
|
298
|
-
methods
|
|
794
|
+
methods,
|
|
299
795
|
customMiddleware: middlewares,
|
|
300
|
-
enableValidation
|
|
301
|
-
versionPrefix: prefix
|
|
796
|
+
enableValidation,
|
|
797
|
+
versionPrefix: prefix,
|
|
798
|
+
events: this.events
|
|
302
799
|
}, this.Hono);
|
|
303
800
|
|
|
304
801
|
// Mount resource routes (with or without prefix)
|
|
@@ -309,8 +806,8 @@ export class ApiServer {
|
|
|
309
806
|
console.log(`[API Plugin] Mounted routes for resource '${name}' at ${mountPath}`);
|
|
310
807
|
}
|
|
311
808
|
|
|
312
|
-
// Mount custom routes for this resource
|
|
313
|
-
if (config
|
|
809
|
+
// Mount custom routes for this resource
|
|
810
|
+
if (resource.config?.routes) {
|
|
314
811
|
const routeContext = {
|
|
315
812
|
resource,
|
|
316
813
|
database,
|
|
@@ -319,18 +816,26 @@ export class ApiServer {
|
|
|
319
816
|
};
|
|
320
817
|
|
|
321
818
|
// Mount on the resourceApp (nested under resource path)
|
|
322
|
-
mountCustomRoutes(resourceApp, config.routes, routeContext, this.options.verbose);
|
|
819
|
+
mountCustomRoutes(resourceApp, resource.config.routes, routeContext, this.options.verbose);
|
|
323
820
|
}
|
|
324
821
|
}
|
|
325
822
|
}
|
|
326
823
|
|
|
327
824
|
/**
|
|
328
|
-
* Setup authentication routes (when auth
|
|
825
|
+
* Setup authentication routes (when auth drivers are configured)
|
|
329
826
|
* @private
|
|
330
827
|
*/
|
|
331
828
|
_setupAuthRoutes() {
|
|
332
829
|
const { database, auth } = this.options;
|
|
333
|
-
const {
|
|
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
|
+
}
|
|
334
839
|
|
|
335
840
|
// Get auth resource from database
|
|
336
841
|
const authResource = database.resources[resourceName];
|
|
@@ -339,15 +844,17 @@ export class ApiServer {
|
|
|
339
844
|
return;
|
|
340
845
|
}
|
|
341
846
|
|
|
847
|
+
const driverConfig = jwtDriver.config || {};
|
|
848
|
+
|
|
342
849
|
// Prepare auth config for routes
|
|
343
850
|
const authConfig = {
|
|
344
|
-
driver,
|
|
851
|
+
driver: 'jwt',
|
|
345
852
|
usernameField,
|
|
346
853
|
passwordField,
|
|
347
|
-
jwtSecret:
|
|
348
|
-
jwtExpiresIn:
|
|
349
|
-
passphrase:
|
|
350
|
-
allowRegistration:
|
|
854
|
+
jwtSecret: driverConfig.jwtSecret || driverConfig.secret,
|
|
855
|
+
jwtExpiresIn: driverConfig.jwtExpiresIn || driverConfig.expiresIn || '7d',
|
|
856
|
+
passphrase: driverConfig.passphrase || 'secret',
|
|
857
|
+
allowRegistration: driverConfig.allowRegistration !== false
|
|
351
858
|
};
|
|
352
859
|
|
|
353
860
|
// Create auth routes
|
|
@@ -357,56 +864,339 @@ export class ApiServer {
|
|
|
357
864
|
this.app.route('/auth', authApp);
|
|
358
865
|
|
|
359
866
|
if (this.options.verbose) {
|
|
360
|
-
console.log(
|
|
867
|
+
console.log('[API Plugin] Mounted auth routes (driver: jwt) at /auth');
|
|
361
868
|
}
|
|
362
869
|
}
|
|
363
870
|
|
|
364
871
|
/**
|
|
365
|
-
*
|
|
872
|
+
* Setup OIDC routes (when oidc driver is configured)
|
|
366
873
|
* @private
|
|
367
|
-
* @
|
|
874
|
+
* @param {Object} config - OIDC driver configuration
|
|
875
|
+
*/
|
|
876
|
+
_setupOIDCRoutes(config) {
|
|
877
|
+
const { database, auth } = this.options;
|
|
878
|
+
const authResource = database.resources[auth.resource];
|
|
879
|
+
|
|
880
|
+
if (!authResource) {
|
|
881
|
+
console.error(`[API Plugin] Auth resource '${auth.resource}' not found for OIDC`);
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// Create OIDC handler (which creates routes + middleware)
|
|
886
|
+
const oidcHandler = createOIDCHandler(config, this.app, authResource, this.events);
|
|
887
|
+
|
|
888
|
+
// Store middleware for later use in _createAuthMiddleware
|
|
889
|
+
this.oidcMiddleware = oidcHandler.middleware;
|
|
890
|
+
|
|
891
|
+
if (this.options.verbose) {
|
|
892
|
+
console.log('[API Plugin] Mounted OIDC routes:');
|
|
893
|
+
for (const [path, description] of Object.entries(oidcHandler.routes)) {
|
|
894
|
+
console.log(`[API Plugin] ${path} - ${description}`);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
/**
|
|
900
|
+
* Create authentication middleware based on configured drivers
|
|
901
|
+
* @private
|
|
902
|
+
* @returns {Function|null} Hono middleware or null
|
|
368
903
|
*/
|
|
369
904
|
_createAuthMiddleware() {
|
|
370
905
|
const { database, auth } = this.options;
|
|
371
|
-
const {
|
|
906
|
+
const { drivers, resource: defaultResourceName, pathAuth, pathRules } = auth;
|
|
372
907
|
|
|
373
|
-
|
|
908
|
+
// If no drivers configured, no auth
|
|
909
|
+
if (!drivers || drivers.length === 0) {
|
|
374
910
|
return null;
|
|
375
911
|
}
|
|
376
912
|
|
|
377
|
-
|
|
913
|
+
// Get auth resource
|
|
914
|
+
const authResource = database.resources[defaultResourceName];
|
|
378
915
|
if (!authResource) {
|
|
379
|
-
console.error(`[API Plugin] Auth resource '${
|
|
916
|
+
console.error(`[API Plugin] Auth resource '${defaultResourceName}' not found for middleware`);
|
|
380
917
|
return null;
|
|
381
918
|
}
|
|
382
919
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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
|
+
if (pathAuth) {
|
|
927
|
+
try {
|
|
928
|
+
validatePathAuth(pathAuth);
|
|
929
|
+
} catch (err) {
|
|
930
|
+
console.error(`[API Plugin] Invalid pathAuth configuration: ${err.message}`);
|
|
931
|
+
throw err;
|
|
388
932
|
}
|
|
933
|
+
}
|
|
389
934
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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
|
+
};
|
|
396
1042
|
}
|
|
397
1043
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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
|
+
}
|
|
406
1084
|
}
|
|
407
1085
|
|
|
408
|
-
|
|
409
|
-
return
|
|
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
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
|
|
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(', ')}`);
|
|
1175
|
+
}
|
|
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
|
+
});
|
|
410
1200
|
}
|
|
411
1201
|
|
|
412
1202
|
/**
|
|
@@ -503,6 +1293,105 @@ export class ApiServer {
|
|
|
503
1293
|
}
|
|
504
1294
|
}
|
|
505
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
|
+
|
|
506
1395
|
/**
|
|
507
1396
|
* Start the server
|
|
508
1397
|
* @returns {Promise<void>}
|
|
@@ -519,15 +1408,22 @@ export class ApiServer {
|
|
|
519
1408
|
const { Hono } = await import('hono');
|
|
520
1409
|
const { serve } = await import('@hono/node-server');
|
|
521
1410
|
const { swaggerUI } = await import('@hono/swagger-ui');
|
|
1411
|
+
const { cors } = await import('hono/cors');
|
|
522
1412
|
|
|
523
1413
|
// Store for use in _setupRoutes
|
|
524
1414
|
this.Hono = Hono;
|
|
525
1415
|
this.serve = serve;
|
|
526
1416
|
this.swaggerUI = swaggerUI;
|
|
1417
|
+
this.cors = cors;
|
|
527
1418
|
|
|
528
1419
|
// Initialize app
|
|
529
1420
|
this.app = new Hono();
|
|
530
1421
|
|
|
1422
|
+
// Initialize failban manager if enabled
|
|
1423
|
+
if (this.failban) {
|
|
1424
|
+
await this.failban.initialize();
|
|
1425
|
+
}
|
|
1426
|
+
|
|
531
1427
|
// Setup all routes
|
|
532
1428
|
this._setupRoutes();
|
|
533
1429
|
|
|
@@ -545,6 +1441,22 @@ export class ApiServer {
|
|
|
545
1441
|
}, (info) => {
|
|
546
1442
|
this.isRunning = true;
|
|
547
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
|
+
|
|
548
1460
|
resolve();
|
|
549
1461
|
});
|
|
550
1462
|
} catch (err) {
|
|
@@ -576,6 +1488,16 @@ export class ApiServer {
|
|
|
576
1488
|
this.isRunning = false;
|
|
577
1489
|
console.log('[API Plugin] Server stopped');
|
|
578
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
|
+
}
|
|
579
1501
|
}
|
|
580
1502
|
|
|
581
1503
|
/**
|
|
@@ -603,9 +1525,9 @@ export class ApiServer {
|
|
|
603
1525
|
* Generate OpenAPI specification
|
|
604
1526
|
* @private
|
|
605
1527
|
* @returns {Object} OpenAPI spec
|
|
606
|
-
|
|
1528
|
+
*/
|
|
607
1529
|
_generateOpenAPISpec() {
|
|
608
|
-
const { port, host, database, resources, auth, apiInfo } = this.options;
|
|
1530
|
+
const { port, host, database, resources, auth, apiInfo, versionPrefix } = this.options;
|
|
609
1531
|
|
|
610
1532
|
return generateOpenAPISpec(database, {
|
|
611
1533
|
title: apiInfo.title,
|
|
@@ -613,7 +1535,8 @@ export class ApiServer {
|
|
|
613
1535
|
description: apiInfo.description,
|
|
614
1536
|
serverUrl: `http://${host === '0.0.0.0' ? 'localhost' : host}:${port}`,
|
|
615
1537
|
auth,
|
|
616
|
-
resources
|
|
1538
|
+
resources,
|
|
1539
|
+
versionPrefix
|
|
617
1540
|
});
|
|
618
1541
|
}
|
|
619
1542
|
}
|