s3db.js 13.4.0 → 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 (110) hide show
  1. package/README.md +25 -10
  2. package/dist/{s3db.cjs.js → s3db.cjs} +38801 -32446
  3. package/dist/s3db.cjs.map +1 -0
  4. package/dist/s3db.es.js +38653 -32291
  5. package/dist/s3db.es.js.map +1 -1
  6. package/package.json +218 -22
  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 +6 -2
  11. package/src/plugins/api/auth/basic-auth.js +40 -10
  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 +510 -57
  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 +119 -78
  28. package/src/plugins/api/routes/resource-routes.js +73 -30
  29. package/src/plugins/api/server.js +1139 -45
  30. package/src/plugins/api/utils/custom-routes.js +102 -0
  31. package/src/plugins/api/utils/guards.js +213 -0
  32. package/src/plugins/api/utils/mime-types.js +154 -0
  33. package/src/plugins/api/utils/openapi-generator.js +91 -12
  34. package/src/plugins/api/utils/path-matcher.js +173 -0
  35. package/src/plugins/api/utils/static-filesystem.js +262 -0
  36. package/src/plugins/api/utils/static-s3.js +231 -0
  37. package/src/plugins/api/utils/template-engine.js +188 -0
  38. package/src/plugins/cloud-inventory/drivers/alibaba-driver.js +853 -0
  39. package/src/plugins/cloud-inventory/drivers/aws-driver.js +2554 -0
  40. package/src/plugins/cloud-inventory/drivers/azure-driver.js +637 -0
  41. package/src/plugins/cloud-inventory/drivers/base-driver.js +99 -0
  42. package/src/plugins/cloud-inventory/drivers/cloudflare-driver.js +620 -0
  43. package/src/plugins/cloud-inventory/drivers/digitalocean-driver.js +698 -0
  44. package/src/plugins/cloud-inventory/drivers/gcp-driver.js +645 -0
  45. package/src/plugins/cloud-inventory/drivers/hetzner-driver.js +559 -0
  46. package/src/plugins/cloud-inventory/drivers/linode-driver.js +614 -0
  47. package/src/plugins/cloud-inventory/drivers/mock-drivers.js +449 -0
  48. package/src/plugins/cloud-inventory/drivers/mongodb-atlas-driver.js +771 -0
  49. package/src/plugins/cloud-inventory/drivers/oracle-driver.js +768 -0
  50. package/src/plugins/cloud-inventory/drivers/vultr-driver.js +636 -0
  51. package/src/plugins/cloud-inventory/index.js +20 -0
  52. package/src/plugins/cloud-inventory/registry.js +146 -0
  53. package/src/plugins/cloud-inventory/terraform-exporter.js +362 -0
  54. package/src/plugins/cloud-inventory.plugin.js +1333 -0
  55. package/src/plugins/concerns/plugin-dependencies.js +62 -2
  56. package/src/plugins/eventual-consistency/analytics.js +1 -0
  57. package/src/plugins/eventual-consistency/consolidation.js +2 -2
  58. package/src/plugins/eventual-consistency/garbage-collection.js +2 -2
  59. package/src/plugins/eventual-consistency/install.js +2 -2
  60. package/src/plugins/identity/README.md +335 -0
  61. package/src/plugins/identity/concerns/mfa-manager.js +204 -0
  62. package/src/plugins/identity/concerns/password.js +138 -0
  63. package/src/plugins/identity/concerns/resource-schemas.js +273 -0
  64. package/src/plugins/identity/concerns/token-generator.js +172 -0
  65. package/src/plugins/identity/email-service.js +422 -0
  66. package/src/plugins/identity/index.js +1052 -0
  67. package/src/plugins/identity/oauth2-server.js +1033 -0
  68. package/src/plugins/identity/oidc-discovery.js +285 -0
  69. package/src/plugins/identity/rsa-keys.js +323 -0
  70. package/src/plugins/identity/server.js +500 -0
  71. package/src/plugins/identity/session-manager.js +453 -0
  72. package/src/plugins/identity/ui/layouts/base.js +251 -0
  73. package/src/plugins/identity/ui/middleware.js +135 -0
  74. package/src/plugins/identity/ui/pages/admin/client-form.js +247 -0
  75. package/src/plugins/identity/ui/pages/admin/clients.js +179 -0
  76. package/src/plugins/identity/ui/pages/admin/dashboard.js +181 -0
  77. package/src/plugins/identity/ui/pages/admin/user-form.js +283 -0
  78. package/src/plugins/identity/ui/pages/admin/users.js +263 -0
  79. package/src/plugins/identity/ui/pages/consent.js +262 -0
  80. package/src/plugins/identity/ui/pages/forgot-password.js +104 -0
  81. package/src/plugins/identity/ui/pages/login.js +144 -0
  82. package/src/plugins/identity/ui/pages/mfa-backup-codes.js +180 -0
  83. package/src/plugins/identity/ui/pages/mfa-enrollment.js +187 -0
  84. package/src/plugins/identity/ui/pages/mfa-verification.js +178 -0
  85. package/src/plugins/identity/ui/pages/oauth-error.js +225 -0
  86. package/src/plugins/identity/ui/pages/profile.js +361 -0
  87. package/src/plugins/identity/ui/pages/register.js +226 -0
  88. package/src/plugins/identity/ui/pages/reset-password.js +128 -0
  89. package/src/plugins/identity/ui/pages/verify-email.js +172 -0
  90. package/src/plugins/identity/ui/routes.js +2541 -0
  91. package/src/plugins/identity/ui/styles/main.css +465 -0
  92. package/src/plugins/index.js +4 -1
  93. package/src/plugins/ml/base-model.class.js +65 -16
  94. package/src/plugins/ml/classification-model.class.js +1 -1
  95. package/src/plugins/ml/timeseries-model.class.js +3 -1
  96. package/src/plugins/ml.plugin.js +584 -31
  97. package/src/plugins/shared/error-handler.js +147 -0
  98. package/src/plugins/shared/index.js +9 -0
  99. package/src/plugins/shared/middlewares/compression.js +117 -0
  100. package/src/plugins/shared/middlewares/cors.js +49 -0
  101. package/src/plugins/shared/middlewares/index.js +11 -0
  102. package/src/plugins/shared/middlewares/logging.js +54 -0
  103. package/src/plugins/shared/middlewares/rate-limit.js +73 -0
  104. package/src/plugins/shared/middlewares/security.js +158 -0
  105. package/src/plugins/shared/response-formatter.js +264 -0
  106. package/src/plugins/state-machine.plugin.js +57 -2
  107. package/src/resource.class.js +140 -12
  108. package/src/schema.class.js +30 -1
  109. package/src/validator.class.js +57 -6
  110. 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,12 +130,18 @@ 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,
56
138
  host: options.host || '0.0.0.0',
57
139
  verbose: options.verbose || false,
58
140
 
141
+ // Version prefix configuration (global default)
142
+ // Can be: true (use resource version), false (no prefix - DEFAULT), or string (custom prefix like 'api/v1')
143
+ versionPrefix: options.versionPrefix !== undefined ? options.versionPrefix : false,
144
+
59
145
  docs: {
60
146
  enabled: options.docs?.enabled !== false && options.docsEnabled !== false, // Enable by default
61
147
  ui: options.docs?.ui || 'redoc', // 'swagger' or 'redoc' (redoc is prettier!)
@@ -64,25 +150,21 @@ export class ApiPlugin extends Plugin {
64
150
  description: options.docs?.description || options.apiDescription || 'Auto-generated REST API for s3db.js resources'
65
151
  },
66
152
 
67
- // Authentication configuration
68
- auth: {
69
- jwt: {
70
- enabled: options.auth?.jwt?.enabled || false,
71
- secret: options.auth?.jwt?.secret || null,
72
- expiresIn: options.auth?.jwt?.expiresIn || '7d'
73
- },
74
- apiKey: {
75
- enabled: options.auth?.apiKey?.enabled || false,
76
- headerName: options.auth?.apiKey?.headerName || 'X-API-Key'
77
- },
78
- basic: {
79
- enabled: options.auth?.basic?.enabled || false,
80
- realm: options.auth?.basic?.realm || 'API Access'
81
- }
82
- },
153
+ // Authentication configuration (multiple drivers)
154
+ auth: normalizedAuth,
155
+
156
+ // Custom routes (plugin-level)
157
+ routes: options.routes || {},
83
158
 
84
- // Resource configuration
85
- resources: options.resources || {},
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
+ },
86
168
 
87
169
  // CORS configuration
88
170
  cors: {
@@ -125,19 +207,83 @@ export class ApiPlugin extends Plugin {
125
207
  returnValidationErrors: options.validation?.returnValidationErrors !== false
126
208
  },
127
209
 
128
- // 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)
129
283
  csp: {
130
284
  enabled: options.csp?.enabled || false,
131
- // Default CSP that works with Redoc v2.5.1 (allows CDN scripts/styles)
132
- directives: options.csp?.directives || {
133
- 'default-src': ["'self'"],
134
- 'script-src': ["'self'", "'unsafe-inline'", 'https://cdn.redoc.ly/redoc/v2.5.1/'],
135
- 'style-src': ["'self'", "'unsafe-inline'", 'https://cdn.redoc.ly/redoc/v2.5.1/', 'https://fonts.googleapis.com'],
136
- 'font-src': ["'self'", 'https://fonts.gstatic.com'],
137
- 'img-src': ["'self'", 'data:', 'https:'],
138
- 'connect-src': ["'self'"]
139
- },
140
- reportOnly: options.csp?.reportOnly || false, // If true, uses Content-Security-Policy-Report-Only
285
+ directives: options.csp?.directives || {},
286
+ reportOnly: options.csp?.reportOnly || false,
141
287
  reportUri: options.csp?.reportUri || null
142
288
  },
143
289
 
@@ -145,10 +291,78 @@ export class ApiPlugin extends Plugin {
145
291
  middlewares: options.middlewares || []
146
292
  };
147
293
 
294
+ this.config.resources = this._normalizeResourcesConfig(options.resources);
295
+
148
296
  this.server = null;
149
297
  this.usersResource = null;
150
298
  }
151
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
+
152
366
  /**
153
367
  * Validate plugin dependencies
154
368
  * @private
@@ -176,10 +390,8 @@ export class ApiPlugin extends Plugin {
176
390
  throw err;
177
391
  }
178
392
 
179
- // Create users resource if authentication is enabled
180
- const authEnabled = this.config.auth.jwt.enabled ||
181
- this.config.auth.apiKey.enabled ||
182
- this.config.auth.basic.enabled;
393
+ // Create users resource if authentication drivers are configured
394
+ const authEnabled = this.config.auth.drivers.length > 0;
183
395
 
184
396
  if (authEnabled) {
185
397
  await this._createUsersResource();
@@ -204,11 +416,12 @@ export class ApiPlugin extends Plugin {
204
416
  attributes: {
205
417
  id: 'string|required',
206
418
  username: 'string|required|minlength:3',
207
- email: 'string|optional|email',
419
+ email: 'string|required|email', // Required to support email-based auth
208
420
  password: 'secret|required|minlength:8',
209
421
  apiKey: 'string|optional',
210
422
  jwtSecret: 'string|optional',
211
423
  role: 'string|default:user',
424
+ scopes: 'array|items:string|optional', // Authorization scopes (e.g., ['read:users', 'write:cars'])
212
425
  active: 'boolean|default:true',
213
426
  createdAt: 'string|optional',
214
427
  lastLoginAt: 'string|optional',
@@ -235,7 +448,6 @@ export class ApiPlugin extends Plugin {
235
448
  throw err;
236
449
  }
237
450
  }
238
-
239
451
  /**
240
452
  * Setup middlewares
241
453
  * @private
@@ -245,19 +457,26 @@ export class ApiPlugin extends Plugin {
245
457
 
246
458
  // Add request ID middleware
247
459
  middlewares.push(async (c, next) => {
248
- c.set('requestId', crypto.randomUUID());
460
+ c.set('requestId', idGenerator());
249
461
  c.set('verbose', this.config.verbose);
250
462
  await next();
251
463
  });
252
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
+
253
471
  // Add CORS middleware
254
472
  if (this.config.cors.enabled) {
255
473
  const corsMiddleware = await this._createCorsMiddleware();
256
474
  middlewares.push(corsMiddleware);
257
475
  }
258
476
 
259
- // Add CSP middleware
260
- 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) {
261
480
  const cspMiddleware = await this._createCSPMiddleware();
262
481
  middlewares.push(cspMiddleware);
263
482
  }
@@ -288,8 +507,11 @@ export class ApiPlugin extends Plugin {
288
507
  }
289
508
 
290
509
  /**
291
- * Create CORS middleware (placeholder)
510
+ * Create CORS middleware
292
511
  * @private
512
+ *
513
+ * Handles Cross-Origin Resource Sharing (CORS) headers and preflight requests.
514
+ * Supports wildcard origins, credential-based requests, and OPTIONS preflight.
293
515
  */
294
516
  async _createCorsMiddleware() {
295
517
  return async (c, next) => {
@@ -353,8 +575,12 @@ export class ApiPlugin extends Plugin {
353
575
  }
354
576
 
355
577
  /**
356
- * Create rate limiting middleware (placeholder)
578
+ * Create rate limiting middleware
357
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.
358
584
  */
359
585
  async _createRateLimitMiddleware() {
360
586
  const requests = new Map();
@@ -410,10 +636,23 @@ export class ApiPlugin extends Plugin {
410
636
  }
411
637
 
412
638
  /**
413
- * Create logging middleware (placeholder)
639
+ * Create logging middleware with customizable format
414
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'
415
652
  */
416
653
  async _createLoggingMiddleware() {
654
+ const { format } = this.config.logging;
655
+
417
656
  return async (c, next) => {
418
657
  const start = Date.now();
419
658
  const method = c.req.method;
@@ -424,32 +663,236 @@ export class ApiPlugin extends Plugin {
424
663
 
425
664
  const duration = Date.now() - start;
426
665
  const status = c.res.status;
427
- const user = c.get('user')?.username || 'anonymous';
428
-
429
- 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}`);
430
678
  };
431
679
  }
432
680
 
433
681
  /**
434
- * Create compression middleware (placeholder)
682
+ * Create compression middleware (using Node.js zlib)
435
683
  * @private
436
684
  */
437
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
+
438
701
  return async (c, next) => {
439
702
  await next();
440
703
 
441
- // TODO: Implement actual compression using zlib
442
- // For now, this is a no-op placeholder to avoid ERR_CONTENT_DECODING_FAILED errors
443
- //
444
- // WARNING: Do NOT set Content-Encoding headers without actually compressing!
445
- // Setting these headers without compression causes browsers to fail with:
446
- // net::ERR_CONTENT_DECODING_FAILED 200 (OK)
447
- //
448
- // Real implementation would require:
449
- // 1. Check Accept-Encoding header
450
- // 2. Compress response body with zlib.gzip() or zlib.deflate()
451
- // 3. Set Content-Encoding header
452
- // 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();
453
896
  };
454
897
  }
455
898
 
@@ -466,7 +909,10 @@ export class ApiPlugin extends Plugin {
466
909
  port: this.config.port,
467
910
  host: this.config.host,
468
911
  database: this.database,
912
+ versionPrefix: this.config.versionPrefix,
469
913
  resources: this.config.resources,
914
+ routes: this.config.routes,
915
+ templates: this.config.templates,
470
916
  middlewares: this.compiledMiddlewares,
471
917
  verbose: this.config.verbose,
472
918
  auth: this.config.auth,
@@ -542,3 +988,10 @@ export class ApiPlugin extends Plugin {
542
988
  return this.server ? this.server.getApp() : null;
543
989
  }
544
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';