cumstack 1.0.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.
@@ -0,0 +1,292 @@
1
+ /**
2
+ * cumstack Hono Utilities
3
+ * helper functions for working with hono
4
+ */
5
+
6
+ /**
7
+ * create a middleware for i18n detection
8
+ * @param {Object} config - i18n configuration
9
+ * @param {string} config.fallbackLng - Fallback language
10
+ * @param {boolean} config.explicitRouting - Enable explicit language routing
11
+ * @param {Array<string>} config.supportedLanguages - Supported languages
12
+ * @param {string} config.defaultLng - Default language setting
13
+ * @returns {Function} Hono middleware function
14
+ */
15
+ export function createI18nMiddleware(config) {
16
+ return async (c, next) => {
17
+ const url = new URL(c.req.url);
18
+ let language = config.fallbackLng;
19
+
20
+ // check url path first if explicit routing
21
+ if (config.explicitRouting) {
22
+ const pathSegments = url.pathname.split('/').filter(Boolean);
23
+ if (pathSegments.length > 0 && config.supportedLanguages.includes(pathSegments[0])) language = pathSegments[0];
24
+ }
25
+
26
+ // check accept-language header if auto-detect
27
+ if (language === config.fallbackLng && config.defaultLng === 'auto') {
28
+ const acceptLang = c.req.header('accept-language');
29
+ if (acceptLang) {
30
+ const detected = acceptLang
31
+ .split(',')
32
+ .map((l) => l.split(';')[0].trim().split('-')[0])
33
+ .find((l) => config.supportedLanguages.includes(l));
34
+ if (detected) language = detected;
35
+ }
36
+ }
37
+ c.set('language', language);
38
+ await next();
39
+ };
40
+ }
41
+
42
+ /**
43
+ * create a middleware for CORS handling
44
+ * @param {Object} [options] - CORS configuration options
45
+ * @param {string} [options.origin] - Allowed origin
46
+ * @param {Array<string>} [options.methods] - Allowed HTTP methods
47
+ * @param {Array<string>} [options.allowHeaders] - Allowed headers
48
+ * @param {Array<string>} [options.exposeHeaders] - Exposed headers
49
+ * @param {boolean} [options.credentials] - Allow credentials
50
+ * @param {number} [options.maxAge] - Max age for preflight cache
51
+ * @returns {Function} Hono middleware function
52
+ */
53
+ export function createCorsMiddleware(options = {}) {
54
+ const {
55
+ origin = '*',
56
+ methods = ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
57
+ allowHeaders = ['Content-Type', 'Authorization'],
58
+ exposeHeaders = [],
59
+ credentials = false,
60
+ maxAge = 86400,
61
+ } = options;
62
+
63
+ return async (c, next) => {
64
+ // handle preflight
65
+ if (c.req.method === 'OPTIONS') {
66
+ return new Response(null, {
67
+ status: 204,
68
+ headers: {
69
+ 'Access-Control-Allow-Origin': origin,
70
+ 'Access-Control-Allow-Methods': methods.join(', '),
71
+ 'Access-Control-Allow-Headers': allowHeaders.join(', '),
72
+ 'Access-Control-Max-Age': maxAge.toString(),
73
+ ...(credentials && { 'Access-Control-Allow-Credentials': 'true' }),
74
+ ...(exposeHeaders.length && {
75
+ 'Access-Control-Expose-Headers': exposeHeaders.join(', '),
76
+ }),
77
+ },
78
+ });
79
+ }
80
+
81
+ await next();
82
+
83
+ // add cors headers to response
84
+ c.header('Access-Control-Allow-Origin', origin);
85
+ if (credentials) c.header('Access-Control-Allow-Credentials', 'true');
86
+ };
87
+ }
88
+
89
+ /**
90
+ * create a middleware for security headers
91
+ * @returns {Function} Hono middleware function that adds security headers
92
+ */
93
+ export function createSecurityHeadersMiddleware() {
94
+ return async (c, next) => {
95
+ await next();
96
+
97
+ // security headers
98
+ c.header('X-Content-Type-Options', 'nosniff');
99
+ c.header('X-Frame-Options', 'DENY');
100
+ c.header('X-XSS-Protection', '1; mode=block');
101
+ c.header('Referrer-Policy', 'strict-origin-when-cross-origin');
102
+ };
103
+ }
104
+
105
+ /**
106
+ * create a middleware for HTTP caching
107
+ * @param {Object} [options] - Cache configuration
108
+ * @param {number} [options.maxAge] - Max age in seconds
109
+ * @param {number} [options.sMaxAge] - Shared cache max age
110
+ * @param {number} [options.staleWhileRevalidate] - Stale while revalidate time
111
+ * @param {number} [options.staleIfError] - Stale if error time
112
+ * @param {boolean} [options.public] - Public cache
113
+ * @param {boolean} [options.private] - Private cache
114
+ * @param {boolean} [options.noCache] - No cache directive
115
+ * @param {boolean} [options.noStore] - No store directive
116
+ * @param {boolean} [options.mustRevalidate] - Must revalidate directive
117
+ * @returns {Function} Hono middleware function
118
+ */
119
+ export function createCacheMiddleware(options = {}) {
120
+ const {
121
+ maxAge = 0,
122
+ sMaxAge,
123
+ staleWhileRevalidate,
124
+ staleIfError,
125
+ public: isPublic = true,
126
+ private: isPrivate = false,
127
+ noCache = false,
128
+ noStore = false,
129
+ mustRevalidate = false,
130
+ } = options;
131
+
132
+ return async (c, next) => {
133
+ await next();
134
+ const directives = [];
135
+ if (noStore) directives.push('no-store');
136
+ if (noCache) directives.push('no-cache');
137
+ if (isPublic) directives.push('public');
138
+ if (isPrivate) directives.push('private');
139
+ if (mustRevalidate) directives.push('must-revalidate');
140
+ if (maxAge !== undefined) directives.push(`max-age=${maxAge}`);
141
+ if (sMaxAge !== undefined) directives.push(`s-maxage=${sMaxAge}`);
142
+ if (staleWhileRevalidate !== undefined) directives.push(`stale-while-revalidate=${staleWhileRevalidate}`);
143
+ if (staleIfError !== undefined) directives.push(`stale-if-error=${staleIfError}`);
144
+ if (directives.length > 0) c.header('Cache-Control', directives.join(', '));
145
+ };
146
+ }
147
+
148
+ /**
149
+ * create a middleware for response compression
150
+ * @returns {Function} Hono middleware function that adds compression headers
151
+ */
152
+ export function createCompressionMiddleware() {
153
+ return async (c, next) => {
154
+ await next();
155
+ const acceptEncoding = c.req.header('accept-encoding') || '';
156
+ if (acceptEncoding.includes('gzip')) c.header('Content-Encoding', 'gzip');
157
+ else if (acceptEncoding.includes('deflate')) c.header('Content-Encoding', 'deflate');
158
+ };
159
+ }
160
+
161
+ /**
162
+ * create a middleware for request logging
163
+ * @returns {Function} Hono middleware function that logs requests
164
+ */
165
+ export function createLoggerMiddleware() {
166
+ return async (c, next) => {
167
+ const start = Date.now();
168
+ const method = c.req.method;
169
+ const url = c.req.url;
170
+ await next();
171
+ const duration = Date.now() - start;
172
+ const status = c.res.status;
173
+ console.log(`[${new Date().toISOString()}] ${method} ${url} ${status} ${duration}ms`);
174
+ };
175
+ }
176
+
177
+ /**
178
+ * create a middleware for rate limiting
179
+ * @param {Object} [options] - Rate limit configuration
180
+ * @param {number} [options.windowMs] - Time window in milliseconds
181
+ * @param {number} [options.max] - Maximum requests per window
182
+ * @param {Function} [options.keyGenerator] - Function to generate rate limit key
183
+ * @returns {Function} Hono middleware function that enforces rate limits
184
+ */
185
+ export function createRateLimitMiddleware(options = {}) {
186
+ const {
187
+ windowMs = 60000, // 1 minute
188
+ max = 60, // 60 requests per window
189
+ keyGenerator = (c) => c.req.header('x-forwarded-for') || c.req.header('x-real-ip') || 'unknown',
190
+ } = options;
191
+
192
+ const requests = new Map();
193
+
194
+ return async (c, next) => {
195
+ const key = keyGenerator(c);
196
+ const now = Date.now();
197
+
198
+ // clean old entries
199
+ for (const [k, data] of requests.entries()) if (now - data.resetTime > windowMs) requests.delete(k);
200
+
201
+ // get or create entry
202
+ let entry = requests.get(key);
203
+ if (!entry || now - entry.resetTime > windowMs) {
204
+ entry = { count: 0, resetTime: now };
205
+ requests.set(key, entry);
206
+ }
207
+
208
+ entry.count++;
209
+
210
+ // check limit
211
+ if (entry.count > max) return c.text('Too Many Requests', 429);
212
+
213
+ // rate limit headers
214
+ c.header('X-RateLimit-Limit', max.toString());
215
+ c.header('X-RateLimit-Remaining', (max - entry.count).toString());
216
+ c.header('X-RateLimit-Reset', new Date(entry.resetTime + windowMs).toISOString());
217
+
218
+ await next();
219
+ };
220
+ }
221
+
222
+ /**
223
+ * create error response helper
224
+ * @param {Object} c - Hono context
225
+ * @param {Error} error - Error object
226
+ * @param {number} [status=500] - HTTP status code
227
+ * @returns {Response} JSON error response
228
+ */
229
+ export function errorResponse(c, error, status = 500) {
230
+ console.error('Error:', error);
231
+ const isDev = globalThis.__ENVIRONMENT__ === 'development';
232
+ return c.json(
233
+ {
234
+ error: {
235
+ message: error.message || 'Internal Server Error',
236
+ status,
237
+ ...(isDev && { stack: error.stack }),
238
+ },
239
+ },
240
+ status
241
+ );
242
+ }
243
+
244
+ /**
245
+ * create success response helper
246
+ * @param {Object} c - Hono context
247
+ * @param {*} data - Response data
248
+ * @param {number} [status=200] - HTTP status code
249
+ * @returns {Response} JSON success response
250
+ */
251
+ export function successResponse(c, data, status = 200) {
252
+ return c.json(
253
+ {
254
+ success: true,
255
+ data,
256
+ },
257
+ status
258
+ );
259
+ }
260
+
261
+ /**
262
+ * redirect helper
263
+ * @param {Object} c - Hono context
264
+ * @param {string} location - Redirect URL
265
+ * @param {number} [status=302] - HTTP status code
266
+ * @returns {Response} Redirect response
267
+ */
268
+ export function redirect(c, location, status = 302) {
269
+ return c.redirect(location, status);
270
+ }
271
+
272
+ /**
273
+ * stream HTML response
274
+ * @param {Object} c - Hono context
275
+ * @param {Function} generator - Async generator function that yields HTML chunks
276
+ * @returns {Promise<Response>} Streaming HTML response
277
+ */
278
+ export async function streamHTML(c, generator) {
279
+ const stream = new ReadableStream({
280
+ async start(controller) {
281
+ controller.enqueue(new TextEncoder().encode('<!DOCTYPE html>'));
282
+ for await (const chunk of generator()) controller.enqueue(new TextEncoder().encode(chunk));
283
+ controller.close();
284
+ },
285
+ });
286
+ return new Response(stream, {
287
+ headers: {
288
+ 'Content-Type': 'text/html; charset=utf-8',
289
+ 'Transfer-Encoding': 'chunked',
290
+ },
291
+ });
292
+ }