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
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Error Handler - Global error handling middleware
3
+ *
4
+ * Catches and formats errors from routes and middlewares
5
+ */
6
+
7
+ import { error as formatError } from './response-formatter.js';
8
+
9
+ /**
10
+ * Map s3db.js errors to HTTP status codes
11
+ */
12
+ const errorStatusMap = {
13
+ 'ValidationError': 400,
14
+ 'InvalidResourceItem': 400,
15
+ 'ResourceNotFound': 404,
16
+ 'NoSuchKey': 404,
17
+ 'NoSuchBucket': 404,
18
+ 'PartitionError': 400,
19
+ 'CryptoError': 500,
20
+ 'SchemaError': 400,
21
+ 'QueueError': 500,
22
+ 'ResourceError': 500
23
+ };
24
+
25
+ /**
26
+ * Get HTTP status code from error
27
+ * @param {Error} err - Error object
28
+ * @returns {number} HTTP status code
29
+ */
30
+ function getStatusFromError(err) {
31
+ // Check error name
32
+ if (err.name && errorStatusMap[err.name]) {
33
+ return errorStatusMap[err.name];
34
+ }
35
+
36
+ // Check error constructor name
37
+ if (err.constructor && err.constructor.name && errorStatusMap[err.constructor.name]) {
38
+ return errorStatusMap[err.constructor.name];
39
+ }
40
+
41
+ // Check for specific error patterns
42
+ if (err.message) {
43
+ if (err.message.includes('not found') || err.message.includes('does not exist')) {
44
+ return 404;
45
+ }
46
+ if (err.message.includes('validation') || err.message.includes('invalid')) {
47
+ return 400;
48
+ }
49
+ if (err.message.includes('unauthorized') || err.message.includes('authentication')) {
50
+ return 401;
51
+ }
52
+ if (err.message.includes('forbidden') || err.message.includes('permission')) {
53
+ return 403;
54
+ }
55
+ }
56
+
57
+ // Default to 500
58
+ return 500;
59
+ }
60
+
61
+ /**
62
+ * Global error handler middleware
63
+ * @param {Error} err - Error object
64
+ * @param {Object} c - Hono context
65
+ * @returns {Response} Error response
66
+ */
67
+ export function errorHandler(err, c) {
68
+ const status = getStatusFromError(err);
69
+
70
+ // Get error code from error name or default
71
+ const code = err.name || 'INTERNAL_ERROR';
72
+
73
+ // Extract error details
74
+ const details = {};
75
+
76
+ if (err.resource) details.resource = err.resource;
77
+ if (err.bucket) details.bucket = err.bucket;
78
+ if (err.key) details.key = err.key;
79
+ if (err.operation) details.operation = err.operation;
80
+ if (err.suggestion) details.suggestion = err.suggestion;
81
+ if (err.availableResources) details.availableResources = err.availableResources;
82
+
83
+ // Format error response
84
+ const response = formatError(err, {
85
+ status,
86
+ code,
87
+ details
88
+ });
89
+
90
+ // Log error (except for expected errors like 404)
91
+ if (status >= 500) {
92
+ console.error('[API Plugin] Error:', {
93
+ message: err.message,
94
+ code,
95
+ status,
96
+ stack: err.stack,
97
+ details
98
+ });
99
+ } else if (status >= 400 && status < 500 && c.get('verbose')) {
100
+ console.warn('[API Plugin] Client error:', {
101
+ message: err.message,
102
+ code,
103
+ status,
104
+ details
105
+ });
106
+ }
107
+
108
+ return c.json(response, response._status);
109
+ }
110
+
111
+ /**
112
+ * Async error wrapper for route handlers
113
+ * @param {Function} fn - Async route handler
114
+ * @returns {Function} Wrapped handler with error catching
115
+ */
116
+ export function asyncHandler(fn) {
117
+ return async (c) => {
118
+ try {
119
+ return await fn(c);
120
+ } catch (err) {
121
+ return errorHandler(err, c);
122
+ }
123
+ };
124
+ }
125
+
126
+ /**
127
+ * Try-catch wrapper with formatted error response
128
+ * @param {Function} fn - Function to execute
129
+ * @param {Object} c - Hono context
130
+ * @returns {Promise<[boolean, Error|null, any]>} [ok, error, result] tuple
131
+ */
132
+ export async function tryApiCall(fn, c) {
133
+ try {
134
+ const result = await fn();
135
+ return [true, null, result];
136
+ } catch (err) {
137
+ const response = errorHandler(err, c);
138
+ return [false, err, response];
139
+ }
140
+ }
141
+
142
+ export default {
143
+ errorHandler,
144
+ asyncHandler,
145
+ tryApiCall,
146
+ getStatusFromError
147
+ };
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Shared Plugin Utilities
3
+ *
4
+ * Common code shared between API Plugin and Identity Plugin
5
+ */
6
+
7
+ export { errorHandler } from './error-handler.js';
8
+ export * as formatter from './response-formatter.js';
9
+ export * as middlewares from './middlewares/index.js';
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Compression Middleware
3
+ *
4
+ * Compresses HTTP responses using gzip or brotli compression.
5
+ * Automatically skips already compressed content and small payloads.
6
+ */
7
+
8
+ import { gzip, brotliCompress } from 'zlib';
9
+ import { promisify } from 'util';
10
+
11
+ const gzipAsync = promisify(gzip);
12
+ const brotliAsync = promisify(brotliCompress);
13
+
14
+ /**
15
+ * Create compression middleware
16
+ * @param {Object} config - Compression configuration
17
+ * @param {number} config.threshold - Minimum size in bytes to compress
18
+ * @param {number} config.level - Compression level (1-9)
19
+ * @param {boolean} config.verbose - Enable verbose logging
20
+ * @returns {Function} Hono middleware
21
+ */
22
+ export function createCompressionMiddleware(config = {}) {
23
+ const {
24
+ threshold = 1024, // 1KB
25
+ level = 6,
26
+ verbose = false
27
+ } = config;
28
+
29
+ // Content types that should NOT be compressed (already compressed)
30
+ const skipContentTypes = [
31
+ 'image/', 'video/', 'audio/',
32
+ 'application/zip', 'application/gzip',
33
+ 'application/x-gzip', 'application/x-bzip2'
34
+ ];
35
+
36
+ return async (c, next) => {
37
+ await next();
38
+
39
+ // Skip if response has no body
40
+ if (!c.res || !c.res.body) {
41
+ return;
42
+ }
43
+
44
+ // Skip if already compressed
45
+ if (c.res.headers.has('content-encoding')) {
46
+ return;
47
+ }
48
+
49
+ // Skip if content-type should not be compressed
50
+ const contentType = c.res.headers.get('content-type') || '';
51
+ if (skipContentTypes.some(type => contentType.startsWith(type))) {
52
+ return;
53
+ }
54
+
55
+ // Check Accept-Encoding header
56
+ const acceptEncoding = c.req.header('accept-encoding') || '';
57
+ const supportsBrotli = acceptEncoding.includes('br');
58
+ const supportsGzip = acceptEncoding.includes('gzip');
59
+
60
+ if (!supportsBrotli && !supportsGzip) {
61
+ return; // Client doesn't support compression
62
+ }
63
+
64
+ // Get response body as buffer
65
+ let body;
66
+ try {
67
+ const text = await c.res.text();
68
+ body = Buffer.from(text, 'utf-8');
69
+ } catch (err) {
70
+ // If body is already consumed or not text, skip compression
71
+ return;
72
+ }
73
+
74
+ // Skip if body is too small
75
+ if (body.length < threshold) {
76
+ return;
77
+ }
78
+
79
+ // Compress with brotli (better) or gzip (fallback)
80
+ let compressed;
81
+ let encoding;
82
+
83
+ try {
84
+ if (supportsBrotli) {
85
+ compressed = await brotliAsync(body);
86
+ encoding = 'br';
87
+ } else {
88
+ compressed = await gzipAsync(body, { level });
89
+ encoding = 'gzip';
90
+ }
91
+
92
+ // Only use compressed if it's actually smaller
93
+ if (compressed.length >= body.length) {
94
+ return; // Compression didn't help, use original
95
+ }
96
+
97
+ // Create new response with compressed body
98
+ const headers = new Headers(c.res.headers);
99
+ headers.set('Content-Encoding', encoding);
100
+ headers.set('Content-Length', compressed.length.toString());
101
+ headers.set('Vary', 'Accept-Encoding');
102
+
103
+ // Replace response
104
+ c.res = new Response(compressed, {
105
+ status: c.res.status,
106
+ statusText: c.res.statusText,
107
+ headers
108
+ });
109
+
110
+ } catch (err) {
111
+ // Compression failed, log and continue with uncompressed response
112
+ if (verbose) {
113
+ console.error('[Compression] Error:', err.message);
114
+ }
115
+ }
116
+ };
117
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * CORS Middleware
3
+ *
4
+ * Handles Cross-Origin Resource Sharing (CORS) headers and preflight requests.
5
+ * Supports wildcard origins, credential-based requests, and OPTIONS preflight.
6
+ */
7
+
8
+ /**
9
+ * Create CORS middleware
10
+ * @param {Object} config - CORS configuration
11
+ * @param {string} config.origin - Allowed origin ('*' or specific domain)
12
+ * @param {Array<string>} config.methods - Allowed HTTP methods
13
+ * @param {Array<string>} config.allowedHeaders - Allowed request headers
14
+ * @param {Array<string>} config.exposedHeaders - Exposed response headers
15
+ * @param {boolean} config.credentials - Allow credentials
16
+ * @param {number} config.maxAge - Preflight cache duration
17
+ * @returns {Function} Hono middleware
18
+ */
19
+ export function createCorsMiddleware(config = {}) {
20
+ const {
21
+ origin = '*',
22
+ methods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
23
+ allowedHeaders = ['Content-Type', 'Authorization', 'X-API-Key'],
24
+ exposedHeaders = ['X-Total-Count', 'X-Page-Count'],
25
+ credentials = true,
26
+ maxAge = 86400
27
+ } = config;
28
+
29
+ return async (c, next) => {
30
+ // Set CORS headers
31
+ c.header('Access-Control-Allow-Origin', origin);
32
+ c.header('Access-Control-Allow-Methods', methods.join(', '));
33
+ c.header('Access-Control-Allow-Headers', allowedHeaders.join(', '));
34
+ c.header('Access-Control-Expose-Headers', exposedHeaders.join(', '));
35
+
36
+ if (credentials) {
37
+ c.header('Access-Control-Allow-Credentials', 'true');
38
+ }
39
+
40
+ c.header('Access-Control-Max-Age', maxAge.toString());
41
+
42
+ // Handle OPTIONS preflight
43
+ if (c.req.method === 'OPTIONS') {
44
+ return c.body(null, 204);
45
+ }
46
+
47
+ await next();
48
+ };
49
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Shared Middlewares
3
+ *
4
+ * Common HTTP middlewares used by multiple plugins (API Plugin, Identity Plugin)
5
+ */
6
+
7
+ export { createCorsMiddleware } from './cors.js';
8
+ export { createRateLimitMiddleware } from './rate-limit.js';
9
+ export { createLoggingMiddleware } from './logging.js';
10
+ export { createCompressionMiddleware } from './compression.js';
11
+ export { createSecurityMiddleware } from './security.js';
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Logging Middleware
3
+ *
4
+ * Logs HTTP requests with customizable format and tokens.
5
+ *
6
+ * Supported tokens:
7
+ * - :method - HTTP method (GET, POST, etc)
8
+ * - :path - Request path
9
+ * - :status - HTTP status code
10
+ * - :response-time - Response time in milliseconds
11
+ * - :user - Username or 'anonymous'
12
+ * - :requestId - Request ID (UUID)
13
+ *
14
+ * Example format: ':method :path :status :response-time ms - :user'
15
+ * Output: 'GET /api/v1/cars 200 45ms - john'
16
+ */
17
+
18
+ /**
19
+ * Create logging middleware
20
+ * @param {Object} config - Logging configuration
21
+ * @param {string} config.format - Log format string with tokens
22
+ * @param {boolean} config.verbose - Enable verbose logging
23
+ * @returns {Function} Hono middleware
24
+ */
25
+ export function createLoggingMiddleware(config = {}) {
26
+ const {
27
+ format = ':method :path :status :response-time ms',
28
+ verbose = false
29
+ } = config;
30
+
31
+ return async (c, next) => {
32
+ const start = Date.now();
33
+ const method = c.req.method;
34
+ const path = c.req.path;
35
+ const requestId = c.get('requestId');
36
+
37
+ await next();
38
+
39
+ const duration = Date.now() - start;
40
+ const status = c.res.status;
41
+ const user = c.get('user')?.username || c.get('user')?.email || 'anonymous';
42
+
43
+ // Parse format string with token replacement
44
+ let logMessage = format
45
+ .replace(':method', method)
46
+ .replace(':path', path)
47
+ .replace(':status', status)
48
+ .replace(':response-time', duration)
49
+ .replace(':user', user)
50
+ .replace(':requestId', requestId);
51
+
52
+ console.log(`[HTTP] ${logMessage}`);
53
+ };
54
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Rate Limiting Middleware
3
+ *
4
+ * Implements sliding window rate limiting with configurable window size and max requests.
5
+ * Returns 429 status code with Retry-After header when limit is exceeded.
6
+ * Uses IP address or custom key generator to track request counts.
7
+ */
8
+
9
+ /**
10
+ * Create rate limiting middleware
11
+ * @param {Object} config - Rate limiting configuration
12
+ * @param {number} config.windowMs - Time window in milliseconds
13
+ * @param {number} config.maxRequests - Maximum requests per window
14
+ * @param {Function} config.keyGenerator - Custom key generator function
15
+ * @returns {Function} Hono middleware
16
+ */
17
+ export function createRateLimitMiddleware(config = {}) {
18
+ const {
19
+ windowMs = 60000, // 1 minute
20
+ maxRequests = 100,
21
+ keyGenerator = null
22
+ } = config;
23
+
24
+ const requests = new Map();
25
+
26
+ return async (c, next) => {
27
+ // Generate key (IP or custom)
28
+ const key = keyGenerator
29
+ ? keyGenerator(c)
30
+ : c.req.header('x-forwarded-for') || c.req.header('cf-connecting-ip') || 'unknown';
31
+
32
+ // Get or create request count
33
+ if (!requests.has(key)) {
34
+ requests.set(key, { count: 0, resetAt: Date.now() + windowMs });
35
+ }
36
+
37
+ const record = requests.get(key);
38
+
39
+ // Reset if window expired
40
+ if (Date.now() > record.resetAt) {
41
+ record.count = 0;
42
+ record.resetAt = Date.now() + windowMs;
43
+ }
44
+
45
+ // Check limit
46
+ if (record.count >= maxRequests) {
47
+ const retryAfter = Math.ceil((record.resetAt - Date.now()) / 1000);
48
+ c.header('Retry-After', retryAfter.toString());
49
+ c.header('X-RateLimit-Limit', maxRequests.toString());
50
+ c.header('X-RateLimit-Remaining', '0');
51
+ c.header('X-RateLimit-Reset', record.resetAt.toString());
52
+
53
+ return c.json({
54
+ success: false,
55
+ error: {
56
+ message: 'Rate limit exceeded',
57
+ code: 'RATE_LIMIT_EXCEEDED',
58
+ details: { retryAfter }
59
+ }
60
+ }, 429);
61
+ }
62
+
63
+ // Increment count
64
+ record.count++;
65
+
66
+ // Set rate limit headers
67
+ c.header('X-RateLimit-Limit', maxRequests.toString());
68
+ c.header('X-RateLimit-Remaining', (maxRequests - record.count).toString());
69
+ c.header('X-RateLimit-Reset', record.resetAt.toString());
70
+
71
+ await next();
72
+ };
73
+ }
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Security Headers Middleware (Helmet-like)
3
+ *
4
+ * Adds security headers to HTTP responses:
5
+ * - Content-Security-Policy (CSP)
6
+ * - X-Frame-Options (clickjacking)
7
+ * - X-Content-Type-Options (MIME sniffing)
8
+ * - Strict-Transport-Security (HSTS)
9
+ * - Referrer-Policy
10
+ * - X-DNS-Prefetch-Control
11
+ * - X-Download-Options
12
+ * - X-Permitted-Cross-Domain-Policies
13
+ * - X-XSS-Protection
14
+ * - Permissions-Policy
15
+ */
16
+
17
+ /**
18
+ * Create security headers middleware
19
+ * @param {Object} config - Security configuration
20
+ * @returns {Function} Hono middleware
21
+ */
22
+ export function createSecurityMiddleware(config = {}) {
23
+ const {
24
+ contentSecurityPolicy = {
25
+ enabled: true,
26
+ directives: {
27
+ 'default-src': ["'self'"],
28
+ 'script-src': ["'self'", "'unsafe-inline'"],
29
+ 'style-src': ["'self'", "'unsafe-inline'"],
30
+ 'img-src': ["'self'", 'data:', 'https:']
31
+ },
32
+ reportOnly: false,
33
+ reportUri: null
34
+ },
35
+ frameguard = { action: 'deny' },
36
+ noSniff = true,
37
+ hsts = {
38
+ maxAge: 15552000, // 180 days
39
+ includeSubDomains: true,
40
+ preload: false
41
+ },
42
+ referrerPolicy = { policy: 'no-referrer' },
43
+ dnsPrefetchControl = { allow: false },
44
+ ieNoOpen = true,
45
+ permittedCrossDomainPolicies = { policy: 'none' },
46
+ xssFilter = { mode: 'block' },
47
+ permissionsPolicy = {
48
+ features: {
49
+ geolocation: [],
50
+ microphone: [],
51
+ camera: [],
52
+ payment: [],
53
+ usb: []
54
+ }
55
+ }
56
+ } = config;
57
+
58
+ return async (c, next) => {
59
+ // X-Content-Type-Options: nosniff (MIME sniffing protection)
60
+ if (noSniff) {
61
+ c.header('X-Content-Type-Options', 'nosniff');
62
+ }
63
+
64
+ // X-Frame-Options (clickjacking protection)
65
+ if (frameguard) {
66
+ const action = frameguard.action.toUpperCase();
67
+ if (action === 'DENY') {
68
+ c.header('X-Frame-Options', 'DENY');
69
+ } else if (action === 'SAMEORIGIN') {
70
+ c.header('X-Frame-Options', 'SAMEORIGIN');
71
+ }
72
+ }
73
+
74
+ // Strict-Transport-Security (HSTS - force HTTPS)
75
+ if (hsts) {
76
+ const parts = [`max-age=${hsts.maxAge}`];
77
+ if (hsts.includeSubDomains) {
78
+ parts.push('includeSubDomains');
79
+ }
80
+ if (hsts.preload) {
81
+ parts.push('preload');
82
+ }
83
+ c.header('Strict-Transport-Security', parts.join('; '));
84
+ }
85
+
86
+ // Referrer-Policy (privacy)
87
+ if (referrerPolicy) {
88
+ c.header('Referrer-Policy', referrerPolicy.policy);
89
+ }
90
+
91
+ // X-DNS-Prefetch-Control (DNS leak protection)
92
+ if (dnsPrefetchControl) {
93
+ const value = dnsPrefetchControl.allow ? 'on' : 'off';
94
+ c.header('X-DNS-Prefetch-Control', value);
95
+ }
96
+
97
+ // X-Download-Options (IE8+ download security)
98
+ if (ieNoOpen) {
99
+ c.header('X-Download-Options', 'noopen');
100
+ }
101
+
102
+ // X-Permitted-Cross-Domain-Policies (Flash/PDF security)
103
+ if (permittedCrossDomainPolicies) {
104
+ c.header('X-Permitted-Cross-Domain-Policies', permittedCrossDomainPolicies.policy);
105
+ }
106
+
107
+ // X-XSS-Protection (legacy XSS filter)
108
+ if (xssFilter) {
109
+ const mode = xssFilter.mode;
110
+ c.header('X-XSS-Protection', mode === 'block' ? '1; mode=block' : '0');
111
+ }
112
+
113
+ // Permissions-Policy (modern feature policy)
114
+ if (permissionsPolicy && permissionsPolicy.features) {
115
+ const features = permissionsPolicy.features;
116
+ const policies = [];
117
+
118
+ for (const [feature, allowList] of Object.entries(features)) {
119
+ if (Array.isArray(allowList)) {
120
+ const value = allowList.length === 0
121
+ ? `${feature}=()`
122
+ : `${feature}=(${allowList.join(' ')})`;
123
+ policies.push(value);
124
+ }
125
+ }
126
+
127
+ if (policies.length > 0) {
128
+ c.header('Permissions-Policy', policies.join(', '));
129
+ }
130
+ }
131
+
132
+ // Content-Security-Policy (CSP)
133
+ if (contentSecurityPolicy && contentSecurityPolicy.enabled !== false && contentSecurityPolicy.directives) {
134
+ const cspParts = [];
135
+ for (const [directive, values] of Object.entries(contentSecurityPolicy.directives)) {
136
+ if (Array.isArray(values) && values.length > 0) {
137
+ cspParts.push(`${directive} ${values.join(' ')}`);
138
+ } else if (typeof values === 'string') {
139
+ cspParts.push(`${directive} ${values}`);
140
+ }
141
+ }
142
+
143
+ if (contentSecurityPolicy.reportUri) {
144
+ cspParts.push(`report-uri ${contentSecurityPolicy.reportUri}`);
145
+ }
146
+
147
+ if (cspParts.length > 0) {
148
+ const cspValue = cspParts.join('; ');
149
+ const headerName = contentSecurityPolicy.reportOnly
150
+ ? 'Content-Security-Policy-Report-Only'
151
+ : 'Content-Security-Policy';
152
+ c.header(headerName, cspValue);
153
+ }
154
+ }
155
+
156
+ await next();
157
+ };
158
+ }