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.
- package/README.md +25 -10
- package/dist/{s3db.cjs.js → s3db.cjs} +38801 -32446
- package/dist/s3db.cjs.map +1 -0
- package/dist/s3db.es.js +38653 -32291
- package/dist/s3db.es.js.map +1 -1
- package/package.json +218 -22
- 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 +6 -2
- package/src/plugins/api/auth/basic-auth.js +40 -10
- 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/index.js +510 -57
- 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 +119 -78
- package/src/plugins/api/routes/resource-routes.js +73 -30
- package/src/plugins/api/server.js +1139 -45
- package/src/plugins/api/utils/custom-routes.js +102 -0
- 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 +91 -12
- 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 +188 -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 +62 -2
- package/src/plugins/eventual-consistency/analytics.js +1 -0
- package/src/plugins/eventual-consistency/consolidation.js +2 -2
- package/src/plugins/eventual-consistency/garbage-collection.js +2 -2
- package/src/plugins/eventual-consistency/install.js +2 -2
- 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 +65 -16
- 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 +584 -31
- 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/state-machine.plugin.js +57 -2
- 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
|
@@ -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
|
+
}
|