s3db.js 13.5.1 → 13.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +89 -19
- package/dist/{s3db.cjs.js → s3db.cjs} +29780 -24384
- package/dist/s3db.cjs.map +1 -0
- package/dist/s3db.es.js +24263 -18860
- package/dist/s3db.es.js.map +1 -1
- package/package.json +227 -21
- package/src/concerns/id.js +90 -6
- package/src/concerns/index.js +2 -1
- package/src/concerns/password-hashing.js +150 -0
- package/src/database.class.js +4 -0
- package/src/plugins/api/auth/basic-auth.js +23 -1
- package/src/plugins/api/auth/index.js +49 -3
- package/src/plugins/api/auth/oauth2-auth.js +171 -0
- package/src/plugins/api/auth/oidc-auth.js +789 -0
- package/src/plugins/api/auth/oidc-client.js +462 -0
- package/src/plugins/api/auth/path-auth-matcher.js +284 -0
- package/src/plugins/api/concerns/event-emitter.js +134 -0
- package/src/plugins/api/concerns/failban-manager.js +651 -0
- package/src/plugins/api/concerns/guards-helpers.js +402 -0
- package/src/plugins/api/concerns/metrics-collector.js +346 -0
- package/src/plugins/api/concerns/opengraph-helper.js +116 -0
- package/src/plugins/api/concerns/state-machine.js +288 -0
- package/src/plugins/api/index.js +514 -54
- package/src/plugins/api/middlewares/failban.js +305 -0
- package/src/plugins/api/middlewares/rate-limit.js +301 -0
- package/src/plugins/api/middlewares/request-id.js +74 -0
- package/src/plugins/api/middlewares/security-headers.js +120 -0
- package/src/plugins/api/middlewares/session-tracking.js +194 -0
- package/src/plugins/api/routes/auth-routes.js +23 -3
- package/src/plugins/api/routes/resource-routes.js +71 -29
- package/src/plugins/api/server.js +1017 -94
- package/src/plugins/api/utils/guards.js +213 -0
- package/src/plugins/api/utils/mime-types.js +154 -0
- package/src/plugins/api/utils/openapi-generator.js +44 -11
- package/src/plugins/api/utils/path-matcher.js +173 -0
- package/src/plugins/api/utils/static-filesystem.js +262 -0
- package/src/plugins/api/utils/static-s3.js +231 -0
- package/src/plugins/api/utils/template-engine.js +262 -0
- package/src/plugins/cloud-inventory/drivers/alibaba-driver.js +853 -0
- package/src/plugins/cloud-inventory/drivers/aws-driver.js +2554 -0
- package/src/plugins/cloud-inventory/drivers/azure-driver.js +637 -0
- package/src/plugins/cloud-inventory/drivers/base-driver.js +99 -0
- package/src/plugins/cloud-inventory/drivers/cloudflare-driver.js +620 -0
- package/src/plugins/cloud-inventory/drivers/digitalocean-driver.js +698 -0
- package/src/plugins/cloud-inventory/drivers/gcp-driver.js +645 -0
- package/src/plugins/cloud-inventory/drivers/hetzner-driver.js +559 -0
- package/src/plugins/cloud-inventory/drivers/linode-driver.js +614 -0
- package/src/plugins/cloud-inventory/drivers/mock-drivers.js +449 -0
- package/src/plugins/cloud-inventory/drivers/mongodb-atlas-driver.js +771 -0
- package/src/plugins/cloud-inventory/drivers/oracle-driver.js +768 -0
- package/src/plugins/cloud-inventory/drivers/vultr-driver.js +636 -0
- package/src/plugins/cloud-inventory/index.js +20 -0
- package/src/plugins/cloud-inventory/registry.js +146 -0
- package/src/plugins/cloud-inventory/terraform-exporter.js +362 -0
- package/src/plugins/cloud-inventory.plugin.js +1333 -0
- package/src/plugins/concerns/plugin-dependencies.js +61 -1
- package/src/plugins/eventual-consistency/analytics.js +1 -0
- package/src/plugins/identity/README.md +335 -0
- package/src/plugins/identity/concerns/mfa-manager.js +204 -0
- package/src/plugins/identity/concerns/password.js +138 -0
- package/src/plugins/identity/concerns/resource-schemas.js +273 -0
- package/src/plugins/identity/concerns/token-generator.js +172 -0
- package/src/plugins/identity/email-service.js +422 -0
- package/src/plugins/identity/index.js +1052 -0
- package/src/plugins/identity/oauth2-server.js +1033 -0
- package/src/plugins/identity/oidc-discovery.js +285 -0
- package/src/plugins/identity/rsa-keys.js +323 -0
- package/src/plugins/identity/server.js +500 -0
- package/src/plugins/identity/session-manager.js +453 -0
- package/src/plugins/identity/ui/layouts/base.js +251 -0
- package/src/plugins/identity/ui/middleware.js +135 -0
- package/src/plugins/identity/ui/pages/admin/client-form.js +247 -0
- package/src/plugins/identity/ui/pages/admin/clients.js +179 -0
- package/src/plugins/identity/ui/pages/admin/dashboard.js +181 -0
- package/src/plugins/identity/ui/pages/admin/user-form.js +283 -0
- package/src/plugins/identity/ui/pages/admin/users.js +263 -0
- package/src/plugins/identity/ui/pages/consent.js +262 -0
- package/src/plugins/identity/ui/pages/forgot-password.js +104 -0
- package/src/plugins/identity/ui/pages/login.js +144 -0
- package/src/plugins/identity/ui/pages/mfa-backup-codes.js +180 -0
- package/src/plugins/identity/ui/pages/mfa-enrollment.js +187 -0
- package/src/plugins/identity/ui/pages/mfa-verification.js +178 -0
- package/src/plugins/identity/ui/pages/oauth-error.js +225 -0
- package/src/plugins/identity/ui/pages/profile.js +361 -0
- package/src/plugins/identity/ui/pages/register.js +226 -0
- package/src/plugins/identity/ui/pages/reset-password.js +128 -0
- package/src/plugins/identity/ui/pages/verify-email.js +172 -0
- package/src/plugins/identity/ui/routes.js +2541 -0
- package/src/plugins/identity/ui/styles/main.css +465 -0
- package/src/plugins/index.js +4 -1
- package/src/plugins/ml/base-model.class.js +32 -7
- package/src/plugins/ml/classification-model.class.js +1 -1
- package/src/plugins/ml/timeseries-model.class.js +3 -1
- package/src/plugins/ml.plugin.js +124 -32
- package/src/plugins/shared/error-handler.js +147 -0
- package/src/plugins/shared/index.js +9 -0
- package/src/plugins/shared/middlewares/compression.js +117 -0
- package/src/plugins/shared/middlewares/cors.js +49 -0
- package/src/plugins/shared/middlewares/index.js +11 -0
- package/src/plugins/shared/middlewares/logging.js +54 -0
- package/src/plugins/shared/middlewares/rate-limit.js +73 -0
- package/src/plugins/shared/middlewares/security.js +158 -0
- package/src/plugins/shared/response-formatter.js +264 -0
- package/src/plugins/tfstate/README.md +126 -126
- package/src/resource.class.js +140 -12
- package/src/schema.class.js +30 -1
- package/src/validator.class.js +57 -6
- package/dist/s3db.cjs.js.map +0 -1
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Response Formatter - Standard JSON API responses
|
|
3
|
+
*
|
|
4
|
+
* Provides consistent response formatting across all API endpoints
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Format successful response
|
|
9
|
+
* @param {Object} data - Response data
|
|
10
|
+
* @param {Object} options - Response options
|
|
11
|
+
* @param {number} options.status - HTTP status code (default: 200)
|
|
12
|
+
* @param {Object} options.meta - Additional metadata
|
|
13
|
+
* @returns {Object} Formatted response
|
|
14
|
+
*/
|
|
15
|
+
export function success(data, options = {}) {
|
|
16
|
+
const { status = 200, meta = {} } = options;
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
success: true,
|
|
20
|
+
data,
|
|
21
|
+
meta: {
|
|
22
|
+
timestamp: new Date().toISOString(),
|
|
23
|
+
...meta
|
|
24
|
+
},
|
|
25
|
+
_status: status
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Format error response
|
|
31
|
+
* @param {string|Error} error - Error message or Error object
|
|
32
|
+
* @param {Object} options - Error options
|
|
33
|
+
* @param {number} options.status - HTTP status code (default: 500)
|
|
34
|
+
* @param {string} options.code - Error code
|
|
35
|
+
* @param {Object} options.details - Additional error details
|
|
36
|
+
* @returns {Object} Formatted error response
|
|
37
|
+
*/
|
|
38
|
+
export function error(error, options = {}) {
|
|
39
|
+
const { status = 500, code = 'INTERNAL_ERROR', details = {} } = options;
|
|
40
|
+
|
|
41
|
+
const errorMessage = error instanceof Error ? error.message : error;
|
|
42
|
+
const errorStack = error instanceof Error && process.env.NODE_ENV !== 'production'
|
|
43
|
+
? error.stack
|
|
44
|
+
: undefined;
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
success: false,
|
|
48
|
+
error: {
|
|
49
|
+
message: errorMessage,
|
|
50
|
+
code,
|
|
51
|
+
details,
|
|
52
|
+
stack: errorStack
|
|
53
|
+
},
|
|
54
|
+
meta: {
|
|
55
|
+
timestamp: new Date().toISOString()
|
|
56
|
+
},
|
|
57
|
+
_status: status
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Format list response with pagination
|
|
63
|
+
* @param {Array} items - List items
|
|
64
|
+
* @param {Object} pagination - Pagination info
|
|
65
|
+
* @param {number} pagination.total - Total count
|
|
66
|
+
* @param {number} pagination.page - Current page
|
|
67
|
+
* @param {number} pagination.pageSize - Items per page
|
|
68
|
+
* @param {number} pagination.pageCount - Total pages
|
|
69
|
+
* @returns {Object} Formatted list response
|
|
70
|
+
*/
|
|
71
|
+
export function list(items, pagination = {}) {
|
|
72
|
+
const { total, page, pageSize, pageCount } = pagination;
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
success: true,
|
|
76
|
+
data: items,
|
|
77
|
+
pagination: {
|
|
78
|
+
total: total || items.length,
|
|
79
|
+
page: page || 1,
|
|
80
|
+
pageSize: pageSize || items.length,
|
|
81
|
+
pageCount: pageCount || 1
|
|
82
|
+
},
|
|
83
|
+
meta: {
|
|
84
|
+
timestamp: new Date().toISOString()
|
|
85
|
+
},
|
|
86
|
+
_status: 200
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Format created response
|
|
92
|
+
* @param {Object} data - Created resource data
|
|
93
|
+
* @param {string} location - Resource location URL
|
|
94
|
+
* @returns {Object} Formatted created response
|
|
95
|
+
*/
|
|
96
|
+
export function created(data, location) {
|
|
97
|
+
return {
|
|
98
|
+
success: true,
|
|
99
|
+
data,
|
|
100
|
+
meta: {
|
|
101
|
+
timestamp: new Date().toISOString(),
|
|
102
|
+
location
|
|
103
|
+
},
|
|
104
|
+
_status: 201
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Format no content response
|
|
110
|
+
* @returns {Object} Formatted no content response
|
|
111
|
+
*/
|
|
112
|
+
export function noContent() {
|
|
113
|
+
return {
|
|
114
|
+
success: true,
|
|
115
|
+
data: null,
|
|
116
|
+
meta: {
|
|
117
|
+
timestamp: new Date().toISOString()
|
|
118
|
+
},
|
|
119
|
+
_status: 204
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Format validation error response
|
|
125
|
+
* @param {Array} errors - Validation errors
|
|
126
|
+
* @returns {Object} Formatted validation error response
|
|
127
|
+
*/
|
|
128
|
+
export function validationError(errors) {
|
|
129
|
+
return error('Validation failed', {
|
|
130
|
+
status: 400,
|
|
131
|
+
code: 'VALIDATION_ERROR',
|
|
132
|
+
details: { errors }
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Format not found response
|
|
138
|
+
* @param {string} resource - Resource name
|
|
139
|
+
* @param {string} id - Resource ID
|
|
140
|
+
* @returns {Object} Formatted not found response
|
|
141
|
+
*/
|
|
142
|
+
export function notFound(resource, id) {
|
|
143
|
+
return error(`${resource} with id '${id}' not found`, {
|
|
144
|
+
status: 404,
|
|
145
|
+
code: 'NOT_FOUND',
|
|
146
|
+
details: { resource, id }
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Format unauthorized response
|
|
152
|
+
* @param {string} message - Unauthorized message
|
|
153
|
+
* @returns {Object} Formatted unauthorized response
|
|
154
|
+
*/
|
|
155
|
+
export function unauthorized(message = 'Unauthorized') {
|
|
156
|
+
return error(message, {
|
|
157
|
+
status: 401,
|
|
158
|
+
code: 'UNAUTHORIZED'
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Format forbidden response
|
|
164
|
+
* @param {string} message - Forbidden message
|
|
165
|
+
* @returns {Object} Formatted forbidden response
|
|
166
|
+
*/
|
|
167
|
+
export function forbidden(message = 'Forbidden') {
|
|
168
|
+
return error(message, {
|
|
169
|
+
status: 403,
|
|
170
|
+
code: 'FORBIDDEN'
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Format rate limit exceeded response
|
|
176
|
+
* @param {number} retryAfter - Retry after seconds
|
|
177
|
+
* @returns {Object} Formatted rate limit response
|
|
178
|
+
*/
|
|
179
|
+
export function rateLimitExceeded(retryAfter) {
|
|
180
|
+
return error('Rate limit exceeded', {
|
|
181
|
+
status: 429,
|
|
182
|
+
code: 'RATE_LIMIT_EXCEEDED',
|
|
183
|
+
details: { retryAfter }
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Format payload too large response
|
|
189
|
+
* @param {number} size - Received payload size in bytes
|
|
190
|
+
* @param {number} limit - Maximum allowed size in bytes
|
|
191
|
+
* @returns {Object} Formatted payload too large response
|
|
192
|
+
*/
|
|
193
|
+
export function payloadTooLarge(size, limit) {
|
|
194
|
+
return error('Request payload too large', {
|
|
195
|
+
status: 413,
|
|
196
|
+
code: 'PAYLOAD_TOO_LARGE',
|
|
197
|
+
details: {
|
|
198
|
+
receivedSize: size,
|
|
199
|
+
maxSize: limit,
|
|
200
|
+
receivedMB: (size / 1024 / 1024).toFixed(2),
|
|
201
|
+
maxMB: (limit / 1024 / 1024).toFixed(2)
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Create custom formatters with override support
|
|
208
|
+
*
|
|
209
|
+
* Allows customization of response formats while maintaining fallbacks.
|
|
210
|
+
* Useful for adapting to existing API contracts or organizational standards.
|
|
211
|
+
*
|
|
212
|
+
* @param {Object} customFormatters - Custom formatter functions
|
|
213
|
+
* @param {Function} customFormatters.success - Custom success formatter
|
|
214
|
+
* @param {Function} customFormatters.error - Custom error formatter
|
|
215
|
+
* @param {Function} customFormatters.list - Custom list formatter
|
|
216
|
+
* @param {Function} customFormatters.created - Custom created formatter
|
|
217
|
+
* @returns {Object} Formatters object with custom overrides
|
|
218
|
+
*
|
|
219
|
+
* @example
|
|
220
|
+
* const formatters = createCustomFormatters({
|
|
221
|
+
* success: (data, meta) => ({ ok: true, result: data, ...meta }),
|
|
222
|
+
* error: (err, status) => ({ ok: false, message: err.message, code: status })
|
|
223
|
+
* });
|
|
224
|
+
*
|
|
225
|
+
* // Use in API routes:
|
|
226
|
+
* return c.json(formatters.success(user));
|
|
227
|
+
*/
|
|
228
|
+
export function createCustomFormatters(customFormatters = {}) {
|
|
229
|
+
// Default formatters
|
|
230
|
+
const defaults = {
|
|
231
|
+
success: (data, meta = {}) => success(data, { meta }),
|
|
232
|
+
error: (err, status, code) => error(err, { status, code }),
|
|
233
|
+
list: (items, pagination) => list(items, pagination),
|
|
234
|
+
created: (data, location) => created(data, location),
|
|
235
|
+
noContent: () => noContent(),
|
|
236
|
+
validationError: (errors) => validationError(errors),
|
|
237
|
+
notFound: (resource, id) => notFound(resource, id),
|
|
238
|
+
unauthorized: (message) => unauthorized(message),
|
|
239
|
+
forbidden: (message) => forbidden(message),
|
|
240
|
+
rateLimitExceeded: (retryAfter) => rateLimitExceeded(retryAfter),
|
|
241
|
+
payloadTooLarge: (size, limit) => payloadTooLarge(size, limit)
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
// Merge custom formatters with defaults
|
|
245
|
+
return {
|
|
246
|
+
...defaults,
|
|
247
|
+
...customFormatters
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export default {
|
|
252
|
+
success,
|
|
253
|
+
error,
|
|
254
|
+
list,
|
|
255
|
+
created,
|
|
256
|
+
noContent,
|
|
257
|
+
validationError,
|
|
258
|
+
notFound,
|
|
259
|
+
unauthorized,
|
|
260
|
+
forbidden,
|
|
261
|
+
rateLimitExceeded,
|
|
262
|
+
payloadTooLarge,
|
|
263
|
+
createCustomFormatters
|
|
264
|
+
};
|