s3db.js 13.5.1 → 13.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/README.md +25 -10
  2. package/dist/{s3db.cjs.js → s3db.cjs} +30323 -24958
  3. package/dist/s3db.cjs.map +1 -0
  4. package/dist/s3db.es.js +24026 -18654
  5. package/dist/s3db.es.js.map +1 -1
  6. package/package.json +216 -20
  7. package/src/concerns/id.js +90 -6
  8. package/src/concerns/index.js +2 -1
  9. package/src/concerns/password-hashing.js +150 -0
  10. package/src/database.class.js +4 -0
  11. package/src/plugins/api/auth/basic-auth.js +23 -1
  12. package/src/plugins/api/auth/index.js +49 -3
  13. package/src/plugins/api/auth/oauth2-auth.js +171 -0
  14. package/src/plugins/api/auth/oidc-auth.js +789 -0
  15. package/src/plugins/api/auth/oidc-client.js +462 -0
  16. package/src/plugins/api/auth/path-auth-matcher.js +284 -0
  17. package/src/plugins/api/concerns/event-emitter.js +134 -0
  18. package/src/plugins/api/concerns/failban-manager.js +651 -0
  19. package/src/plugins/api/concerns/guards-helpers.js +402 -0
  20. package/src/plugins/api/concerns/metrics-collector.js +346 -0
  21. package/src/plugins/api/index.js +503 -54
  22. package/src/plugins/api/middlewares/failban.js +305 -0
  23. package/src/plugins/api/middlewares/rate-limit.js +301 -0
  24. package/src/plugins/api/middlewares/request-id.js +74 -0
  25. package/src/plugins/api/middlewares/security-headers.js +120 -0
  26. package/src/plugins/api/middlewares/session-tracking.js +194 -0
  27. package/src/plugins/api/routes/auth-routes.js +23 -3
  28. package/src/plugins/api/routes/resource-routes.js +71 -29
  29. package/src/plugins/api/server.js +1017 -94
  30. package/src/plugins/api/utils/guards.js +213 -0
  31. package/src/plugins/api/utils/mime-types.js +154 -0
  32. package/src/plugins/api/utils/openapi-generator.js +44 -11
  33. package/src/plugins/api/utils/path-matcher.js +173 -0
  34. package/src/plugins/api/utils/static-filesystem.js +262 -0
  35. package/src/plugins/api/utils/static-s3.js +231 -0
  36. package/src/plugins/api/utils/template-engine.js +188 -0
  37. package/src/plugins/cloud-inventory/drivers/alibaba-driver.js +853 -0
  38. package/src/plugins/cloud-inventory/drivers/aws-driver.js +2554 -0
  39. package/src/plugins/cloud-inventory/drivers/azure-driver.js +637 -0
  40. package/src/plugins/cloud-inventory/drivers/base-driver.js +99 -0
  41. package/src/plugins/cloud-inventory/drivers/cloudflare-driver.js +620 -0
  42. package/src/plugins/cloud-inventory/drivers/digitalocean-driver.js +698 -0
  43. package/src/plugins/cloud-inventory/drivers/gcp-driver.js +645 -0
  44. package/src/plugins/cloud-inventory/drivers/hetzner-driver.js +559 -0
  45. package/src/plugins/cloud-inventory/drivers/linode-driver.js +614 -0
  46. package/src/plugins/cloud-inventory/drivers/mock-drivers.js +449 -0
  47. package/src/plugins/cloud-inventory/drivers/mongodb-atlas-driver.js +771 -0
  48. package/src/plugins/cloud-inventory/drivers/oracle-driver.js +768 -0
  49. package/src/plugins/cloud-inventory/drivers/vultr-driver.js +636 -0
  50. package/src/plugins/cloud-inventory/index.js +20 -0
  51. package/src/plugins/cloud-inventory/registry.js +146 -0
  52. package/src/plugins/cloud-inventory/terraform-exporter.js +362 -0
  53. package/src/plugins/cloud-inventory.plugin.js +1333 -0
  54. package/src/plugins/concerns/plugin-dependencies.js +61 -1
  55. package/src/plugins/eventual-consistency/analytics.js +1 -0
  56. package/src/plugins/identity/README.md +335 -0
  57. package/src/plugins/identity/concerns/mfa-manager.js +204 -0
  58. package/src/plugins/identity/concerns/password.js +138 -0
  59. package/src/plugins/identity/concerns/resource-schemas.js +273 -0
  60. package/src/plugins/identity/concerns/token-generator.js +172 -0
  61. package/src/plugins/identity/email-service.js +422 -0
  62. package/src/plugins/identity/index.js +1052 -0
  63. package/src/plugins/identity/oauth2-server.js +1033 -0
  64. package/src/plugins/identity/oidc-discovery.js +285 -0
  65. package/src/plugins/identity/rsa-keys.js +323 -0
  66. package/src/plugins/identity/server.js +500 -0
  67. package/src/plugins/identity/session-manager.js +453 -0
  68. package/src/plugins/identity/ui/layouts/base.js +251 -0
  69. package/src/plugins/identity/ui/middleware.js +135 -0
  70. package/src/plugins/identity/ui/pages/admin/client-form.js +247 -0
  71. package/src/plugins/identity/ui/pages/admin/clients.js +179 -0
  72. package/src/plugins/identity/ui/pages/admin/dashboard.js +181 -0
  73. package/src/plugins/identity/ui/pages/admin/user-form.js +283 -0
  74. package/src/plugins/identity/ui/pages/admin/users.js +263 -0
  75. package/src/plugins/identity/ui/pages/consent.js +262 -0
  76. package/src/plugins/identity/ui/pages/forgot-password.js +104 -0
  77. package/src/plugins/identity/ui/pages/login.js +144 -0
  78. package/src/plugins/identity/ui/pages/mfa-backup-codes.js +180 -0
  79. package/src/plugins/identity/ui/pages/mfa-enrollment.js +187 -0
  80. package/src/plugins/identity/ui/pages/mfa-verification.js +178 -0
  81. package/src/plugins/identity/ui/pages/oauth-error.js +225 -0
  82. package/src/plugins/identity/ui/pages/profile.js +361 -0
  83. package/src/plugins/identity/ui/pages/register.js +226 -0
  84. package/src/plugins/identity/ui/pages/reset-password.js +128 -0
  85. package/src/plugins/identity/ui/pages/verify-email.js +172 -0
  86. package/src/plugins/identity/ui/routes.js +2541 -0
  87. package/src/plugins/identity/ui/styles/main.css +465 -0
  88. package/src/plugins/index.js +4 -1
  89. package/src/plugins/ml/base-model.class.js +32 -7
  90. package/src/plugins/ml/classification-model.class.js +1 -1
  91. package/src/plugins/ml/timeseries-model.class.js +3 -1
  92. package/src/plugins/ml.plugin.js +124 -32
  93. package/src/plugins/shared/error-handler.js +147 -0
  94. package/src/plugins/shared/index.js +9 -0
  95. package/src/plugins/shared/middlewares/compression.js +117 -0
  96. package/src/plugins/shared/middlewares/cors.js +49 -0
  97. package/src/plugins/shared/middlewares/index.js +11 -0
  98. package/src/plugins/shared/middlewares/logging.js +54 -0
  99. package/src/plugins/shared/middlewares/rate-limit.js +73 -0
  100. package/src/plugins/shared/middlewares/security.js +158 -0
  101. package/src/plugins/shared/response-formatter.js +264 -0
  102. package/src/resource.class.js +140 -12
  103. package/src/schema.class.js +30 -1
  104. package/src/validator.class.js +57 -6
  105. package/dist/s3db.cjs.js.map +0 -1
@@ -36,6 +36,86 @@ import { Plugin } from '../plugin.class.js';
36
36
  import { requirePluginDependency } from '../concerns/plugin-dependencies.js';
37
37
  import tryFn from '../../concerns/try-fn.js';
38
38
  import { ApiServer } from './server.js';
39
+ import { idGenerator } from '../../concerns/id.js';
40
+
41
+ const AUTH_DRIVER_KEYS = ['jwt', 'apiKey', 'basic', 'oidc', 'oauth2'];
42
+
43
+ function normalizeAuthConfig(authOptions = {}) {
44
+ if (!authOptions) {
45
+ return {
46
+ drivers: [],
47
+ pathRules: [],
48
+ pathAuth: undefined,
49
+ strategy: 'any',
50
+ priorities: {},
51
+ resource: 'users',
52
+ usernameField: 'email',
53
+ passwordField: 'password',
54
+ driver: null
55
+ };
56
+ }
57
+
58
+ const normalized = {
59
+ drivers: [],
60
+ pathRules: Array.isArray(authOptions.pathRules) ? authOptions.pathRules : [],
61
+ pathAuth: authOptions.pathAuth,
62
+ strategy: authOptions.strategy || 'any',
63
+ priorities: authOptions.priorities || {},
64
+ resource: authOptions.resource || 'users',
65
+ usernameField: authOptions.usernameField || 'email',
66
+ passwordField: authOptions.passwordField || 'password'
67
+ };
68
+
69
+ const seen = new Set();
70
+
71
+ const addDriver = (name, driverConfig = {}) => {
72
+ if (!name) return;
73
+ const driverName = String(name).trim();
74
+ if (!driverName || seen.has(driverName)) return;
75
+ seen.add(driverName);
76
+ normalized.drivers.push({
77
+ driver: driverName,
78
+ config: driverConfig || {}
79
+ });
80
+ };
81
+
82
+ // Drivers provided as array
83
+ if (Array.isArray(authOptions.drivers)) {
84
+ for (const entry of authOptions.drivers) {
85
+ if (typeof entry === 'string') {
86
+ addDriver(entry, {});
87
+ } else if (entry && typeof entry === 'object') {
88
+ addDriver(entry.driver, entry.config || {});
89
+ }
90
+ }
91
+ }
92
+
93
+ // Single driver shortcut
94
+ if (authOptions.driver) {
95
+ if (typeof authOptions.driver === 'string') {
96
+ addDriver(authOptions.driver, authOptions.config || {});
97
+ } else if (typeof authOptions.driver === 'object') {
98
+ addDriver(authOptions.driver.driver, authOptions.driver.config || authOptions.config || {});
99
+ }
100
+ }
101
+
102
+ // Support legacy per-driver objects (jwt: {...}, apiKey: {...})
103
+ for (const driverName of AUTH_DRIVER_KEYS) {
104
+ if (authOptions[driverName] === undefined) continue;
105
+
106
+ const value = authOptions[driverName];
107
+ if (!value || value.enabled === false) continue;
108
+
109
+ const config = typeof value === 'object' ? { ...value } : {};
110
+ if (config.enabled !== undefined) {
111
+ delete config.enabled;
112
+ }
113
+ addDriver(driverName, config);
114
+ }
115
+
116
+ normalized.driver = normalized.drivers.length > 0 ? normalized.drivers[0].driver : null;
117
+ return normalized;
118
+ }
39
119
 
40
120
  /**
41
121
  * API Plugin class
@@ -50,6 +130,8 @@ export class ApiPlugin extends Plugin {
50
130
  constructor(options = {}) {
51
131
  super(options);
52
132
 
133
+ const normalizedAuth = normalizeAuthConfig(options.auth);
134
+
53
135
  this.config = {
54
136
  // Server configuration
55
137
  port: options.port || 3000,
@@ -68,27 +150,22 @@ export class ApiPlugin extends Plugin {
68
150
  description: options.docs?.description || options.apiDescription || 'Auto-generated REST API for s3db.js resources'
69
151
  },
70
152
 
71
- // Authentication configuration (driver-based)
72
- auth: options.auth ? {
73
- driver: options.auth.driver || null, // 'jwt' or 'basic'
74
- resource: options.auth.resource || 'users', // Resource that manages auth
75
- usernameField: options.auth.usernameField || 'email', // Default: email
76
- passwordField: options.auth.passwordField || 'password', // Default: password
77
- config: options.auth.config || {} // Driver-specific config
78
- } : {
79
- driver: null,
80
- resource: 'users',
81
- usernameField: 'email',
82
- passwordField: 'password',
83
- config: {}
84
- },
85
-
86
- // Resource configuration
87
- resources: options.resources || {},
153
+ // Authentication configuration (multiple drivers)
154
+ auth: normalizedAuth,
88
155
 
89
156
  // Custom routes (plugin-level)
90
157
  routes: options.routes || {},
91
158
 
159
+ // Template engine configuration
160
+ templates: {
161
+ enabled: options.templates?.enabled || false,
162
+ engine: options.templates?.engine || 'jsx', // 'jsx' (default), 'ejs', 'custom'
163
+ templatesDir: options.templates?.templatesDir || './views',
164
+ layout: options.templates?.layout || null,
165
+ engineOptions: options.templates?.engineOptions || {},
166
+ customRenderer: options.templates?.customRenderer || null
167
+ },
168
+
92
169
  // CORS configuration
93
170
  cors: {
94
171
  enabled: options.cors?.enabled || false,
@@ -130,19 +207,83 @@ export class ApiPlugin extends Plugin {
130
207
  returnValidationErrors: options.validation?.returnValidationErrors !== false
131
208
  },
132
209
 
133
- // Content Security Policy (CSP) configuration
210
+ // Security Headers (Helmet-like configuration)
211
+ security: {
212
+ enabled: options.security?.enabled !== false, // Enabled by default
213
+
214
+ // Content Security Policy (CSP)
215
+ contentSecurityPolicy: options.security?.contentSecurityPolicy !== false ? {
216
+ enabled: options.security?.contentSecurityPolicy?.enabled !== false,
217
+ directives: options.security?.contentSecurityPolicy?.directives || options.csp?.directives || {
218
+ 'default-src': ["'self'"],
219
+ 'script-src': ["'self'", "'unsafe-inline'", 'https://cdn.redoc.ly/redoc/v2.5.1/'],
220
+ 'style-src': ["'self'", "'unsafe-inline'", 'https://cdn.redoc.ly/redoc/v2.5.1/', 'https://fonts.googleapis.com'],
221
+ 'font-src': ["'self'", 'https://fonts.gstatic.com'],
222
+ 'img-src': ["'self'", 'data:', 'https:'],
223
+ 'connect-src': ["'self'"]
224
+ },
225
+ reportOnly: options.security?.contentSecurityPolicy?.reportOnly || options.csp?.reportOnly || false,
226
+ reportUri: options.security?.contentSecurityPolicy?.reportUri || options.csp?.reportUri || null
227
+ } : false,
228
+
229
+ // X-Frame-Options (clickjacking protection)
230
+ frameguard: options.security?.frameguard !== false ? {
231
+ action: options.security?.frameguard?.action || 'deny' // 'deny' or 'sameorigin'
232
+ } : false,
233
+
234
+ // X-Content-Type-Options (MIME sniffing protection)
235
+ noSniff: options.security?.noSniff !== false, // Enabled by default
236
+
237
+ // Strict-Transport-Security (HSTS - force HTTPS)
238
+ hsts: options.security?.hsts !== false ? {
239
+ maxAge: options.security?.hsts?.maxAge || 15552000, // 180 days (Helmet default)
240
+ includeSubDomains: options.security?.hsts?.includeSubDomains !== false,
241
+ preload: options.security?.hsts?.preload || false
242
+ } : false,
243
+
244
+ // Referrer-Policy (privacy)
245
+ referrerPolicy: options.security?.referrerPolicy !== false ? {
246
+ policy: options.security?.referrerPolicy?.policy || 'no-referrer'
247
+ } : false,
248
+
249
+ // X-DNS-Prefetch-Control (DNS leak protection)
250
+ dnsPrefetchControl: options.security?.dnsPrefetchControl !== false ? {
251
+ allow: options.security?.dnsPrefetchControl?.allow || false
252
+ } : false,
253
+
254
+ // X-Download-Options (IE8+ download security)
255
+ ieNoOpen: options.security?.ieNoOpen !== false, // Enabled by default
256
+
257
+ // X-Permitted-Cross-Domain-Policies (Flash/PDF security)
258
+ permittedCrossDomainPolicies: options.security?.permittedCrossDomainPolicies !== false ? {
259
+ policy: options.security?.permittedCrossDomainPolicies?.policy || 'none'
260
+ } : false,
261
+
262
+ // X-XSS-Protection (legacy XSS filter)
263
+ xssFilter: options.security?.xssFilter !== false ? {
264
+ mode: options.security?.xssFilter?.mode || 'block'
265
+ } : false,
266
+
267
+ // Permissions-Policy (modern feature policy)
268
+ permissionsPolicy: options.security?.permissionsPolicy !== false ? {
269
+ features: options.security?.permissionsPolicy?.features || {
270
+ geolocation: [],
271
+ microphone: [],
272
+ camera: [],
273
+ payment: [],
274
+ usb: [],
275
+ magnetometer: [],
276
+ gyroscope: [],
277
+ accelerometer: []
278
+ }
279
+ } : false
280
+ },
281
+
282
+ // Legacy CSP config (backward compatibility)
134
283
  csp: {
135
284
  enabled: options.csp?.enabled || false,
136
- // Default CSP that works with Redoc v2.5.1 (allows CDN scripts/styles)
137
- directives: options.csp?.directives || {
138
- 'default-src': ["'self'"],
139
- 'script-src': ["'self'", "'unsafe-inline'", 'https://cdn.redoc.ly/redoc/v2.5.1/'],
140
- 'style-src': ["'self'", "'unsafe-inline'", 'https://cdn.redoc.ly/redoc/v2.5.1/', 'https://fonts.googleapis.com'],
141
- 'font-src': ["'self'", 'https://fonts.gstatic.com'],
142
- 'img-src': ["'self'", 'data:', 'https:'],
143
- 'connect-src': ["'self'"]
144
- },
145
- reportOnly: options.csp?.reportOnly || false, // If true, uses Content-Security-Policy-Report-Only
285
+ directives: options.csp?.directives || {},
286
+ reportOnly: options.csp?.reportOnly || false,
146
287
  reportUri: options.csp?.reportUri || null
147
288
  },
148
289
 
@@ -150,10 +291,78 @@ export class ApiPlugin extends Plugin {
150
291
  middlewares: options.middlewares || []
151
292
  };
152
293
 
294
+ this.config.resources = this._normalizeResourcesConfig(options.resources);
295
+
153
296
  this.server = null;
154
297
  this.usersResource = null;
155
298
  }
156
299
 
300
+ /**
301
+ * Normalize resources config so array/string inputs become object map
302
+ * @private
303
+ * @param {Object|Array<string|Object>} resources
304
+ * @returns {Object<string, Object>}
305
+ */
306
+ _normalizeResourcesConfig(resources) {
307
+ if (!resources) {
308
+ return {};
309
+ }
310
+
311
+ const normalized = {};
312
+ const verbose = this.options?.verbose;
313
+
314
+ const addResourceConfig = (name, config = {}) => {
315
+ if (typeof name !== 'string' || !name.trim()) {
316
+ if (verbose) {
317
+ console.warn('[API Plugin] Ignoring resource config with invalid name:', name);
318
+ }
319
+ return;
320
+ }
321
+
322
+ normalized[name] = { ...config };
323
+ };
324
+
325
+ if (Array.isArray(resources)) {
326
+ for (const entry of resources) {
327
+ if (typeof entry === 'string') {
328
+ addResourceConfig(entry);
329
+ } else if (entry && typeof entry === 'object' && typeof entry.name === 'string') {
330
+ const { name, ...config } = entry;
331
+ addResourceConfig(name, config);
332
+ } else {
333
+ if (verbose) {
334
+ console.warn('[API Plugin] Ignoring invalid resource config entry (expected string or object with name):', entry);
335
+ }
336
+ }
337
+ }
338
+ return normalized;
339
+ }
340
+
341
+ if (typeof resources === 'object') {
342
+ for (const [name, config] of Object.entries(resources)) {
343
+ if (config === false) {
344
+ addResourceConfig(name, { enabled: false });
345
+ } else if (config === true || config === undefined || config === null) {
346
+ addResourceConfig(name);
347
+ } else if (typeof config === 'object') {
348
+ addResourceConfig(name, config);
349
+ } else {
350
+ if (verbose) {
351
+ console.warn('[API Plugin] Coercing resource config to empty object for', name);
352
+ }
353
+ addResourceConfig(name);
354
+ }
355
+ }
356
+ return normalized;
357
+ }
358
+
359
+ if (verbose) {
360
+ console.warn('[API Plugin] Invalid resources configuration. Expected object or array, received:', typeof resources);
361
+ }
362
+
363
+ return {};
364
+ }
365
+
157
366
  /**
158
367
  * Validate plugin dependencies
159
368
  * @private
@@ -181,8 +390,8 @@ export class ApiPlugin extends Plugin {
181
390
  throw err;
182
391
  }
183
392
 
184
- // Create users resource if authentication driver is configured
185
- const authEnabled = this.config.auth.driver !== null;
393
+ // Create users resource if authentication drivers are configured
394
+ const authEnabled = this.config.auth.drivers.length > 0;
186
395
 
187
396
  if (authEnabled) {
188
397
  await this._createUsersResource();
@@ -207,11 +416,12 @@ export class ApiPlugin extends Plugin {
207
416
  attributes: {
208
417
  id: 'string|required',
209
418
  username: 'string|required|minlength:3',
210
- email: 'string|optional|email',
419
+ email: 'string|required|email', // Required to support email-based auth
211
420
  password: 'secret|required|minlength:8',
212
421
  apiKey: 'string|optional',
213
422
  jwtSecret: 'string|optional',
214
423
  role: 'string|default:user',
424
+ scopes: 'array|items:string|optional', // Authorization scopes (e.g., ['read:users', 'write:cars'])
215
425
  active: 'boolean|default:true',
216
426
  createdAt: 'string|optional',
217
427
  lastLoginAt: 'string|optional',
@@ -238,7 +448,6 @@ export class ApiPlugin extends Plugin {
238
448
  throw err;
239
449
  }
240
450
  }
241
-
242
451
  /**
243
452
  * Setup middlewares
244
453
  * @private
@@ -248,19 +457,26 @@ export class ApiPlugin extends Plugin {
248
457
 
249
458
  // Add request ID middleware
250
459
  middlewares.push(async (c, next) => {
251
- c.set('requestId', crypto.randomUUID());
460
+ c.set('requestId', idGenerator());
252
461
  c.set('verbose', this.config.verbose);
253
462
  await next();
254
463
  });
255
464
 
465
+ // Add security headers middleware (FIRST - most critical)
466
+ if (this.config.security.enabled) {
467
+ const securityMiddleware = await this._createSecurityMiddleware();
468
+ middlewares.push(securityMiddleware);
469
+ }
470
+
256
471
  // Add CORS middleware
257
472
  if (this.config.cors.enabled) {
258
473
  const corsMiddleware = await this._createCorsMiddleware();
259
474
  middlewares.push(corsMiddleware);
260
475
  }
261
476
 
262
- // Add CSP middleware
263
- if (this.config.csp.enabled) {
477
+ // Add legacy CSP middleware (deprecated - use security.contentSecurityPolicy instead)
478
+ // This is kept for backward compatibility with old configs
479
+ if (this.config.csp.enabled && !this.config.security.contentSecurityPolicy) {
264
480
  const cspMiddleware = await this._createCSPMiddleware();
265
481
  middlewares.push(cspMiddleware);
266
482
  }
@@ -291,8 +507,11 @@ export class ApiPlugin extends Plugin {
291
507
  }
292
508
 
293
509
  /**
294
- * Create CORS middleware (placeholder)
510
+ * Create CORS middleware
295
511
  * @private
512
+ *
513
+ * Handles Cross-Origin Resource Sharing (CORS) headers and preflight requests.
514
+ * Supports wildcard origins, credential-based requests, and OPTIONS preflight.
296
515
  */
297
516
  async _createCorsMiddleware() {
298
517
  return async (c, next) => {
@@ -356,8 +575,12 @@ export class ApiPlugin extends Plugin {
356
575
  }
357
576
 
358
577
  /**
359
- * Create rate limiting middleware (placeholder)
578
+ * Create rate limiting middleware
360
579
  * @private
580
+ *
581
+ * Implements sliding window rate limiting with configurable window size and max requests.
582
+ * Returns 429 status code with Retry-After header when limit is exceeded.
583
+ * Uses IP address or custom key generator to track request counts.
361
584
  */
362
585
  async _createRateLimitMiddleware() {
363
586
  const requests = new Map();
@@ -413,10 +636,23 @@ export class ApiPlugin extends Plugin {
413
636
  }
414
637
 
415
638
  /**
416
- * Create logging middleware (placeholder)
639
+ * Create logging middleware with customizable format
417
640
  * @private
641
+ *
642
+ * Supported tokens:
643
+ * - :method - HTTP method (GET, POST, etc)
644
+ * - :path - Request path
645
+ * - :status - HTTP status code
646
+ * - :response-time - Response time in milliseconds
647
+ * - :user - Username or 'anonymous'
648
+ * - :requestId - Request ID (UUID)
649
+ *
650
+ * Example format: ':method :path :status :response-time ms - :user'
651
+ * Output: 'GET /api/v1/cars 200 45ms - john'
418
652
  */
419
653
  async _createLoggingMiddleware() {
654
+ const { format } = this.config.logging;
655
+
420
656
  return async (c, next) => {
421
657
  const start = Date.now();
422
658
  const method = c.req.method;
@@ -427,32 +663,236 @@ export class ApiPlugin extends Plugin {
427
663
 
428
664
  const duration = Date.now() - start;
429
665
  const status = c.res.status;
430
- const user = c.get('user')?.username || 'anonymous';
431
-
432
- console.log(`[API Plugin] ${requestId} - ${method} ${path} ${status} ${duration}ms - ${user}`);
666
+ const user = c.get('user')?.username || c.get('user')?.email || 'anonymous';
667
+
668
+ // Parse format string with token replacement
669
+ let logMessage = format
670
+ .replace(':method', method)
671
+ .replace(':path', path)
672
+ .replace(':status', status)
673
+ .replace(':response-time', duration)
674
+ .replace(':user', user)
675
+ .replace(':requestId', requestId);
676
+
677
+ console.log(`[API Plugin] ${logMessage}`);
433
678
  };
434
679
  }
435
680
 
436
681
  /**
437
- * Create compression middleware (placeholder)
682
+ * Create compression middleware (using Node.js zlib)
438
683
  * @private
439
684
  */
440
685
  async _createCompressionMiddleware() {
686
+ const { gzip, brotliCompress } = await import('zlib');
687
+ const { promisify } = await import('util');
688
+
689
+ const gzipAsync = promisify(gzip);
690
+ const brotliAsync = promisify(brotliCompress);
691
+
692
+ const { threshold, level } = this.config.compression;
693
+
694
+ // Content types that should NOT be compressed (already compressed)
695
+ const skipContentTypes = [
696
+ 'image/', 'video/', 'audio/',
697
+ 'application/zip', 'application/gzip',
698
+ 'application/x-gzip', 'application/x-bzip2'
699
+ ];
700
+
441
701
  return async (c, next) => {
442
702
  await next();
443
703
 
444
- // TODO: Implement actual compression using zlib
445
- // For now, this is a no-op placeholder to avoid ERR_CONTENT_DECODING_FAILED errors
446
- //
447
- // WARNING: Do NOT set Content-Encoding headers without actually compressing!
448
- // Setting these headers without compression causes browsers to fail with:
449
- // net::ERR_CONTENT_DECODING_FAILED 200 (OK)
450
- //
451
- // Real implementation would require:
452
- // 1. Check Accept-Encoding header
453
- // 2. Compress response body with zlib.gzip() or zlib.deflate()
454
- // 3. Set Content-Encoding header
455
- // 4. Update Content-Length header
704
+ // Skip if response has no body
705
+ if (!c.res || !c.res.body) {
706
+ return;
707
+ }
708
+
709
+ // Skip if already compressed
710
+ if (c.res.headers.has('content-encoding')) {
711
+ return;
712
+ }
713
+
714
+ // Skip if content-type should not be compressed
715
+ const contentType = c.res.headers.get('content-type') || '';
716
+ if (skipContentTypes.some(type => contentType.startsWith(type))) {
717
+ return;
718
+ }
719
+
720
+ // Check Accept-Encoding header
721
+ const acceptEncoding = c.req.header('accept-encoding') || '';
722
+ const supportsBrotli = acceptEncoding.includes('br');
723
+ const supportsGzip = acceptEncoding.includes('gzip');
724
+
725
+ if (!supportsBrotli && !supportsGzip) {
726
+ return; // Client doesn't support compression
727
+ }
728
+
729
+ // Get response body as buffer
730
+ let body;
731
+ try {
732
+ const text = await c.res.text();
733
+ body = Buffer.from(text, 'utf-8');
734
+ } catch (err) {
735
+ // If body is already consumed or not text, skip compression
736
+ return;
737
+ }
738
+
739
+ // Skip if body is too small
740
+ if (body.length < threshold) {
741
+ return;
742
+ }
743
+
744
+ // Compress with brotli (better) or gzip (fallback)
745
+ let compressed;
746
+ let encoding;
747
+
748
+ try {
749
+ if (supportsBrotli) {
750
+ compressed = await brotliAsync(body);
751
+ encoding = 'br';
752
+ } else {
753
+ compressed = await gzipAsync(body, { level });
754
+ encoding = 'gzip';
755
+ }
756
+
757
+ // Only use compressed if it's actually smaller
758
+ if (compressed.length >= body.length) {
759
+ return; // Compression didn't help, use original
760
+ }
761
+
762
+ // Create new response with compressed body
763
+ const headers = new Headers(c.res.headers);
764
+ headers.set('Content-Encoding', encoding);
765
+ headers.set('Content-Length', compressed.length.toString());
766
+ headers.set('Vary', 'Accept-Encoding');
767
+
768
+ // Replace response
769
+ c.res = new Response(compressed, {
770
+ status: c.res.status,
771
+ statusText: c.res.statusText,
772
+ headers
773
+ });
774
+
775
+ } catch (err) {
776
+ // Compression failed, log and continue with uncompressed response
777
+ if (this.config.verbose) {
778
+ console.error('[API Plugin] Compression error:', err.message);
779
+ }
780
+ }
781
+ };
782
+ }
783
+
784
+ /**
785
+ * Create security headers middleware (Helmet-like)
786
+ * @private
787
+ */
788
+ async _createSecurityMiddleware() {
789
+ const { security } = this.config;
790
+
791
+ return async (c, next) => {
792
+ // X-Content-Type-Options: nosniff (MIME sniffing protection)
793
+ if (security.noSniff) {
794
+ c.header('X-Content-Type-Options', 'nosniff');
795
+ }
796
+
797
+ // X-Frame-Options (clickjacking protection)
798
+ if (security.frameguard) {
799
+ const action = security.frameguard.action.toUpperCase();
800
+ if (action === 'DENY') {
801
+ c.header('X-Frame-Options', 'DENY');
802
+ } else if (action === 'SAMEORIGIN') {
803
+ c.header('X-Frame-Options', 'SAMEORIGIN');
804
+ }
805
+ }
806
+
807
+ // Strict-Transport-Security (HSTS - force HTTPS)
808
+ if (security.hsts) {
809
+ const parts = [`max-age=${security.hsts.maxAge}`];
810
+ if (security.hsts.includeSubDomains) {
811
+ parts.push('includeSubDomains');
812
+ }
813
+ if (security.hsts.preload) {
814
+ parts.push('preload');
815
+ }
816
+ c.header('Strict-Transport-Security', parts.join('; '));
817
+ }
818
+
819
+ // Referrer-Policy (privacy)
820
+ if (security.referrerPolicy) {
821
+ c.header('Referrer-Policy', security.referrerPolicy.policy);
822
+ }
823
+
824
+ // X-DNS-Prefetch-Control (DNS leak protection)
825
+ if (security.dnsPrefetchControl) {
826
+ const value = security.dnsPrefetchControl.allow ? 'on' : 'off';
827
+ c.header('X-DNS-Prefetch-Control', value);
828
+ }
829
+
830
+ // X-Download-Options (IE8+ download security)
831
+ if (security.ieNoOpen) {
832
+ c.header('X-Download-Options', 'noopen');
833
+ }
834
+
835
+ // X-Permitted-Cross-Domain-Policies (Flash/PDF security)
836
+ if (security.permittedCrossDomainPolicies) {
837
+ c.header('X-Permitted-Cross-Domain-Policies', security.permittedCrossDomainPolicies.policy);
838
+ }
839
+
840
+ // X-XSS-Protection (legacy XSS filter)
841
+ if (security.xssFilter) {
842
+ const mode = security.xssFilter.mode;
843
+ c.header('X-XSS-Protection', mode === 'block' ? '1; mode=block' : '0');
844
+ }
845
+
846
+ // Permissions-Policy (modern feature policy)
847
+ if (security.permissionsPolicy && security.permissionsPolicy.features) {
848
+ const features = security.permissionsPolicy.features;
849
+ const policies = [];
850
+
851
+ for (const [feature, allowList] of Object.entries(features)) {
852
+ if (Array.isArray(allowList)) {
853
+ const value = allowList.length === 0
854
+ ? `${feature}=()`
855
+ : `${feature}=(${allowList.join(' ')})`;
856
+ policies.push(value);
857
+ }
858
+ }
859
+
860
+ if (policies.length > 0) {
861
+ c.header('Permissions-Policy', policies.join(', '));
862
+ }
863
+ }
864
+
865
+ // Content-Security-Policy (CSP)
866
+ // Note: This is also handled by _createCSPMiddleware for backward compatibility
867
+ // We check if legacy csp.enabled is true, otherwise use security.contentSecurityPolicy
868
+ const cspConfig = this.config.csp.enabled
869
+ ? this.config.csp
870
+ : security.contentSecurityPolicy;
871
+
872
+ if (cspConfig && cspConfig.enabled !== false && cspConfig.directives) {
873
+ const cspParts = [];
874
+ for (const [directive, values] of Object.entries(cspConfig.directives)) {
875
+ if (Array.isArray(values) && values.length > 0) {
876
+ cspParts.push(`${directive} ${values.join(' ')}`);
877
+ } else if (typeof values === 'string') {
878
+ cspParts.push(`${directive} ${values}`);
879
+ }
880
+ }
881
+
882
+ if (cspConfig.reportUri) {
883
+ cspParts.push(`report-uri ${cspConfig.reportUri}`);
884
+ }
885
+
886
+ if (cspParts.length > 0) {
887
+ const cspValue = cspParts.join('; ');
888
+ const headerName = cspConfig.reportOnly
889
+ ? 'Content-Security-Policy-Report-Only'
890
+ : 'Content-Security-Policy';
891
+ c.header(headerName, cspValue);
892
+ }
893
+ }
894
+
895
+ await next();
456
896
  };
457
897
  }
458
898
 
@@ -469,8 +909,10 @@ export class ApiPlugin extends Plugin {
469
909
  port: this.config.port,
470
910
  host: this.config.host,
471
911
  database: this.database,
912
+ versionPrefix: this.config.versionPrefix,
472
913
  resources: this.config.resources,
473
914
  routes: this.config.routes,
915
+ templates: this.config.templates,
474
916
  middlewares: this.compiledMiddlewares,
475
917
  verbose: this.config.verbose,
476
918
  auth: this.config.auth,
@@ -546,3 +988,10 @@ export class ApiPlugin extends Plugin {
546
988
  return this.server ? this.server.getApp() : null;
547
989
  }
548
990
  }
991
+
992
+ // Export auth utilities (OIDCClient, guards helpers, etc.)
993
+ export { OIDCClient } from './auth/oidc-client.js';
994
+ export * from './concerns/guards-helpers.js';
995
+
996
+ // Export template engine utilities
997
+ export { setupTemplateEngine, ejsEngine, jsxEngine } from './utils/template-engine.js';