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.
Files changed (108) hide show
  1. package/README.md +89 -19
  2. package/dist/{s3db.cjs.js → s3db.cjs} +29780 -24384
  3. package/dist/s3db.cjs.map +1 -0
  4. package/dist/s3db.es.js +24263 -18860
  5. package/dist/s3db.es.js.map +1 -1
  6. package/package.json +227 -21
  7. package/src/concerns/id.js +90 -6
  8. package/src/concerns/index.js +2 -1
  9. package/src/concerns/password-hashing.js +150 -0
  10. package/src/database.class.js +4 -0
  11. package/src/plugins/api/auth/basic-auth.js +23 -1
  12. package/src/plugins/api/auth/index.js +49 -3
  13. package/src/plugins/api/auth/oauth2-auth.js +171 -0
  14. package/src/plugins/api/auth/oidc-auth.js +789 -0
  15. package/src/plugins/api/auth/oidc-client.js +462 -0
  16. package/src/plugins/api/auth/path-auth-matcher.js +284 -0
  17. package/src/plugins/api/concerns/event-emitter.js +134 -0
  18. package/src/plugins/api/concerns/failban-manager.js +651 -0
  19. package/src/plugins/api/concerns/guards-helpers.js +402 -0
  20. package/src/plugins/api/concerns/metrics-collector.js +346 -0
  21. package/src/plugins/api/concerns/opengraph-helper.js +116 -0
  22. package/src/plugins/api/concerns/state-machine.js +288 -0
  23. package/src/plugins/api/index.js +514 -54
  24. package/src/plugins/api/middlewares/failban.js +305 -0
  25. package/src/plugins/api/middlewares/rate-limit.js +301 -0
  26. package/src/plugins/api/middlewares/request-id.js +74 -0
  27. package/src/plugins/api/middlewares/security-headers.js +120 -0
  28. package/src/plugins/api/middlewares/session-tracking.js +194 -0
  29. package/src/plugins/api/routes/auth-routes.js +23 -3
  30. package/src/plugins/api/routes/resource-routes.js +71 -29
  31. package/src/plugins/api/server.js +1017 -94
  32. package/src/plugins/api/utils/guards.js +213 -0
  33. package/src/plugins/api/utils/mime-types.js +154 -0
  34. package/src/plugins/api/utils/openapi-generator.js +44 -11
  35. package/src/plugins/api/utils/path-matcher.js +173 -0
  36. package/src/plugins/api/utils/static-filesystem.js +262 -0
  37. package/src/plugins/api/utils/static-s3.js +231 -0
  38. package/src/plugins/api/utils/template-engine.js +262 -0
  39. package/src/plugins/cloud-inventory/drivers/alibaba-driver.js +853 -0
  40. package/src/plugins/cloud-inventory/drivers/aws-driver.js +2554 -0
  41. package/src/plugins/cloud-inventory/drivers/azure-driver.js +637 -0
  42. package/src/plugins/cloud-inventory/drivers/base-driver.js +99 -0
  43. package/src/plugins/cloud-inventory/drivers/cloudflare-driver.js +620 -0
  44. package/src/plugins/cloud-inventory/drivers/digitalocean-driver.js +698 -0
  45. package/src/plugins/cloud-inventory/drivers/gcp-driver.js +645 -0
  46. package/src/plugins/cloud-inventory/drivers/hetzner-driver.js +559 -0
  47. package/src/plugins/cloud-inventory/drivers/linode-driver.js +614 -0
  48. package/src/plugins/cloud-inventory/drivers/mock-drivers.js +449 -0
  49. package/src/plugins/cloud-inventory/drivers/mongodb-atlas-driver.js +771 -0
  50. package/src/plugins/cloud-inventory/drivers/oracle-driver.js +768 -0
  51. package/src/plugins/cloud-inventory/drivers/vultr-driver.js +636 -0
  52. package/src/plugins/cloud-inventory/index.js +20 -0
  53. package/src/plugins/cloud-inventory/registry.js +146 -0
  54. package/src/plugins/cloud-inventory/terraform-exporter.js +362 -0
  55. package/src/plugins/cloud-inventory.plugin.js +1333 -0
  56. package/src/plugins/concerns/plugin-dependencies.js +61 -1
  57. package/src/plugins/eventual-consistency/analytics.js +1 -0
  58. package/src/plugins/identity/README.md +335 -0
  59. package/src/plugins/identity/concerns/mfa-manager.js +204 -0
  60. package/src/plugins/identity/concerns/password.js +138 -0
  61. package/src/plugins/identity/concerns/resource-schemas.js +273 -0
  62. package/src/plugins/identity/concerns/token-generator.js +172 -0
  63. package/src/plugins/identity/email-service.js +422 -0
  64. package/src/plugins/identity/index.js +1052 -0
  65. package/src/plugins/identity/oauth2-server.js +1033 -0
  66. package/src/plugins/identity/oidc-discovery.js +285 -0
  67. package/src/plugins/identity/rsa-keys.js +323 -0
  68. package/src/plugins/identity/server.js +500 -0
  69. package/src/plugins/identity/session-manager.js +453 -0
  70. package/src/plugins/identity/ui/layouts/base.js +251 -0
  71. package/src/plugins/identity/ui/middleware.js +135 -0
  72. package/src/plugins/identity/ui/pages/admin/client-form.js +247 -0
  73. package/src/plugins/identity/ui/pages/admin/clients.js +179 -0
  74. package/src/plugins/identity/ui/pages/admin/dashboard.js +181 -0
  75. package/src/plugins/identity/ui/pages/admin/user-form.js +283 -0
  76. package/src/plugins/identity/ui/pages/admin/users.js +263 -0
  77. package/src/plugins/identity/ui/pages/consent.js +262 -0
  78. package/src/plugins/identity/ui/pages/forgot-password.js +104 -0
  79. package/src/plugins/identity/ui/pages/login.js +144 -0
  80. package/src/plugins/identity/ui/pages/mfa-backup-codes.js +180 -0
  81. package/src/plugins/identity/ui/pages/mfa-enrollment.js +187 -0
  82. package/src/plugins/identity/ui/pages/mfa-verification.js +178 -0
  83. package/src/plugins/identity/ui/pages/oauth-error.js +225 -0
  84. package/src/plugins/identity/ui/pages/profile.js +361 -0
  85. package/src/plugins/identity/ui/pages/register.js +226 -0
  86. package/src/plugins/identity/ui/pages/reset-password.js +128 -0
  87. package/src/plugins/identity/ui/pages/verify-email.js +172 -0
  88. package/src/plugins/identity/ui/routes.js +2541 -0
  89. package/src/plugins/identity/ui/styles/main.css +465 -0
  90. package/src/plugins/index.js +4 -1
  91. package/src/plugins/ml/base-model.class.js +32 -7
  92. package/src/plugins/ml/classification-model.class.js +1 -1
  93. package/src/plugins/ml/timeseries-model.class.js +3 -1
  94. package/src/plugins/ml.plugin.js +124 -32
  95. package/src/plugins/shared/error-handler.js +147 -0
  96. package/src/plugins/shared/index.js +9 -0
  97. package/src/plugins/shared/middlewares/compression.js +117 -0
  98. package/src/plugins/shared/middlewares/cors.js +49 -0
  99. package/src/plugins/shared/middlewares/index.js +11 -0
  100. package/src/plugins/shared/middlewares/logging.js +54 -0
  101. package/src/plugins/shared/middlewares/rate-limit.js +73 -0
  102. package/src/plugins/shared/middlewares/security.js +158 -0
  103. package/src/plugins/shared/response-formatter.js +264 -0
  104. package/src/plugins/tfstate/README.md +126 -126
  105. package/src/resource.class.js +140 -12
  106. package/src/schema.class.js +30 -1
  107. package/src/validator.class.js +57 -6
  108. 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
+ };