@coderbuzz/ken 0.1.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/dist/index.js ADDED
@@ -0,0 +1,2728 @@
1
+ import {
2
+ getPathname
3
+ } from "./chunk-2BOPD5H7.js";
4
+ import {
5
+ Router,
6
+ WsReadyState,
7
+ WsTopicHub,
8
+ defaultErrorHandler
9
+ } from "./chunk-DPU3PBLP.js";
10
+
11
+ // src/core/app.ts
12
+ var App = class _App extends Router {
13
+ // Middleware patterns
14
+ middleware = [];
15
+ /** App-level error handler */
16
+ _onError;
17
+ /** Not-found handler for this app */
18
+ _notFoundHandler;
19
+ /** Accumulated not-found entries from child apps (via .use() and .define()) */
20
+ _notFoundEntries = [];
21
+ /** WebSocket route registrations */
22
+ wsRoutes = [];
23
+ /**
24
+ * Set app-level error handler.
25
+ * Called when route handlers or middleware throw errors.
26
+ * Route-level onError (in schema) takes priority over app-level.
27
+ *
28
+ * @param handler - Error handler function
29
+ *
30
+ * @example
31
+ * ```ts
32
+ * app.onError((error, ctx) => {
33
+ * console.error(ctx.method, ctx.url, error);
34
+ * return Response.json(
35
+ * { status: 500, message: error instanceof Error ? error.message : 'Internal Server Error' },
36
+ * { status: 500 }
37
+ * );
38
+ * });
39
+ * ```
40
+ */
41
+ onError(handler) {
42
+ this._onError = handler;
43
+ }
44
+ /**
45
+ * Set custom 404 Not Found handler.
46
+ * Called when no route matches the request.
47
+ * Supports nested configuration via .use() prefix scoping.
48
+ *
49
+ * @param handler - Handler function that receives Context
50
+ *
51
+ * @example
52
+ * ```ts
53
+ * // App-level
54
+ * app.notFound((ctx) => {
55
+ * return Response.json({ error: 'Not Found', path: ctx.url }, { status: 404 });
56
+ * });
57
+ *
58
+ * // Sub-app with prefix scoping
59
+ * const api = new App();
60
+ * api.notFound((ctx) => Response.json({ error: 'API Not Found' }, { status: 404 }));
61
+ * app.use('/api', api);
62
+ *
63
+ * // Define scope with middleware state
64
+ * app.define({ auth: (ctx) => verifyAuth(ctx) }, (app) => {
65
+ * app.notFound((ctx) => Response.json({ error: 'Protected', user: ctx.state.auth }, { status: 404 }));
66
+ * });
67
+ * ```
68
+ */
69
+ notFound(handler) {
70
+ this._notFoundHandler = handler;
71
+ }
72
+ apply(pattern = "/*", stateOrHandler) {
73
+ if (typeof stateOrHandler === "function") {
74
+ const randomKey = `_mw_${Math.random().toString(36).substring(2, 11)}`;
75
+ this.middleware.push({ pattern, state: { [randomKey]: stateOrHandler } });
76
+ } else {
77
+ this.middleware.push({ pattern, state: stateOrHandler });
78
+ }
79
+ }
80
+ /**
81
+ * Define middleware with lexical scoping and automatic type inference.
82
+ * Routes registered in the callback inherit middleware state types.
83
+ *
84
+ * @param state - State middleware object
85
+ * @param callback - Callback that receives scoped app with accumulated state types
86
+ */
87
+ define(state, callback) {
88
+ const childApp = new _App();
89
+ childApp.middleware = [...this.middleware];
90
+ childApp.apply("/*", state);
91
+ callback(childApp);
92
+ for (const route of childApp.routes) {
93
+ const matchedMiddleware = childApp.matchMiddleware(route.path);
94
+ const mergedSchema = childApp.mergeSchemas(matchedMiddleware, route.schema);
95
+ this.routes.push({
96
+ ...route,
97
+ schema: mergedSchema
98
+ });
99
+ }
100
+ if (childApp._notFoundHandler) {
101
+ const nfMiddleware = childApp.matchMiddleware("/*");
102
+ const nfSchema = childApp.mergeSchemas(nfMiddleware, void 0);
103
+ this._notFoundEntries.push({
104
+ prefix: "",
105
+ handler: childApp._notFoundHandler,
106
+ schema: nfSchema
107
+ });
108
+ }
109
+ for (const entry of childApp._notFoundEntries) {
110
+ this._notFoundEntries.push(entry);
111
+ }
112
+ }
113
+ /**
114
+ * Match middleware patterns against route path.
115
+ */
116
+ matchMiddleware(path) {
117
+ const matched = [];
118
+ for (const { pattern, state } of this.middleware) {
119
+ if (this.matchPattern(pattern, path)) {
120
+ matched.push(state);
121
+ }
122
+ }
123
+ return matched;
124
+ }
125
+ /**
126
+ * Simple wildcard pattern matching.
127
+ */
128
+ matchPattern(pattern, path) {
129
+ if (pattern === "/*") return true;
130
+ if (pattern.endsWith("/*")) {
131
+ const prefix = pattern.slice(0, -2);
132
+ return path === prefix || path.startsWith(prefix + "/");
133
+ }
134
+ return pattern === path;
135
+ }
136
+ /**
137
+ * Merge multiple middleware states with route schema.
138
+ */
139
+ mergeSchemas(middlewareStates, routeSchema) {
140
+ if (middlewareStates.length === 0 && !routeSchema?.state) {
141
+ if (this._onError && (!routeSchema || !routeSchema.onError)) {
142
+ return { ...routeSchema, onError: this._onError };
143
+ }
144
+ return routeSchema;
145
+ }
146
+ const mergedState = {};
147
+ for (const mwState of middlewareStates) {
148
+ Object.assign(mergedState, mwState);
149
+ }
150
+ if (routeSchema?.state) {
151
+ Object.assign(mergedState, routeSchema.state);
152
+ }
153
+ const result = {
154
+ ...routeSchema,
155
+ state: mergedState
156
+ };
157
+ if (this._onError && !result.onError) {
158
+ result.onError = this._onError;
159
+ }
160
+ return result;
161
+ }
162
+ /**
163
+ * Store raw route registration
164
+ */
165
+ storeRoute(method, path, schema, handler, staticValue) {
166
+ this.routes.push({
167
+ method,
168
+ path,
169
+ schema,
170
+ handler,
171
+ staticValue
172
+ });
173
+ }
174
+ /**
175
+ * Internal route registration logic
176
+ */
177
+ register(method, path, arg2, arg3) {
178
+ if (typeof arg2 === "function") {
179
+ this.storeRoute(method, path, void 0, arg2, void 0);
180
+ return;
181
+ }
182
+ if (arg2 !== null && typeof arg2 === "object" && arg3 !== void 0) {
183
+ if (typeof arg3 === "function") {
184
+ this.storeRoute(method, path, arg2, arg3, void 0);
185
+ } else {
186
+ this.storeRoute(method, path, arg2, void 0, arg3);
187
+ }
188
+ return;
189
+ }
190
+ this.storeRoute(method, path, void 0, void 0, arg2);
191
+ }
192
+ get(arg1, arg2, arg3) {
193
+ this.register("GET", arg1, arg2, arg3);
194
+ }
195
+ post(arg1, arg2, arg3) {
196
+ this.register("POST", arg1, arg2, arg3);
197
+ }
198
+ put(arg1, arg2, arg3) {
199
+ this.register("PUT", arg1, arg2, arg3);
200
+ }
201
+ patch(arg1, arg2, arg3) {
202
+ this.register("PATCH", arg1, arg2, arg3);
203
+ }
204
+ delete(arg1, arg2, arg3) {
205
+ this.register("DELETE", arg1, arg2, arg3);
206
+ }
207
+ head(arg1, arg2, arg3) {
208
+ this.register("HEAD", arg1, arg2, arg3);
209
+ }
210
+ options(arg1, arg2, arg3) {
211
+ this.register("OPTIONS", arg1, arg2, arg3);
212
+ }
213
+ use(arg1, arg2) {
214
+ const prefix = typeof arg1 === "string" ? arg1 : "";
215
+ const other = typeof arg1 === "string" ? arg2 : arg1;
216
+ for (const mw of other.middleware) {
217
+ const combinedPattern = this.combinePath(prefix, mw.pattern);
218
+ this.middleware.push({
219
+ pattern: combinedPattern,
220
+ state: mw.state
221
+ });
222
+ }
223
+ for (const route of other.routes) {
224
+ const combinedPath = this.combinePath(prefix, route.path);
225
+ let schema = route.schema;
226
+ if (other._onError && (!schema || !schema.onError)) {
227
+ schema = { ...schema, onError: other._onError };
228
+ }
229
+ this.routes.push({
230
+ ...route,
231
+ path: combinedPath,
232
+ schema
233
+ });
234
+ }
235
+ if (other._notFoundHandler) {
236
+ const schema = other._onError ? { onError: other._onError } : void 0;
237
+ this._notFoundEntries.push({
238
+ prefix: prefix || "",
239
+ handler: other._notFoundHandler,
240
+ schema
241
+ });
242
+ }
243
+ for (const entry of other._notFoundEntries) {
244
+ let schema = entry.schema;
245
+ if (other._onError && (!schema || !schema.onError)) {
246
+ schema = { ...schema, onError: other._onError };
247
+ }
248
+ this._notFoundEntries.push({
249
+ prefix: this.combinePath(prefix, entry.prefix),
250
+ handler: entry.handler,
251
+ schema
252
+ });
253
+ }
254
+ for (const wsRoute of other.wsRoutes) {
255
+ this.wsRoutes.push({
256
+ ...wsRoute,
257
+ path: this.combinePath(prefix, wsRoute.path)
258
+ });
259
+ }
260
+ }
261
+ /**
262
+ * Register a WebSocket handler at the given path.
263
+ *
264
+ * The generic type parameter `T` defines per-connection data,
265
+ * which is set in the `upgrade` handler and accessible via `peer.data`.
266
+ *
267
+ * @template T - Per-connection data type
268
+ * @param path - URL path to handle WebSocket upgrades
269
+ * @param handler - WebSocket event handlers
270
+ *
271
+ * @example
272
+ * ```ts
273
+ * // Simple echo server
274
+ * app.ws('/ws', {
275
+ * message(peer, message) {
276
+ * peer.send(message);
277
+ * },
278
+ * });
279
+ *
280
+ * // Chat with authentication and pub/sub
281
+ * app.ws<{ userId: string }>('/chat', {
282
+ * upgrade(req) {
283
+ * const token = req.headers.get('authorization');
284
+ * return { userId: verifyToken(token) };
285
+ * },
286
+ * open(peer) {
287
+ * peer.subscribe('chat');
288
+ * peer.publish('chat', `${peer.data.userId} joined`);
289
+ * },
290
+ * message(peer, message) {
291
+ * peer.publish('chat', `${peer.data.userId}: ${message}`);
292
+ * },
293
+ * close(peer) {
294
+ * peer.publish('chat', `${peer.data.userId} left`);
295
+ * },
296
+ * });
297
+ *
298
+ * // With options
299
+ * app.ws('/live', {
300
+ * message(peer, message) { peer.send(message); },
301
+ * }, {
302
+ * pingInterval: 15,
303
+ * maxPayloadLength: 1024 * 1024,
304
+ * });
305
+ * ```
306
+ */
307
+ ws(path, handler, options) {
308
+ this.wsRoutes.push({ path, handler, options });
309
+ }
310
+ /**
311
+ * Print all registered routes to the console with colored formatting.
312
+ *
313
+ * Each HTTP method is color-coded:
314
+ * - GET (green), POST (blue), PUT (yellow), PATCH (cyan)
315
+ * - DELETE (red), HEAD (magenta), OPTIONS (white)
316
+ *
317
+ * @example
318
+ * ```ts
319
+ * app.get('/', 'Hello!');
320
+ * app.post('/users', handler);
321
+ * app.get('/users/:id', handler);
322
+ * app.ws('/chat', wsHandler);
323
+ * app.printRoutes();
324
+ * // ┌──────────┬────────────────────┐
325
+ * // │ Method │ Path │
326
+ * // ├──────────┼────────────────────┤
327
+ * // │ GET │ / │
328
+ * // │ POST │ /users │
329
+ * // │ GET │ /users/:id │
330
+ * // │ WS │ /chat │
331
+ * // └──────────┴────────────────────┘
332
+ * ```
333
+ */
334
+ printRoutes() {
335
+ const routes = this.getRoutes();
336
+ const hasHttp = routes.length > 0;
337
+ const hasWs = this.wsRoutes.length > 0;
338
+ if (!hasHttp && !hasWs) {
339
+ console.log("No routes registered.");
340
+ return;
341
+ }
342
+ const reset = "\x1B[0m";
343
+ const bold = "\x1B[1m";
344
+ const dim = "\x1B[2m";
345
+ const methodColors = {
346
+ GET: "\x1B[32m",
347
+ // green
348
+ POST: "\x1B[34m",
349
+ // blue
350
+ PUT: "\x1B[33m",
351
+ // yellow
352
+ PATCH: "\x1B[36m",
353
+ // cyan
354
+ DELETE: "\x1B[31m",
355
+ // red
356
+ HEAD: "\x1B[35m",
357
+ // magenta
358
+ OPTIONS: "\x1B[37m",
359
+ // white
360
+ WS: "\x1B[35m"
361
+ // magenta
362
+ };
363
+ const allRows = [
364
+ ...routes.map((r) => ({ method: r.method, path: r.path })),
365
+ ...this.wsRoutes.map((r) => ({ method: "WS", path: r.path }))
366
+ ];
367
+ const methodInnerWidth = Math.max(
368
+ "Method".length,
369
+ allRows.reduce((max, row) => Math.max(max, row.method.length), 0)
370
+ );
371
+ const pathInnerWidth = Math.max(
372
+ "Path".length,
373
+ allRows.reduce((max, row) => Math.max(max, row.path.length), 0)
374
+ );
375
+ const methodCellWidth = methodInnerWidth + 2;
376
+ const pathCellWidth = pathInnerWidth + 2;
377
+ const top = `${dim}\u250C${"\u2500".repeat(methodCellWidth)}\u252C${"\u2500".repeat(pathCellWidth)}\u2510${reset}`;
378
+ const headerSep = `${dim}\u251C${"\u2500".repeat(methodCellWidth)}\u253C${"\u2500".repeat(pathCellWidth)}\u2524${reset}`;
379
+ const bottom = `${dim}\u2514${"\u2500".repeat(methodCellWidth)}\u2534${"\u2500".repeat(pathCellWidth)}\u2518${reset}`;
380
+ const header = `${dim}\u2502${reset} ${bold}${"Method".padEnd(methodInnerWidth)}${reset} ${dim}\u2502${reset} ${bold}${"Path".padEnd(pathInnerWidth)}${reset} ${dim}\u2502${reset}`;
381
+ const lines = [top, header, headerSep];
382
+ for (const row of allRows) {
383
+ const color = methodColors[row.method] || "";
384
+ const method = `${color}${bold}${row.method}${reset}`;
385
+ const methodPad = " ".repeat(methodInnerWidth - row.method.length);
386
+ const pathPad = " ".repeat(pathInnerWidth - row.path.length);
387
+ lines.push(
388
+ `${dim}\u2502${reset} ${method}${methodPad} ${dim}\u2502${reset} ${row.path}${pathPad} ${dim}\u2502${reset}`
389
+ );
390
+ }
391
+ lines.push(bottom);
392
+ console.log(lines.join("\n"));
393
+ }
394
+ /**
395
+ * Combine prefix with path
396
+ */
397
+ combinePath(prefix, path) {
398
+ if (!prefix) return path;
399
+ if (!path || path === "/") return prefix;
400
+ let combined = prefix.endsWith("/") ? prefix : prefix + "/";
401
+ combined += path.startsWith("/") ? path.substring(1) : path;
402
+ if (!combined.startsWith("/")) combined = "/" + combined;
403
+ return combined;
404
+ }
405
+ };
406
+
407
+ // src/runtime/index.ts
408
+ var isDeno = typeof Deno !== "undefined" && typeof Deno.version !== "undefined";
409
+ var isBun = typeof Bun !== "undefined" && typeof Bun.version !== "undefined";
410
+ var isNode = !isBun && typeof globalThis.process !== "undefined" && globalThis.process.versions != null && globalThis.process.versions.node != null;
411
+ async function resolveNodeRuntime() {
412
+ const flag = globalThis.process?.env?.UWS;
413
+ console.log("UWS flag:", flag);
414
+ if (flag === "1" || flag === "true") {
415
+ try {
416
+ await import("uWebSockets.js");
417
+ return (await import("./uws-VNY2LPIZ.js")).server;
418
+ } catch {
419
+ throw new Error(
420
+ "UWS is enabled but uWebSockets.js is not installed. Install with: npm install uWebSockets.js"
421
+ );
422
+ }
423
+ }
424
+ return (await import("./node-JLUTIPEN.js")).server;
425
+ }
426
+ async function server(options) {
427
+ if (isDeno) {
428
+ const { server: server2 } = await import("./deno-LZU5JBGL.js");
429
+ return server2(options);
430
+ }
431
+ if (isBun) {
432
+ const { server: server2 } = await import("./bun-GUK26ACN.js");
433
+ return server2(options);
434
+ }
435
+ if (isNode) {
436
+ const factory = await resolveNodeRuntime();
437
+ return factory(options);
438
+ }
439
+ throw new Error(
440
+ "Unsupported runtime. Ken supports Bun, Deno, and Node.js (with optional uWebSockets.js)."
441
+ );
442
+ }
443
+
444
+ // src/core/server.ts
445
+ var AppServer = class extends App {
446
+ /** Configured hostname */
447
+ hostname;
448
+ /** Configured port */
449
+ port;
450
+ _server;
451
+ constructor(init = {}) {
452
+ super();
453
+ this.hostname = init.hostname ?? "0.0.0.0";
454
+ this.port = init.port ?? 3e3;
455
+ }
456
+ /**
457
+ * Start the server with automatic runtime detection.
458
+ * Returns the resolved hostname and port.
459
+ *
460
+ * @example
461
+ * ```ts
462
+ * const app = new AppServer({ port: 3000 });
463
+ * app.get('/', 'Hello!');
464
+ * const { hostname, port } = await app.run();
465
+ * console.log(`Listening on ${hostname}:${port}`);
466
+ * ```
467
+ */
468
+ async run() {
469
+ await this.stop();
470
+ this._server = await server({
471
+ port: this.port,
472
+ hostname: this.hostname,
473
+ router: this
474
+ });
475
+ return this._server.run();
476
+ }
477
+ /**
478
+ * Stop the server gracefully.
479
+ */
480
+ async stop() {
481
+ if (this._server) {
482
+ await this._server.stop();
483
+ this._server = void 0;
484
+ }
485
+ }
486
+ };
487
+
488
+ // src/utils/encoding.ts
489
+ var encoder = new TextEncoder();
490
+ var decoder = new TextDecoder();
491
+ var B64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
492
+ var B64_LOOKUP = new Uint8Array(128);
493
+ for (let i = 0; i < B64_CHARS.length; i++) B64_LOOKUP[B64_CHARS.charCodeAt(i)] = i;
494
+ function toBase64(bytes) {
495
+ const len = bytes.length;
496
+ const pad = len % 3;
497
+ const mainLen = len - pad;
498
+ let result = "";
499
+ for (let i = 0; i < mainLen; i += 3) {
500
+ const b0 = bytes[i], b1 = bytes[i + 1], b2 = bytes[i + 2];
501
+ result += B64_CHARS[b0 >> 2] + B64_CHARS[(b0 & 3) << 4 | b1 >> 4] + B64_CHARS[(b1 & 15) << 2 | b2 >> 6] + B64_CHARS[b2 & 63];
502
+ }
503
+ if (pad === 1) {
504
+ const b0 = bytes[mainLen];
505
+ result += B64_CHARS[b0 >> 2] + B64_CHARS[(b0 & 3) << 4] + "==";
506
+ } else if (pad === 2) {
507
+ const b0 = bytes[mainLen], b1 = bytes[mainLen + 1];
508
+ result += B64_CHARS[b0 >> 2] + B64_CHARS[(b0 & 3) << 4 | b1 >> 4] + B64_CHARS[(b1 & 15) << 2] + "=";
509
+ }
510
+ return result;
511
+ }
512
+ function fromBase64(str) {
513
+ let len = str.length;
514
+ let pad = 0;
515
+ if (str.charCodeAt(len - 1) === 61) pad++;
516
+ if (str.charCodeAt(len - 2) === 61) pad++;
517
+ const byteLen = (len * 3 >> 2) - pad;
518
+ const out = new Uint8Array(byteLen);
519
+ let j = 0;
520
+ for (let i = 0; i < len; i += 4) {
521
+ const a = B64_LOOKUP[str.charCodeAt(i)];
522
+ const b = B64_LOOKUP[str.charCodeAt(i + 1)];
523
+ const c = B64_LOOKUP[str.charCodeAt(i + 2)];
524
+ const d = B64_LOOKUP[str.charCodeAt(i + 3)];
525
+ out[j++] = a << 2 | b >> 4;
526
+ if (j < byteLen) out[j++] = (b & 15) << 4 | c >> 2;
527
+ if (j < byteLen) out[j++] = (c & 3) << 6 | d;
528
+ }
529
+ return out;
530
+ }
531
+ var B64URL_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
532
+ var B64URL_LOOKUP = new Uint8Array(128);
533
+ for (let i = 0; i < B64URL_CHARS.length; i++) B64URL_LOOKUP[B64URL_CHARS.charCodeAt(i)] = i;
534
+ function encodeBase64Url(bytes) {
535
+ const len = bytes.length;
536
+ const rem = len % 3;
537
+ const mainLen = len - rem;
538
+ let result = "";
539
+ for (let i = 0; i < mainLen; i += 3) {
540
+ const b0 = bytes[i], b1 = bytes[i + 1], b2 = bytes[i + 2];
541
+ result += B64URL_CHARS[b0 >> 2] + B64URL_CHARS[(b0 & 3) << 4 | b1 >> 4] + B64URL_CHARS[(b1 & 15) << 2 | b2 >> 6] + B64URL_CHARS[b2 & 63];
542
+ }
543
+ if (rem === 1) {
544
+ const b0 = bytes[mainLen];
545
+ result += B64URL_CHARS[b0 >> 2] + B64URL_CHARS[(b0 & 3) << 4];
546
+ } else if (rem === 2) {
547
+ const b0 = bytes[mainLen], b1 = bytes[mainLen + 1];
548
+ result += B64URL_CHARS[b0 >> 2] + B64URL_CHARS[(b0 & 3) << 4 | b1 >> 4] + B64URL_CHARS[(b1 & 15) << 2];
549
+ }
550
+ return result;
551
+ }
552
+ function decodeBase64Url(str) {
553
+ const len = str.length;
554
+ const rem = len & 3;
555
+ const fullGroups = len - rem >> 2;
556
+ const byteLen = fullGroups * 3 + (rem === 2 ? 1 : rem === 3 ? 2 : 0);
557
+ const out = new Uint8Array(byteLen);
558
+ const mainLen = fullGroups << 2;
559
+ let j = 0;
560
+ for (let i = 0; i < mainLen; i += 4) {
561
+ const a = B64URL_LOOKUP[str.charCodeAt(i)];
562
+ const b = B64URL_LOOKUP[str.charCodeAt(i + 1)];
563
+ const c = B64URL_LOOKUP[str.charCodeAt(i + 2)];
564
+ const d = B64URL_LOOKUP[str.charCodeAt(i + 3)];
565
+ out[j++] = a << 2 | b >> 4;
566
+ out[j++] = (b & 15) << 4 | c >> 2;
567
+ out[j++] = (c & 3) << 6 | d;
568
+ }
569
+ if (rem === 2) {
570
+ const a = B64URL_LOOKUP[str.charCodeAt(mainLen)];
571
+ const b = B64URL_LOOKUP[str.charCodeAt(mainLen + 1)];
572
+ out[j] = a << 2 | b >> 4;
573
+ } else if (rem === 3) {
574
+ const a = B64URL_LOOKUP[str.charCodeAt(mainLen)];
575
+ const b = B64URL_LOOKUP[str.charCodeAt(mainLen + 1)];
576
+ const c = B64URL_LOOKUP[str.charCodeAt(mainLen + 2)];
577
+ out[j++] = a << 2 | b >> 4;
578
+ out[j] = (b & 15) << 4 | c >> 2;
579
+ }
580
+ return out;
581
+ }
582
+
583
+ // src/utils/encryption.ts
584
+ var AES_GCM = { name: "AES-GCM", length: 256 };
585
+ var KEY_CACHE_MAX = 64;
586
+ var keyCache = /* @__PURE__ */ new Map();
587
+ async function deriveCryptoKey(password) {
588
+ const cached = keyCache.get(password);
589
+ if (cached !== void 0) return cached;
590
+ const keyData = await crypto.subtle.digest("SHA-256", encoder.encode(password));
591
+ const key = await crypto.subtle.importKey("raw", keyData, AES_GCM, false, ["encrypt", "decrypt"]);
592
+ if (keyCache.size >= KEY_CACHE_MAX) {
593
+ const oldest = keyCache.keys().next().value;
594
+ keyCache.delete(oldest);
595
+ }
596
+ keyCache.set(password, key);
597
+ return key;
598
+ }
599
+ function generateSecretKey() {
600
+ return toBase64(crypto.getRandomValues(new Uint8Array(32)));
601
+ }
602
+ async function encryptString(value, password) {
603
+ const key = await deriveCryptoKey(password);
604
+ const iv = crypto.getRandomValues(new Uint8Array(12));
605
+ const ciphertext = await crypto.subtle.encrypt(
606
+ { name: "AES-GCM", iv },
607
+ key,
608
+ encoder.encode(value)
609
+ );
610
+ const ctBytes = new Uint8Array(ciphertext);
611
+ const combined = new Uint8Array(12 + ctBytes.byteLength);
612
+ combined.set(iv, 0);
613
+ combined.set(ctBytes, 12);
614
+ return toBase64(combined);
615
+ }
616
+ async function decryptString(encrypted, password) {
617
+ const key = await deriveCryptoKey(password);
618
+ const combined = fromBase64(encrypted);
619
+ const iv = combined.subarray(0, 12);
620
+ const ciphertext = combined.subarray(12);
621
+ const decrypted = await crypto.subtle.decrypt(
622
+ { name: "AES-GCM", iv },
623
+ key,
624
+ ciphertext
625
+ );
626
+ return decoder.decode(decrypted);
627
+ }
628
+
629
+ // src/utils/memoize.ts
630
+ function memoize(fn, options) {
631
+ if (fn.constructor.name === "AsyncFunction") {
632
+ return createAsyncMemoized(fn, options);
633
+ }
634
+ return createSyncMemoized(fn, options);
635
+ }
636
+ function createSyncMemoized(fn, options) {
637
+ const maxSize = options?.maxSize ?? 256;
638
+ const ttl = options?.ttl ?? 0;
639
+ const keyFn = options?.key;
640
+ const cache2 = /* @__PURE__ */ new Map();
641
+ const memoized = (...args) => {
642
+ const k = keyFn ? keyFn(...args) : args[0];
643
+ const entry = cache2.get(k);
644
+ if (entry !== void 0) {
645
+ if (entry.expiry !== 0 && Date.now() > entry.expiry) {
646
+ cache2.delete(k);
647
+ } else {
648
+ return entry.value;
649
+ }
650
+ }
651
+ const result = fn(...args);
652
+ if (cache2.size >= maxSize) {
653
+ const oldest = cache2.keys().next().value;
654
+ cache2.delete(oldest);
655
+ }
656
+ cache2.set(k, {
657
+ value: result,
658
+ expiry: ttl > 0 ? Date.now() + ttl : 0
659
+ });
660
+ return result;
661
+ };
662
+ memoized.cache = cache2;
663
+ memoized.clear = () => cache2.clear();
664
+ return memoized;
665
+ }
666
+ function createAsyncMemoized(fn, options) {
667
+ const maxSize = options?.maxSize ?? 256;
668
+ const ttl = options?.ttl ?? 0;
669
+ const keyFn = options?.key;
670
+ const cache2 = /* @__PURE__ */ new Map();
671
+ const inflight = /* @__PURE__ */ new Map();
672
+ const memoized = (...args) => {
673
+ const k = keyFn ? keyFn(...args) : args[0];
674
+ const entry = cache2.get(k);
675
+ if (entry !== void 0) {
676
+ if (entry.expiry !== 0 && Date.now() > entry.expiry) {
677
+ cache2.delete(k);
678
+ } else {
679
+ return Promise.resolve(entry.value);
680
+ }
681
+ }
682
+ const pending = inflight.get(k);
683
+ if (pending !== void 0) return pending;
684
+ const promise = fn(...args).then((result) => {
685
+ inflight.delete(k);
686
+ if (cache2.size >= maxSize) {
687
+ const oldest = cache2.keys().next().value;
688
+ cache2.delete(oldest);
689
+ }
690
+ cache2.set(k, {
691
+ value: result,
692
+ expiry: ttl > 0 ? Date.now() + ttl : 0
693
+ });
694
+ return result;
695
+ }, (err) => {
696
+ inflight.delete(k);
697
+ throw err;
698
+ });
699
+ inflight.set(k, promise);
700
+ return promise;
701
+ };
702
+ memoized.cache = cache2;
703
+ memoized.inflight = inflight;
704
+ memoized.clear = () => {
705
+ cache2.clear();
706
+ inflight.clear();
707
+ };
708
+ return memoized;
709
+ }
710
+
711
+ // src/utils/compress.ts
712
+ async function readAllBytes(readable) {
713
+ const reader = readable.getReader();
714
+ const chunks = [];
715
+ let totalLen = 0;
716
+ while (true) {
717
+ const { done, value } = await reader.read();
718
+ if (done) break;
719
+ chunks.push(value);
720
+ totalLen += value.byteLength;
721
+ }
722
+ if (chunks.length === 1) return chunks[0];
723
+ const result = new Uint8Array(totalLen);
724
+ let offset = 0;
725
+ for (let i = 0; i < chunks.length; i++) {
726
+ result.set(chunks[i], offset);
727
+ offset += chunks[i].byteLength;
728
+ }
729
+ return result;
730
+ }
731
+ async function compressString(input, options) {
732
+ const encoding = options?.encoding ?? "brotli";
733
+ const cs = options?.level !== void 0 ? new CompressionStream(encoding, { level: options.level }) : new CompressionStream(encoding);
734
+ const writer = cs.writable.getWriter();
735
+ writer.write(encoder.encode(input)).catch(() => {
736
+ });
737
+ writer.close().catch(() => {
738
+ });
739
+ return encodeBase64Url(await readAllBytes(cs.readable));
740
+ }
741
+ async function decompressString(compressed, options) {
742
+ const encoding = options?.encoding ?? "brotli";
743
+ const ds = new DecompressionStream(encoding);
744
+ const writer = ds.writable.getWriter();
745
+ writer.write(decodeBase64Url(compressed)).catch(() => {
746
+ });
747
+ writer.close().catch(() => {
748
+ });
749
+ return decoder.decode(await readAllBytes(ds.readable));
750
+ }
751
+
752
+ // src/utils/file.ts
753
+ import { stat, readdir, writeFile, mkdir } from "fs/promises";
754
+ import { createReadStream } from "fs";
755
+ import { join, extname, dirname } from "path";
756
+ var _isBun = typeof globalThis.Bun !== "undefined" && typeof globalThis.Bun.version !== "undefined";
757
+ var MIME_TYPES = Object.freeze({
758
+ // Text
759
+ ".html": "text/html; charset=utf-8",
760
+ ".htm": "text/html; charset=utf-8",
761
+ ".css": "text/css; charset=utf-8",
762
+ ".js": "text/javascript; charset=utf-8",
763
+ ".mjs": "text/javascript; charset=utf-8",
764
+ ".json": "application/json; charset=utf-8",
765
+ ".xml": "application/xml; charset=utf-8",
766
+ ".txt": "text/plain; charset=utf-8",
767
+ ".csv": "text/csv; charset=utf-8",
768
+ ".md": "text/markdown; charset=utf-8",
769
+ ".yaml": "text/yaml; charset=utf-8",
770
+ ".yml": "text/yaml; charset=utf-8",
771
+ ".svg": "image/svg+xml",
772
+ ".ts": "text/typescript; charset=utf-8",
773
+ ".tsx": "text/tsx; charset=utf-8",
774
+ ".jsx": "text/jsx; charset=utf-8",
775
+ // Images
776
+ ".png": "image/png",
777
+ ".jpg": "image/jpeg",
778
+ ".jpeg": "image/jpeg",
779
+ ".gif": "image/gif",
780
+ ".webp": "image/webp",
781
+ ".ico": "image/x-icon",
782
+ ".avif": "image/avif",
783
+ ".bmp": "image/bmp",
784
+ ".tiff": "image/tiff",
785
+ ".tif": "image/tiff",
786
+ // Fonts
787
+ ".woff": "font/woff",
788
+ ".woff2": "font/woff2",
789
+ ".ttf": "font/ttf",
790
+ ".otf": "font/otf",
791
+ ".eot": "application/vnd.ms-fontobject",
792
+ // Audio / Video
793
+ ".mp3": "audio/mpeg",
794
+ ".mp4": "video/mp4",
795
+ ".webm": "video/webm",
796
+ ".ogg": "audio/ogg",
797
+ ".ogv": "video/ogg",
798
+ ".wav": "audio/wav",
799
+ ".flac": "audio/flac",
800
+ ".aac": "audio/aac",
801
+ ".m4a": "audio/mp4",
802
+ ".m4v": "video/mp4",
803
+ ".avi": "video/x-msvideo",
804
+ ".mov": "video/quicktime",
805
+ ".mkv": "video/x-matroska",
806
+ // Archives
807
+ ".zip": "application/zip",
808
+ ".gz": "application/gzip",
809
+ ".tar": "application/x-tar",
810
+ ".bz2": "application/x-bzip2",
811
+ ".7z": "application/x-7z-compressed",
812
+ ".rar": "application/vnd.rar",
813
+ // Documents
814
+ ".pdf": "application/pdf",
815
+ ".doc": "application/msword",
816
+ ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
817
+ ".xls": "application/vnd.ms-excel",
818
+ ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
819
+ ".ppt": "application/vnd.ms-powerpoint",
820
+ ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
821
+ // Other
822
+ ".wasm": "application/wasm",
823
+ ".map": "application/json",
824
+ ".bin": "application/octet-stream"
825
+ });
826
+ function getMimeType(filePath) {
827
+ return MIME_TYPES[extname(filePath).toLowerCase()] || "application/octet-stream";
828
+ }
829
+ function getHeader(headers, key) {
830
+ if (!headers) return null;
831
+ if (typeof headers.get === "function") return headers.get(key);
832
+ return headers[key] ?? null;
833
+ }
834
+ function parseRange(header, size) {
835
+ if (header.charCodeAt(0) !== 98 || !header.startsWith("bytes=")) return null;
836
+ const range = header.slice(6);
837
+ const comma = range.indexOf(",");
838
+ const single = comma === -1 ? range : range.slice(0, comma);
839
+ const dash = single.indexOf("-");
840
+ if (dash === -1) return null;
841
+ const startStr = single.slice(0, dash).trim();
842
+ const endStr = single.slice(dash + 1).trim();
843
+ let start;
844
+ let end;
845
+ if (startStr === "") {
846
+ const suffix = parseInt(endStr, 10);
847
+ if (isNaN(suffix) || suffix <= 0) return null;
848
+ start = Math.max(0, size - suffix);
849
+ end = size - 1;
850
+ } else {
851
+ start = parseInt(startStr, 10);
852
+ if (isNaN(start) || start < 0) return null;
853
+ end = endStr === "" ? size - 1 : parseInt(endStr, 10);
854
+ if (isNaN(end)) return null;
855
+ }
856
+ if (start > end || start >= size) return null;
857
+ end = Math.min(end, size - 1);
858
+ return { start, end };
859
+ }
860
+ function nodeStreamToWeb(nodeStream) {
861
+ return new ReadableStream({
862
+ start(controller) {
863
+ nodeStream.on("data", (chunk) => {
864
+ controller.enqueue(chunk);
865
+ if (controller.desiredSize !== null && controller.desiredSize <= 0) {
866
+ nodeStream.pause();
867
+ }
868
+ });
869
+ nodeStream.on("end", () => controller.close());
870
+ nodeStream.on("error", (err) => controller.error(err));
871
+ },
872
+ pull() {
873
+ nodeStream.resume();
874
+ },
875
+ cancel() {
876
+ nodeStream.destroy?.();
877
+ }
878
+ });
879
+ }
880
+ function generateEtag(size, mtimeMs) {
881
+ return `W/"${size.toString(16)}-${Math.floor(mtimeMs).toString(16)}"`;
882
+ }
883
+ async function sendFile(filePath, options) {
884
+ let fileStats;
885
+ try {
886
+ fileStats = await stat(filePath);
887
+ } catch (err) {
888
+ if (err.code === "ENOENT" || err.code === "ENOTDIR") {
889
+ return new Response("Not Found", { status: 404 });
890
+ }
891
+ throw err;
892
+ }
893
+ if (!fileStats.isFile()) {
894
+ return new Response("Not Found", { status: 404 });
895
+ }
896
+ const size = fileStats.size;
897
+ const mtimeMs = fileStats.mtimeMs;
898
+ const etag2 = generateEtag(size, mtimeMs);
899
+ const lastModified = new Date(mtimeMs).toUTCString();
900
+ const contentType = options?.contentType || getMimeType(filePath);
901
+ const reqHeaders = options?.reqHeaders;
902
+ const conditionalHeaders = {
903
+ etag: etag2,
904
+ "last-modified": lastModified
905
+ };
906
+ if (options?.cacheControl) conditionalHeaders["cache-control"] = options.cacheControl;
907
+ const ifNoneMatch = getHeader(reqHeaders, "if-none-match");
908
+ if (ifNoneMatch) {
909
+ if (ifNoneMatch === etag2 || ifNoneMatch === "*") {
910
+ return new Response(null, { status: 304, headers: conditionalHeaders });
911
+ }
912
+ if (ifNoneMatch.indexOf(",") !== -1) {
913
+ const tags = ifNoneMatch.split(",");
914
+ for (let i = 0; i < tags.length; i++) {
915
+ if (tags[i].trim() === etag2) {
916
+ return new Response(null, { status: 304, headers: conditionalHeaders });
917
+ }
918
+ }
919
+ }
920
+ }
921
+ if (!ifNoneMatch) {
922
+ const ifModifiedSince = getHeader(reqHeaders, "if-modified-since");
923
+ if (ifModifiedSince) {
924
+ const clientTime = Date.parse(ifModifiedSince);
925
+ if (!isNaN(clientTime) && Math.floor(mtimeMs / 1e3) <= Math.floor(clientTime / 1e3)) {
926
+ return new Response(null, { status: 304, headers: conditionalHeaders });
927
+ }
928
+ }
929
+ }
930
+ const headers = {
931
+ "content-type": contentType,
932
+ etag: etag2,
933
+ "last-modified": lastModified,
934
+ "accept-ranges": "bytes"
935
+ };
936
+ if (options?.cacheControl) headers["cache-control"] = options.cacheControl;
937
+ if (options?.download) {
938
+ const filename = typeof options.download === "string" ? options.download : filePath.slice(Math.max(filePath.lastIndexOf("/"), filePath.lastIndexOf("\\")) + 1);
939
+ headers["content-disposition"] = `attachment; filename="${filename}"`;
940
+ }
941
+ if (options?.headers) {
942
+ for (const key in options.headers) {
943
+ headers[key] = options.headers[key];
944
+ }
945
+ }
946
+ const rangeHeader = getHeader(reqHeaders, "range");
947
+ if (rangeHeader) {
948
+ const range = parseRange(rangeHeader, size);
949
+ if (range) {
950
+ const { start, end } = range;
951
+ headers["content-range"] = `bytes ${start}-${end}/${size}`;
952
+ headers["content-length"] = String(end - start + 1);
953
+ let body2;
954
+ if (_isBun) {
955
+ body2 = globalThis.Bun.file(filePath).slice(start, end + 1);
956
+ } else {
957
+ body2 = nodeStreamToWeb(createReadStream(filePath, { start, end }));
958
+ }
959
+ return new Response(body2, { status: 206, headers });
960
+ }
961
+ return new Response(null, {
962
+ status: 416,
963
+ headers: { "content-range": `bytes */${size}` }
964
+ });
965
+ }
966
+ headers["content-length"] = String(size);
967
+ let body;
968
+ if (_isBun) {
969
+ body = globalThis.Bun.file(filePath);
970
+ } else {
971
+ body = nodeStreamToWeb(createReadStream(filePath));
972
+ }
973
+ return new Response(body, { status: options?.status || 200, headers });
974
+ }
975
+ async function listDirectory(dirPath, options) {
976
+ const includeStats = options?.stats !== false;
977
+ const recursive = options?.recursive === true;
978
+ const maxDepth = options?.maxDepth ?? 10;
979
+ const filter = options?.filter;
980
+ const entries = [];
981
+ const stack = [
982
+ { dir: dirPath, relativePath: "", depth: 0 }
983
+ ];
984
+ while (stack.length > 0) {
985
+ const { dir, relativePath, depth } = stack.pop();
986
+ let dirents;
987
+ try {
988
+ dirents = await readdir(dir, { withFileTypes: true });
989
+ } catch {
990
+ continue;
991
+ }
992
+ if (includeStats) {
993
+ const statResults = await Promise.all(
994
+ dirents.map(async (dirent) => {
995
+ try {
996
+ return await stat(join(dir, dirent.name));
997
+ } catch {
998
+ return null;
999
+ }
1000
+ })
1001
+ );
1002
+ for (let i = 0; i < dirents.length; i++) {
1003
+ const dirent = dirents[i];
1004
+ const s = statResults[i];
1005
+ const name = dirent.name;
1006
+ const entryPath = relativePath ? `${relativePath}/${name}` : name;
1007
+ const isDir = dirent.isDirectory();
1008
+ const entry = {
1009
+ name,
1010
+ path: entryPath,
1011
+ isDirectory: isDir,
1012
+ size: isDir ? 0 : s?.size ?? 0,
1013
+ modifiedAt: s ? s.mtime : /* @__PURE__ */ new Date(0)
1014
+ };
1015
+ if (!filter || filter(entry)) entries.push(entry);
1016
+ if (isDir && recursive && depth < maxDepth) {
1017
+ stack.push({ dir: join(dir, name), relativePath: entryPath, depth: depth + 1 });
1018
+ }
1019
+ }
1020
+ } else {
1021
+ for (const dirent of dirents) {
1022
+ const name = dirent.name;
1023
+ const entryPath = relativePath ? `${relativePath}/${name}` : name;
1024
+ const isDir = dirent.isDirectory();
1025
+ const entry = {
1026
+ name,
1027
+ path: entryPath,
1028
+ isDirectory: isDir,
1029
+ size: 0,
1030
+ modifiedAt: /* @__PURE__ */ new Date(0)
1031
+ };
1032
+ if (!filter || filter(entry)) entries.push(entry);
1033
+ if (isDir && recursive && depth < maxDepth) {
1034
+ stack.push({ dir: join(dir, name), relativePath: entryPath, depth: depth + 1 });
1035
+ }
1036
+ }
1037
+ }
1038
+ }
1039
+ return entries;
1040
+ }
1041
+ async function receiveFiles(formData, options) {
1042
+ const maxSize = options?.maxFileSize ?? Infinity;
1043
+ const maxCount = options?.maxFiles ?? Infinity;
1044
+ const allowedTypes = options?.allowedTypes;
1045
+ const fields = options?.fields;
1046
+ const files = [];
1047
+ for (const [key, value] of formData.entries()) {
1048
+ if (typeof value === "string") continue;
1049
+ const file = value;
1050
+ if (fields && !fields.includes(key)) continue;
1051
+ if (files.length >= maxCount) break;
1052
+ if (file.size > maxSize) {
1053
+ throw new Error(`File "${file.name}" exceeds maximum size of ${maxSize} bytes`);
1054
+ }
1055
+ if (allowedTypes && !allowedTypes.includes(file.type)) {
1056
+ throw new Error(
1057
+ `File type "${file.type}" is not allowed. Allowed: ${allowedTypes.join(", ")}`
1058
+ );
1059
+ }
1060
+ files.push({
1061
+ fieldName: key,
1062
+ fileName: file.name,
1063
+ type: file.type,
1064
+ size: file.size,
1065
+ data: await file.arrayBuffer()
1066
+ });
1067
+ }
1068
+ return files;
1069
+ }
1070
+ async function saveFile(filePath, data) {
1071
+ let bytes;
1072
+ if (data instanceof Blob) {
1073
+ bytes = new Uint8Array(await data.arrayBuffer());
1074
+ } else if (data instanceof ArrayBuffer) {
1075
+ bytes = new Uint8Array(data);
1076
+ } else {
1077
+ bytes = data;
1078
+ }
1079
+ try {
1080
+ await writeFile(filePath, bytes);
1081
+ } catch (err) {
1082
+ if (err.code === "ENOENT") {
1083
+ await mkdir(dirname(filePath), { recursive: true });
1084
+ await writeFile(filePath, bytes);
1085
+ } else {
1086
+ throw err;
1087
+ }
1088
+ }
1089
+ }
1090
+
1091
+ // src/middleware/basic-auth.ts
1092
+ function basicAuth(options) {
1093
+ const realm = options.realm ?? "Secure Area";
1094
+ const wwwAuth = `Basic realm="${realm}"`;
1095
+ const unauthorizedResponse = () => new Response("Unauthorized", {
1096
+ status: 401,
1097
+ headers: { "WWW-Authenticate": wwwAuth }
1098
+ });
1099
+ if (options.verifyUser) {
1100
+ const verify = options.verifyUser;
1101
+ return async (ctx) => {
1102
+ const authorization = ctx.headers["authorization"];
1103
+ if (!authorization || !authorization.startsWith("Basic ")) {
1104
+ return unauthorizedResponse();
1105
+ }
1106
+ const decoded = atob(authorization.slice(6));
1107
+ const colonIndex = decoded.indexOf(":");
1108
+ if (colonIndex === -1) return unauthorizedResponse();
1109
+ const username = decoded.substring(0, colonIndex);
1110
+ const password = decoded.substring(colonIndex + 1);
1111
+ const valid = await verify(username, password, ctx);
1112
+ if (!valid) return unauthorizedResponse();
1113
+ return { username };
1114
+ };
1115
+ }
1116
+ const expectedUser = options.username;
1117
+ const expectedPass = options.password;
1118
+ return (ctx) => {
1119
+ const authorization = ctx.headers["authorization"];
1120
+ if (!authorization || !authorization.startsWith("Basic ")) {
1121
+ return unauthorizedResponse();
1122
+ }
1123
+ const decoded = atob(authorization.slice(6));
1124
+ const colonIndex = decoded.indexOf(":");
1125
+ if (colonIndex === -1) return unauthorizedResponse();
1126
+ const username = decoded.substring(0, colonIndex);
1127
+ const password = decoded.substring(colonIndex + 1);
1128
+ if (username !== expectedUser || password !== expectedPass) {
1129
+ return unauthorizedResponse();
1130
+ }
1131
+ return { username };
1132
+ };
1133
+ }
1134
+
1135
+ // src/middleware/bearer-auth.ts
1136
+ function bearerAuth(options) {
1137
+ const realm = options.realm ?? "";
1138
+ const prefix = options.prefix ?? "Bearer";
1139
+ const headerName = (options.headerName ?? "authorization").toLowerCase();
1140
+ const prefixWithSpace = prefix + " ";
1141
+ const unauthorizedResponse = (message = "Unauthorized") => new Response(message, {
1142
+ status: 401,
1143
+ headers: {
1144
+ "WWW-Authenticate": `${prefix} realm="${realm}"${realm ? "" : ', error="invalid_token"'}`
1145
+ }
1146
+ });
1147
+ if (options.verifyToken) {
1148
+ const verify = options.verifyToken;
1149
+ return async (ctx) => {
1150
+ const authHeader = ctx.headers[headerName];
1151
+ if (!authHeader || !authHeader.startsWith(prefixWithSpace)) {
1152
+ return unauthorizedResponse("No authentication token provided");
1153
+ }
1154
+ const token = authHeader.slice(prefixWithSpace.length).trim();
1155
+ if (!token) return unauthorizedResponse("No authentication token provided");
1156
+ const valid = await verify(token, ctx);
1157
+ if (!valid) return unauthorizedResponse("Invalid token");
1158
+ return { token };
1159
+ };
1160
+ }
1161
+ const validTokens = Array.isArray(options.token) ? new Set(options.token) : new Set(options.token ? [options.token] : []);
1162
+ return (ctx) => {
1163
+ const authHeader = ctx.headers[headerName];
1164
+ if (!authHeader || !authHeader.startsWith(prefixWithSpace)) {
1165
+ return unauthorizedResponse("No authentication token provided");
1166
+ }
1167
+ const token = authHeader.slice(prefixWithSpace.length).trim();
1168
+ if (!token) return unauthorizedResponse("No authentication token provided");
1169
+ if (!validTokens.has(token)) {
1170
+ return unauthorizedResponse("Invalid token");
1171
+ }
1172
+ return { token };
1173
+ };
1174
+ }
1175
+
1176
+ // src/middleware/body-limit.ts
1177
+ function bodyLimit(options) {
1178
+ const maxSize = options.maxSize;
1179
+ const onError = options.onError;
1180
+ return (ctx) => {
1181
+ const contentLength = ctx.headers["content-length"];
1182
+ if (contentLength) {
1183
+ const size = parseInt(contentLength, 10);
1184
+ if (!isNaN(size) && size > maxSize) {
1185
+ if (onError) {
1186
+ return onError(ctx);
1187
+ }
1188
+ return new Response(`Payload Too Large. Max size: ${maxSize} bytes`, {
1189
+ status: 413
1190
+ });
1191
+ }
1192
+ }
1193
+ };
1194
+ }
1195
+
1196
+ // src/middleware/cache.ts
1197
+ function cache(options = {}) {
1198
+ const directives = [];
1199
+ if (options.public) directives.push("public");
1200
+ if (options.private) directives.push("private");
1201
+ if (options.noCache) directives.push("no-cache");
1202
+ if (options.noStore) directives.push("no-store");
1203
+ if (options.mustRevalidate) directives.push("must-revalidate");
1204
+ if (options.proxyRevalidate) directives.push("proxy-revalidate");
1205
+ if (options.immutable) directives.push("immutable");
1206
+ if (options.noTransform) directives.push("no-transform");
1207
+ if (options.maxAge !== void 0) directives.push(`max-age=${options.maxAge}`);
1208
+ if (options.sMaxAge !== void 0) directives.push(`s-maxage=${options.sMaxAge}`);
1209
+ if (options.staleWhileRevalidate !== void 0) directives.push(`stale-while-revalidate=${options.staleWhileRevalidate}`);
1210
+ if (options.staleIfError !== void 0) directives.push(`stale-if-error=${options.staleIfError}`);
1211
+ const cacheControl = directives.length > 0 ? directives.join(", ") : "no-cache";
1212
+ const vary = options.vary;
1213
+ return (ctx) => {
1214
+ ctx.onFinish((resp) => {
1215
+ if (resp instanceof Response) {
1216
+ resp.headers.set("Cache-Control", cacheControl);
1217
+ if (vary) {
1218
+ resp.headers.set("Vary", vary);
1219
+ }
1220
+ }
1221
+ });
1222
+ };
1223
+ }
1224
+
1225
+ // src/middleware/compress.ts
1226
+ function compress(options = {}) {
1227
+ const preferred = options.preferred ?? ["br", "gzip", "deflate"];
1228
+ return (ctx) => {
1229
+ const acceptEncoding = ctx.headers["accept-encoding"] ?? "";
1230
+ let encoding = null;
1231
+ for (let i = 0; i < preferred.length; i++) {
1232
+ if (acceptEncoding.indexOf(preferred[i]) !== -1) {
1233
+ encoding = preferred[i];
1234
+ break;
1235
+ }
1236
+ }
1237
+ ctx.onFinish((resp) => {
1238
+ if (resp instanceof Response) {
1239
+ resp.headers.append("Vary", "Accept-Encoding");
1240
+ }
1241
+ });
1242
+ return { encoding };
1243
+ };
1244
+ }
1245
+
1246
+ // src/middleware/cors.ts
1247
+ function cors(options = {}) {
1248
+ const originOpt = options.origin ?? "*";
1249
+ const allowMethods = (options.allowMethods ?? ["GET", "HEAD", "PUT", "POST", "DELETE", "PATCH"]).join(",");
1250
+ const allowHeaders = options.allowHeaders?.join(",") ?? "";
1251
+ const exposeHeaders = options.exposeHeaders?.join(",") ?? "";
1252
+ const maxAge = options.maxAge !== void 0 ? String(options.maxAge) : "";
1253
+ const credentials = options.credentials ?? false;
1254
+ const resolveOrigin = (requestOrigin, ctx) => {
1255
+ if (typeof originOpt === "string") return originOpt;
1256
+ if (Array.isArray(originOpt)) {
1257
+ return originOpt.includes(requestOrigin) ? requestOrigin : originOpt[0];
1258
+ }
1259
+ return originOpt(requestOrigin, ctx);
1260
+ };
1261
+ const corsMiddleware = (ctx) => {
1262
+ const requestOrigin = ctx.headers["origin"] ?? "";
1263
+ const allowedOrigin = resolveOrigin(requestOrigin, ctx);
1264
+ ctx.onFinish((resp) => {
1265
+ if (resp instanceof Response) {
1266
+ resp.headers.set("Access-Control-Allow-Origin", allowedOrigin);
1267
+ if (credentials) resp.headers.set("Access-Control-Allow-Credentials", "true");
1268
+ if (exposeHeaders) resp.headers.set("Access-Control-Expose-Headers", exposeHeaders);
1269
+ }
1270
+ });
1271
+ };
1272
+ const preflightHandler = (ctx) => {
1273
+ const requestOrigin = ctx.headers["origin"] ?? "";
1274
+ const allowedOrigin = resolveOrigin(requestOrigin, ctx);
1275
+ const headers = {
1276
+ "Access-Control-Allow-Origin": allowedOrigin,
1277
+ "Access-Control-Allow-Methods": allowMethods
1278
+ };
1279
+ if (allowHeaders) {
1280
+ headers["Access-Control-Allow-Headers"] = allowHeaders;
1281
+ } else {
1282
+ const requestedHeaders = ctx.headers["access-control-request-headers"];
1283
+ if (requestedHeaders) {
1284
+ headers["Access-Control-Allow-Headers"] = requestedHeaders;
1285
+ }
1286
+ }
1287
+ if (maxAge) headers["Access-Control-Max-Age"] = maxAge;
1288
+ if (credentials) headers["Access-Control-Allow-Credentials"] = "true";
1289
+ if (exposeHeaders) headers["Access-Control-Expose-Headers"] = exposeHeaders;
1290
+ return new Response(null, { status: 204, headers });
1291
+ };
1292
+ const corsApp = new App();
1293
+ corsApp.apply("/*", corsMiddleware);
1294
+ corsApp.options("/", preflightHandler);
1295
+ return corsApp;
1296
+ }
1297
+
1298
+ // src/middleware/csrf.ts
1299
+ var UNSAFE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
1300
+ var FORM_CONTENT_TYPES = ["application/x-www-form-urlencoded", "multipart/form-data", "text/plain"];
1301
+ function csrf(options = {}) {
1302
+ const originOpt = options.origin;
1303
+ return (ctx) => {
1304
+ if (!UNSAFE_METHODS.has(ctx.method)) return;
1305
+ const contentType = (ctx.headers["content-type"] ?? "").toLowerCase();
1306
+ const isFormLike = FORM_CONTENT_TYPES.some((t) => contentType.startsWith(t));
1307
+ if (contentType && !isFormLike) return;
1308
+ const requestOrigin = ctx.headers["origin"] ?? "";
1309
+ if (!requestOrigin) {
1310
+ const referer = ctx.headers["referer"];
1311
+ if (referer) {
1312
+ try {
1313
+ const refOrigin = new URL(referer).origin;
1314
+ if (isAllowed(refOrigin, ctx, originOpt)) return;
1315
+ } catch {
1316
+ }
1317
+ }
1318
+ return new Response("CSRF validation failed: missing Origin header", {
1319
+ status: 403
1320
+ });
1321
+ }
1322
+ if (!isAllowed(requestOrigin, ctx, originOpt)) {
1323
+ return new Response("CSRF validation failed: origin not allowed", {
1324
+ status: 403
1325
+ });
1326
+ }
1327
+ };
1328
+ }
1329
+ function isAllowed(origin, ctx, originOpt) {
1330
+ if (!originOpt) {
1331
+ try {
1332
+ const requestUrl = new URL(ctx.url);
1333
+ const requestOrigin = requestUrl.origin;
1334
+ return origin === requestOrigin;
1335
+ } catch {
1336
+ return false;
1337
+ }
1338
+ }
1339
+ if (typeof originOpt === "string") {
1340
+ return origin === originOpt;
1341
+ }
1342
+ if (Array.isArray(originOpt)) {
1343
+ return originOpt.includes(origin);
1344
+ }
1345
+ return originOpt(origin, ctx);
1346
+ }
1347
+
1348
+ // src/middleware/etag.ts
1349
+ function etag(_options = {}) {
1350
+ return (ctx) => {
1351
+ const ifNoneMatch = ctx.headers["if-none-match"] ?? null;
1352
+ return ifNoneMatch;
1353
+ };
1354
+ }
1355
+
1356
+ // src/middleware/ip-restriction.ts
1357
+ function ipRestriction(options) {
1358
+ const allowSet = options.allowList ? new Set(options.allowList) : null;
1359
+ const denySet = options.denyList ? new Set(options.denyList) : null;
1360
+ const onError = options.onError;
1361
+ const forbiddenResponse = (ctx) => {
1362
+ if (onError) return onError(ctx);
1363
+ return new Response("Forbidden", { status: 403 });
1364
+ };
1365
+ return (ctx) => {
1366
+ const ip = ctx.remoteInfo.address;
1367
+ if (allowSet && !allowSet.has(ip)) {
1368
+ return forbiddenResponse(ctx);
1369
+ }
1370
+ if (denySet && denySet.has(ip)) {
1371
+ return forbiddenResponse(ctx);
1372
+ }
1373
+ };
1374
+ }
1375
+
1376
+ // src/middleware/jwk.ts
1377
+ var RSA_HASH_MAP = {
1378
+ "RS256": "SHA-256",
1379
+ "RS384": "SHA-384",
1380
+ "RS512": "SHA-512"
1381
+ };
1382
+ var EC_HASH_MAP = {
1383
+ "ES256": { hash: "SHA-256", namedCurve: "P-256" },
1384
+ "ES384": { hash: "SHA-384", namedCurve: "P-384" },
1385
+ "ES512": { hash: "SHA-512", namedCurve: "P-521" }
1386
+ };
1387
+ var cachedKeys = null;
1388
+ var cacheExpiry = 0;
1389
+ async function fetchJwks(url, ttl) {
1390
+ const now = Date.now();
1391
+ if (cachedKeys && now < cacheExpiry) {
1392
+ return cachedKeys;
1393
+ }
1394
+ const response = await fetch(url);
1395
+ if (!response.ok) {
1396
+ throw new Error(`Failed to fetch JWKS: ${response.status}`);
1397
+ }
1398
+ const jwks = await response.json();
1399
+ cachedKeys = jwks.keys;
1400
+ cacheExpiry = now + ttl;
1401
+ return cachedKeys;
1402
+ }
1403
+ async function verifyWithJwk(token, keys, options) {
1404
+ const parts = token.split(".");
1405
+ if (parts.length !== 3) throw new Error("Invalid JWT format");
1406
+ const headerJson = decoder.decode(decodeBase64Url(parts[0]));
1407
+ const header = JSON.parse(headerJson);
1408
+ const alg = header.alg;
1409
+ const kid = header.kid;
1410
+ let jwk2;
1411
+ if (kid) {
1412
+ jwk2 = keys.find((k) => k.kid === kid);
1413
+ }
1414
+ if (!jwk2) {
1415
+ jwk2 = keys.find((k) => k.alg === alg || !k.alg && k.use === "sig");
1416
+ }
1417
+ if (!jwk2) {
1418
+ throw new Error(`No matching JWK found for kid=${kid}, alg=${alg}`);
1419
+ }
1420
+ const data = encoder.encode(`${parts[0]}.${parts[1]}`);
1421
+ const signature = decodeBase64Url(parts[2]);
1422
+ let key;
1423
+ if (RSA_HASH_MAP[alg]) {
1424
+ key = await crypto.subtle.importKey(
1425
+ "jwk",
1426
+ jwk2,
1427
+ { name: "RSASSA-PKCS1-v1_5", hash: RSA_HASH_MAP[alg] },
1428
+ false,
1429
+ ["verify"]
1430
+ );
1431
+ const valid = await crypto.subtle.verify("RSASSA-PKCS1-v1_5", key, signature, data);
1432
+ if (!valid) throw new Error("Invalid JWT signature");
1433
+ } else if (EC_HASH_MAP[alg]) {
1434
+ const ecInfo = EC_HASH_MAP[alg];
1435
+ key = await crypto.subtle.importKey(
1436
+ "jwk",
1437
+ jwk2,
1438
+ { name: "ECDSA", namedCurve: ecInfo.namedCurve },
1439
+ false,
1440
+ ["verify"]
1441
+ );
1442
+ const valid = await crypto.subtle.verify(
1443
+ { name: "ECDSA", hash: ecInfo.hash },
1444
+ key,
1445
+ signature,
1446
+ data
1447
+ );
1448
+ if (!valid) throw new Error("Invalid JWT signature");
1449
+ } else {
1450
+ throw new Error(`Unsupported algorithm: ${alg}`);
1451
+ }
1452
+ const payloadJson = decoder.decode(decodeBase64Url(parts[1]));
1453
+ const payload = JSON.parse(payloadJson);
1454
+ const now = Math.floor(Date.now() / 1e3);
1455
+ const tolerance = options.clockTolerance ?? 0;
1456
+ if (payload.exp !== void 0 && now >= payload.exp + tolerance) {
1457
+ throw new Error("JWT expired");
1458
+ }
1459
+ if (payload.nbf !== void 0 && now < payload.nbf - tolerance) {
1460
+ throw new Error("JWT not yet valid");
1461
+ }
1462
+ if (options.issuer && payload.iss !== options.issuer) {
1463
+ throw new Error(`Invalid issuer: expected ${options.issuer}`);
1464
+ }
1465
+ if (options.audience) {
1466
+ const aud = payload.aud;
1467
+ const validAudience = Array.isArray(aud) ? aud.includes(options.audience) : aud === options.audience;
1468
+ if (!validAudience) {
1469
+ throw new Error(`Invalid audience: expected ${options.audience}`);
1470
+ }
1471
+ }
1472
+ return payload;
1473
+ }
1474
+ function jwk(options) {
1475
+ const headerName = (options.headerName ?? "authorization").toLowerCase();
1476
+ const prefix = options.prefix ?? "Bearer";
1477
+ const prefixWithSpace = prefix + " ";
1478
+ const cacheTtl = options.cacheTtl ?? 6e5;
1479
+ const verifyOpts = {
1480
+ issuer: options.issuer,
1481
+ audience: options.audience,
1482
+ clockTolerance: options.clockTolerance
1483
+ };
1484
+ return async (ctx) => {
1485
+ const authHeader = ctx.headers[headerName];
1486
+ if (!authHeader || !authHeader.startsWith(prefixWithSpace)) {
1487
+ return new Response("JWT token required", {
1488
+ status: 401,
1489
+ headers: { "WWW-Authenticate": `Bearer realm="", error="invalid_token"` }
1490
+ });
1491
+ }
1492
+ const token = authHeader.slice(prefixWithSpace.length).trim();
1493
+ if (!token) {
1494
+ return new Response("JWT token required", {
1495
+ status: 401,
1496
+ headers: { "WWW-Authenticate": `Bearer realm="", error="invalid_token"` }
1497
+ });
1498
+ }
1499
+ try {
1500
+ let keys;
1501
+ if (options.keys) {
1502
+ keys = options.keys;
1503
+ } else if (options.jwksUrl) {
1504
+ keys = await fetchJwks(options.jwksUrl, cacheTtl);
1505
+ } else {
1506
+ throw new Error("Either jwksUrl or keys must be provided");
1507
+ }
1508
+ return await verifyWithJwk(token, keys, verifyOpts);
1509
+ } catch (err) {
1510
+ return new Response(`JWK verification failed: ${err.message}`, {
1511
+ status: 401,
1512
+ headers: { "WWW-Authenticate": `Bearer realm="", error="invalid_token"` }
1513
+ });
1514
+ }
1515
+ };
1516
+ }
1517
+
1518
+ // src/middleware/jwt.ts
1519
+ var HASH_MAP = {
1520
+ "HS256": "SHA-256",
1521
+ "HS384": "SHA-384",
1522
+ "HS512": "SHA-512"
1523
+ };
1524
+ async function verifyJwt(token, secret, options = {}) {
1525
+ const parts = token.split(".");
1526
+ if (parts.length !== 3) {
1527
+ throw new Error("Invalid JWT format");
1528
+ }
1529
+ const headerJson = decoder.decode(decodeBase64Url(parts[0]));
1530
+ const header = JSON.parse(headerJson);
1531
+ const alg = header.alg;
1532
+ const expectedAlg = options.algorithm ?? "HS256";
1533
+ if (alg !== expectedAlg) {
1534
+ throw new Error(`Algorithm mismatch: expected ${expectedAlg}, got ${alg}`);
1535
+ }
1536
+ const hash = HASH_MAP[alg];
1537
+ if (!hash) {
1538
+ throw new Error(`Unsupported algorithm: ${alg}`);
1539
+ }
1540
+ const keyData = encoder.encode(secret);
1541
+ const key = await crypto.subtle.importKey(
1542
+ "raw",
1543
+ keyData,
1544
+ { name: "HMAC", hash },
1545
+ false,
1546
+ ["verify"]
1547
+ );
1548
+ const data = encoder.encode(`${parts[0]}.${parts[1]}`);
1549
+ const signature = decodeBase64Url(parts[2]);
1550
+ const valid = await crypto.subtle.verify("HMAC", key, signature, data);
1551
+ if (!valid) {
1552
+ throw new Error("Invalid JWT signature");
1553
+ }
1554
+ const payloadJson = decoder.decode(decodeBase64Url(parts[1]));
1555
+ const payload = JSON.parse(payloadJson);
1556
+ const now = Math.floor(Date.now() / 1e3);
1557
+ const tolerance = options.clockTolerance ?? 0;
1558
+ if (payload.exp !== void 0 && now >= payload.exp + tolerance) {
1559
+ throw new Error("JWT expired");
1560
+ }
1561
+ if (payload.nbf !== void 0 && now < payload.nbf - tolerance) {
1562
+ throw new Error("JWT not yet valid");
1563
+ }
1564
+ if (options.issuer && payload.iss !== options.issuer) {
1565
+ throw new Error(`Invalid issuer: expected ${options.issuer}, got ${payload.iss}`);
1566
+ }
1567
+ if (options.audience) {
1568
+ const aud = payload.aud;
1569
+ const validAudience = Array.isArray(aud) ? aud.includes(options.audience) : aud === options.audience;
1570
+ if (!validAudience) {
1571
+ throw new Error(`Invalid audience: expected ${options.audience}`);
1572
+ }
1573
+ }
1574
+ return payload;
1575
+ }
1576
+ async function signJwt(payload, secret, algorithm = "HS256") {
1577
+ const header = { alg: algorithm, typ: "JWT" };
1578
+ const headerB64 = encodeBase64Url(encoder.encode(JSON.stringify(header)));
1579
+ const payloadB64 = encodeBase64Url(encoder.encode(JSON.stringify(payload)));
1580
+ const hash = HASH_MAP[algorithm];
1581
+ const keyData = encoder.encode(secret);
1582
+ const key = await crypto.subtle.importKey(
1583
+ "raw",
1584
+ keyData,
1585
+ { name: "HMAC", hash },
1586
+ false,
1587
+ ["sign"]
1588
+ );
1589
+ const data = encoder.encode(`${headerB64}.${payloadB64}`);
1590
+ const signature = await crypto.subtle.sign("HMAC", key, data);
1591
+ const signatureB64 = encodeBase64Url(new Uint8Array(signature));
1592
+ return `${headerB64}.${payloadB64}.${signatureB64}`;
1593
+ }
1594
+ function decodeJwt(token) {
1595
+ const parts = token.split(".");
1596
+ if (parts.length !== 3) throw new Error("Invalid JWT format");
1597
+ const header = JSON.parse(decoder.decode(decodeBase64Url(parts[0])));
1598
+ const payload = JSON.parse(decoder.decode(decodeBase64Url(parts[1])));
1599
+ return { header, payload };
1600
+ }
1601
+ function jwt(options) {
1602
+ const secret = options.secret;
1603
+ const algorithm = options.algorithm ?? "HS256";
1604
+ const headerName = (options.headerName ?? "authorization").toLowerCase();
1605
+ const prefix = options.prefix ?? "Bearer";
1606
+ const prefixWithSpace = prefix + " ";
1607
+ const verifyOpts = {
1608
+ algorithm,
1609
+ issuer: options.issuer,
1610
+ audience: options.audience,
1611
+ clockTolerance: options.clockTolerance
1612
+ };
1613
+ return async (ctx) => {
1614
+ const authHeader = ctx.headers[headerName];
1615
+ if (!authHeader || !authHeader.startsWith(prefixWithSpace)) {
1616
+ return new Response("JWT token required", {
1617
+ status: 401,
1618
+ headers: { "WWW-Authenticate": `Bearer realm="", error="invalid_token"` }
1619
+ });
1620
+ }
1621
+ const token = authHeader.slice(prefixWithSpace.length).trim();
1622
+ if (!token) {
1623
+ return new Response("JWT token required", {
1624
+ status: 401,
1625
+ headers: { "WWW-Authenticate": `Bearer realm="", error="invalid_token"` }
1626
+ });
1627
+ }
1628
+ try {
1629
+ return await verifyJwt(token, secret, verifyOpts);
1630
+ } catch (err) {
1631
+ return new Response(`JWT verification failed: ${err.message}`, {
1632
+ status: 401,
1633
+ headers: { "WWW-Authenticate": `Bearer realm="", error="invalid_token"` }
1634
+ });
1635
+ }
1636
+ };
1637
+ }
1638
+
1639
+ // src/middleware/logger.ts
1640
+ function logger(options = {}) {
1641
+ const logFn = options.logFn ?? console.log;
1642
+ const formatter = options.format ? (method, url, status, duration) => options.format({ method, url, status, duration }) : (method, url, status, duration) => {
1643
+ const methodColor = "\x1B[36m";
1644
+ const statusColor = status >= 500 ? "\x1B[31m" : status >= 400 ? "\x1B[33m" : status >= 300 ? "\x1B[36m" : "\x1B[32m";
1645
+ const reset = "\x1B[0m";
1646
+ return `${methodColor}${method}${reset} ${url} - ${statusColor}${status}${reset} - ${duration}ms`;
1647
+ };
1648
+ const loggerMiddleware = (ctx) => {
1649
+ const start = Date.now();
1650
+ ctx.onFinish((resp) => {
1651
+ const duration = Date.now() - start;
1652
+ const status = resp instanceof Response ? resp.status : 200;
1653
+ logFn(formatter(ctx.method, ctx.url, status, duration));
1654
+ });
1655
+ };
1656
+ const loggerApp = new App();
1657
+ loggerApp.apply("/*", loggerMiddleware);
1658
+ return loggerApp;
1659
+ }
1660
+
1661
+ // src/middleware/request-id.ts
1662
+ function requestId(options = {}) {
1663
+ const generator = options.generator ?? (() => crypto.randomUUID());
1664
+ const responseHeader = options.headerName ?? "X-Request-Id";
1665
+ const requestHeader = options.requestHeaderName;
1666
+ return (ctx) => {
1667
+ let id;
1668
+ if (requestHeader) {
1669
+ id = ctx.headers[requestHeader.toLowerCase()] || void 0;
1670
+ }
1671
+ if (!id) {
1672
+ id = generator();
1673
+ }
1674
+ ctx.onFinish((resp) => {
1675
+ if (resp instanceof Response) {
1676
+ resp.headers.set(responseHeader, id);
1677
+ }
1678
+ });
1679
+ return id;
1680
+ };
1681
+ }
1682
+
1683
+ // src/middleware/secure-headers.ts
1684
+ var DEFAULTS = {
1685
+ "Cross-Origin-Opener-Policy": "same-origin",
1686
+ "Cross-Origin-Resource-Policy": "same-origin",
1687
+ "Origin-Agent-Cluster": "?1",
1688
+ "Referrer-Policy": "no-referrer",
1689
+ "Strict-Transport-Security": "max-age=15552000; includeSubDomains",
1690
+ "X-Content-Type-Options": "nosniff",
1691
+ "X-DNS-Prefetch-Control": "off",
1692
+ "X-Download-Options": "noopen",
1693
+ "X-Frame-Options": "SAMEORIGIN",
1694
+ "X-Permitted-Cross-Domain-Policies": "none",
1695
+ "X-XSS-Protection": "0"
1696
+ };
1697
+ var OPTION_TO_HEADER = {
1698
+ contentSecurityPolicy: "Content-Security-Policy",
1699
+ crossOriginEmbedderPolicy: "Cross-Origin-Embedder-Policy",
1700
+ crossOriginOpenerPolicy: "Cross-Origin-Opener-Policy",
1701
+ crossOriginResourcePolicy: "Cross-Origin-Resource-Policy",
1702
+ originAgentCluster: "Origin-Agent-Cluster",
1703
+ referrerPolicy: "Referrer-Policy",
1704
+ strictTransportSecurity: "Strict-Transport-Security",
1705
+ xContentTypeOptions: "X-Content-Type-Options",
1706
+ xDnsPrefetchControl: "X-DNS-Prefetch-Control",
1707
+ xDownloadOptions: "X-Download-Options",
1708
+ xFrameOptions: "X-Frame-Options",
1709
+ xPermittedCrossDomainPolicies: "X-Permitted-Cross-Domain-Policies",
1710
+ xXssProtection: "X-XSS-Protection"
1711
+ };
1712
+ function secureHeaders(options = {}) {
1713
+ const removePoweredBy = options.removePoweredBy !== false;
1714
+ const headers = [];
1715
+ for (const [optKey, headerName] of Object.entries(OPTION_TO_HEADER)) {
1716
+ const optValue = options[optKey];
1717
+ if (optValue === false) {
1718
+ continue;
1719
+ }
1720
+ if (typeof optValue === "string") {
1721
+ headers.push([headerName, optValue]);
1722
+ } else if (DEFAULTS[headerName]) {
1723
+ headers.push([headerName, DEFAULTS[headerName]]);
1724
+ }
1725
+ }
1726
+ return (ctx) => {
1727
+ ctx.onFinish((resp) => {
1728
+ if (resp instanceof Response) {
1729
+ for (let i = 0; i < headers.length; i++) {
1730
+ resp.headers.set(headers[i][0], headers[i][1]);
1731
+ }
1732
+ if (removePoweredBy) {
1733
+ resp.headers.delete("X-Powered-By");
1734
+ }
1735
+ }
1736
+ });
1737
+ };
1738
+ }
1739
+
1740
+ // src/middleware/session.ts
1741
+ var UNAUTHORIZED_BODY = "Unauthorized";
1742
+ function session(options) {
1743
+ const { cookieName, validate, onUnauthorized } = options;
1744
+ const unauthorized = onUnauthorized ?? (() => new Response(UNAUTHORIZED_BODY, { status: 401 }));
1745
+ if (validate.constructor.name === "AsyncFunction") {
1746
+ return async (ctx) => {
1747
+ const cookie = ctx.cookies[cookieName];
1748
+ if (!cookie) return unauthorized(ctx);
1749
+ try {
1750
+ const result = await validate(cookie, ctx);
1751
+ if (result == null || result instanceof Response) {
1752
+ return result ?? unauthorized(ctx);
1753
+ }
1754
+ return result;
1755
+ } catch (e) {
1756
+ return e instanceof Response ? e : unauthorized(ctx);
1757
+ }
1758
+ };
1759
+ }
1760
+ return (ctx) => {
1761
+ const cookie = ctx.cookies[cookieName];
1762
+ if (!cookie) return unauthorized(ctx);
1763
+ try {
1764
+ const result = validate(cookie, ctx);
1765
+ if (result == null || result instanceof Response) {
1766
+ return result ?? unauthorized(ctx);
1767
+ }
1768
+ return result;
1769
+ } catch (e) {
1770
+ return e instanceof Response ? e : unauthorized(ctx);
1771
+ }
1772
+ };
1773
+ }
1774
+
1775
+ // src/middleware/timeout.ts
1776
+ function timeout(options) {
1777
+ const duration = options.duration;
1778
+ const onTimeout = options.onTimeout;
1779
+ return (ctx) => {
1780
+ const controller = new AbortController();
1781
+ const timer = setTimeout(() => {
1782
+ controller.abort();
1783
+ if (onTimeout) {
1784
+ return onTimeout(ctx);
1785
+ }
1786
+ }, duration);
1787
+ ctx.onFinish(() => {
1788
+ clearTimeout(timer);
1789
+ });
1790
+ return { signal: controller.signal };
1791
+ };
1792
+ }
1793
+
1794
+ // src/middleware/timing.ts
1795
+ function timing(options = {}) {
1796
+ const name = options.name ?? "total";
1797
+ const description = options.description;
1798
+ const enabled = options.enabled !== false;
1799
+ return (ctx) => {
1800
+ if (!enabled) return;
1801
+ const start = performance.now();
1802
+ ctx.onFinish((resp) => {
1803
+ if (resp instanceof Response) {
1804
+ const dur = (performance.now() - start).toFixed(2);
1805
+ let value = `${name};dur=${dur}`;
1806
+ if (description) {
1807
+ value = `${name};desc="${description}";dur=${dur}`;
1808
+ }
1809
+ const existing = resp.headers.get("Server-Timing");
1810
+ if (existing) {
1811
+ resp.headers.set("Server-Timing", `${existing}, ${value}`);
1812
+ } else {
1813
+ resp.headers.set("Server-Timing", value);
1814
+ }
1815
+ }
1816
+ });
1817
+ };
1818
+ }
1819
+
1820
+ // src/ws/codec.ts
1821
+ var MsgType = {
1822
+ PING: 1,
1823
+ PONG: 2,
1824
+ REQUEST: 3,
1825
+ RESPONSE: 4,
1826
+ SUBSCRIBE: 5,
1827
+ UNSUBSCRIBE: 6,
1828
+ PUBLISH: 7,
1829
+ MESSAGE: 8,
1830
+ AUTH: 9,
1831
+ AUTH_OK: 10,
1832
+ AUTH_FAIL: 11
1833
+ };
1834
+ var encoder2 = new TextEncoder();
1835
+ var decoder2 = new TextDecoder();
1836
+ var PING_BUF = new Uint8Array([MsgType.PING]);
1837
+ var PONG_BUF = new Uint8Array([MsgType.PONG]);
1838
+ function encodePing() {
1839
+ return PING_BUF;
1840
+ }
1841
+ function encodePong() {
1842
+ return PONG_BUF;
1843
+ }
1844
+ function encodeRequest(corrId, payload) {
1845
+ const payloadBytes = encoder2.encode(payload);
1846
+ const buf = new Uint8Array(5 + payloadBytes.length);
1847
+ buf[0] = MsgType.REQUEST;
1848
+ buf[1] = corrId >>> 24 & 255;
1849
+ buf[2] = corrId >>> 16 & 255;
1850
+ buf[3] = corrId >>> 8 & 255;
1851
+ buf[4] = corrId & 255;
1852
+ buf.set(payloadBytes, 5);
1853
+ return buf;
1854
+ }
1855
+ function encodeResponse(corrId, payload) {
1856
+ const payloadBytes = typeof payload === "string" ? encoder2.encode(payload) : payload;
1857
+ const buf = new Uint8Array(5 + payloadBytes.length);
1858
+ buf[0] = MsgType.RESPONSE;
1859
+ buf[1] = corrId >>> 24 & 255;
1860
+ buf[2] = corrId >>> 16 & 255;
1861
+ buf[3] = corrId >>> 8 & 255;
1862
+ buf[4] = corrId & 255;
1863
+ buf.set(payloadBytes, 5);
1864
+ return buf;
1865
+ }
1866
+ function encodeSubscribe(topic) {
1867
+ const topicBytes = encoder2.encode(topic);
1868
+ const buf = new Uint8Array(2 + topicBytes.length);
1869
+ buf[0] = MsgType.SUBSCRIBE;
1870
+ buf[1] = topicBytes.length;
1871
+ buf.set(topicBytes, 2);
1872
+ return buf;
1873
+ }
1874
+ function encodeUnsubscribe(topic) {
1875
+ const topicBytes = encoder2.encode(topic);
1876
+ const buf = new Uint8Array(2 + topicBytes.length);
1877
+ buf[0] = MsgType.UNSUBSCRIBE;
1878
+ buf[1] = topicBytes.length;
1879
+ buf.set(topicBytes, 2);
1880
+ return buf;
1881
+ }
1882
+ function encodePublish(topic, payload) {
1883
+ const topicBytes = encoder2.encode(topic);
1884
+ const payloadBytes = encoder2.encode(payload);
1885
+ const buf = new Uint8Array(2 + topicBytes.length + payloadBytes.length);
1886
+ buf[0] = MsgType.PUBLISH;
1887
+ buf[1] = topicBytes.length;
1888
+ buf.set(topicBytes, 2);
1889
+ buf.set(payloadBytes, 2 + topicBytes.length);
1890
+ return buf;
1891
+ }
1892
+ function encodeMessage(topic, payload) {
1893
+ const topicBytes = encoder2.encode(topic);
1894
+ const payloadBytes = encoder2.encode(payload);
1895
+ const buf = new Uint8Array(2 + topicBytes.length + payloadBytes.length);
1896
+ buf[0] = MsgType.MESSAGE;
1897
+ buf[1] = topicBytes.length;
1898
+ buf.set(topicBytes, 2);
1899
+ buf.set(payloadBytes, 2 + topicBytes.length);
1900
+ return buf;
1901
+ }
1902
+ function encodeAuth(token) {
1903
+ const tokenBytes = encoder2.encode(token);
1904
+ const buf = new Uint8Array(1 + tokenBytes.length);
1905
+ buf[0] = MsgType.AUTH;
1906
+ buf.set(tokenBytes, 1);
1907
+ return buf;
1908
+ }
1909
+ function encodeAuthOk(payload = "") {
1910
+ if (payload.length === 0) return new Uint8Array([MsgType.AUTH_OK]);
1911
+ const payloadBytes = encoder2.encode(payload);
1912
+ const buf = new Uint8Array(1 + payloadBytes.length);
1913
+ buf[0] = MsgType.AUTH_OK;
1914
+ buf.set(payloadBytes, 1);
1915
+ return buf;
1916
+ }
1917
+ function encodeAuthFail(reason) {
1918
+ const reasonBytes = encoder2.encode(reason);
1919
+ const buf = new Uint8Array(1 + reasonBytes.length);
1920
+ buf[0] = MsgType.AUTH_FAIL;
1921
+ buf.set(reasonBytes, 1);
1922
+ return buf;
1923
+ }
1924
+ function decode(data) {
1925
+ const bytes = data instanceof Uint8Array ? data : new Uint8Array(data);
1926
+ if (bytes.length === 0) return null;
1927
+ const type = bytes[0];
1928
+ switch (type) {
1929
+ // ── Ping / Pong ───────────────────────────────────────────────────────
1930
+ case MsgType.PING:
1931
+ return { type: MsgType.PING };
1932
+ case MsgType.PONG:
1933
+ return { type: MsgType.PONG };
1934
+ // ── Request / Response ────────────────────────────────────────────────
1935
+ case MsgType.REQUEST:
1936
+ case MsgType.RESPONSE: {
1937
+ if (bytes.length < 5) return null;
1938
+ const corrId = (bytes[1] << 24 | bytes[2] << 16 | bytes[3] << 8 | bytes[4]) >>> 0;
1939
+ const payload = bytes.length > 5 ? decoder2.decode(bytes.subarray(5)) : "";
1940
+ return type === MsgType.REQUEST ? { type: MsgType.REQUEST, corrId, payload } : { type: MsgType.RESPONSE, corrId, payload };
1941
+ }
1942
+ // ── Subscribe / Unsubscribe ───────────────────────────────────────────
1943
+ case MsgType.SUBSCRIBE:
1944
+ case MsgType.UNSUBSCRIBE: {
1945
+ if (bytes.length < 2) return null;
1946
+ const topicLen = bytes[1];
1947
+ if (bytes.length < 2 + topicLen) return null;
1948
+ const topic = decoder2.decode(bytes.subarray(2, 2 + topicLen));
1949
+ return type === MsgType.SUBSCRIBE ? { type: MsgType.SUBSCRIBE, topic } : { type: MsgType.UNSUBSCRIBE, topic };
1950
+ }
1951
+ // ── Publish / Message ─────────────────────────────────────────────────
1952
+ case MsgType.PUBLISH:
1953
+ case MsgType.MESSAGE: {
1954
+ if (bytes.length < 2) return null;
1955
+ const topicLen = bytes[1];
1956
+ if (bytes.length < 2 + topicLen) return null;
1957
+ const topic = decoder2.decode(bytes.subarray(2, 2 + topicLen));
1958
+ const payloadOffset = 2 + topicLen;
1959
+ const payload = bytes.length > payloadOffset ? decoder2.decode(bytes.subarray(payloadOffset)) : "";
1960
+ return type === MsgType.PUBLISH ? { type: MsgType.PUBLISH, topic, payload } : { type: MsgType.MESSAGE, topic, payload };
1961
+ }
1962
+ // ── Auth ───────────────────────────────────────────────────────────────
1963
+ case MsgType.AUTH:
1964
+ case MsgType.AUTH_OK:
1965
+ case MsgType.AUTH_FAIL: {
1966
+ const payload = bytes.length > 1 ? decoder2.decode(bytes.subarray(1)) : "";
1967
+ return type === MsgType.AUTH ? { type: MsgType.AUTH, payload } : type === MsgType.AUTH_OK ? { type: MsgType.AUTH_OK, payload } : { type: MsgType.AUTH_FAIL, payload };
1968
+ }
1969
+ default:
1970
+ return null;
1971
+ }
1972
+ }
1973
+ function isKenBinaryFrame(data) {
1974
+ const firstByte = data instanceof Uint8Array ? data[0] : new Uint8Array(data, 0, 1)[0];
1975
+ return firstByte !== void 0 && firstByte >= MsgType.PING && firstByte <= MsgType.AUTH_FAIL;
1976
+ }
1977
+
1978
+ // src/middleware/ws-client-protocol.ts
1979
+ var PONG_BIN = encodePong();
1980
+ var AUTHOK_BIN = encodeAuthOk();
1981
+ var PROTO_MESSAGE = "message";
1982
+ var AUTH_CLOSE_CODE = 4001;
1983
+ var AUTH_FAIL_INVALID_BIN = encodeAuthFail("Invalid token");
1984
+ var UNAUTHED = /* @__PURE__ */ Symbol("unauthed");
1985
+ function wsClientProtocol(config) {
1986
+ const {
1987
+ upgrade: userUpgrade,
1988
+ open: userOpen,
1989
+ message: userMessage,
1990
+ close: userClose,
1991
+ ping,
1992
+ pong,
1993
+ error,
1994
+ authenticate,
1995
+ tokenParam,
1996
+ maxPayloadLength,
1997
+ backpressureLimit,
1998
+ pingInterval,
1999
+ pongTimeout,
2000
+ perMessageDeflate,
2001
+ idleTimeout
2002
+ } = config;
2003
+ const authTokenParam = tokenParam ?? "token";
2004
+ const options = {};
2005
+ if (maxPayloadLength !== void 0) options.maxPayloadLength = maxPayloadLength;
2006
+ if (backpressureLimit !== void 0) options.backpressureLimit = backpressureLimit;
2007
+ if (pingInterval !== void 0) options.pingInterval = pingInterval;
2008
+ if (pongTimeout !== void 0) options.pongTimeout = pongTimeout;
2009
+ if (perMessageDeflate !== void 0) options.perMessageDeflate = perMessageDeflate;
2010
+ if (idleTimeout !== void 0) options.idleTimeout = idleTimeout;
2011
+ const preAuthPeers = authenticate ? /* @__PURE__ */ new WeakSet() : null;
2012
+ const handleKbwp = (peer, message) => {
2013
+ if (typeof message !== "string") {
2014
+ const bytes = message instanceof Uint8Array ? message : new Uint8Array(message);
2015
+ if (bytes.length === 0) return userMessage(peer, message);
2016
+ if (bytes.length === 1) {
2017
+ if (bytes[0] === MsgType.PING) {
2018
+ peer.send(PONG_BIN);
2019
+ return;
2020
+ }
2021
+ if (bytes[0] === MsgType.PONG) return;
2022
+ return userMessage(peer, message);
2023
+ }
2024
+ const frame = decode(bytes);
2025
+ if (frame === null) return userMessage(peer, message);
2026
+ switch (frame.type) {
2027
+ // ── Binary pub/sub ────────────────────────────────────────────────
2028
+ case MsgType.SUBSCRIBE:
2029
+ peer.subscribe(frame.topic);
2030
+ return;
2031
+ case MsgType.UNSUBSCRIBE:
2032
+ peer.unsubscribe(frame.topic);
2033
+ return;
2034
+ case MsgType.PUBLISH:
2035
+ if (frame.topic) {
2036
+ peer.publish(
2037
+ frame.topic,
2038
+ '{"type":"' + PROTO_MESSAGE + '","topic":' + JSON.stringify(frame.topic) + ',"data":' + frame.payload + "}"
2039
+ );
2040
+ }
2041
+ return;
2042
+ // ── Binary request-response ───────────────────────────────────────
2043
+ case MsgType.REQUEST: {
2044
+ const { corrId, payload } = frame;
2045
+ const origSend = peer.send;
2046
+ peer.send = (data, compress2) => {
2047
+ peer.send = origSend;
2048
+ return origSend.call(
2049
+ peer,
2050
+ typeof data === "string" ? encodeResponse(corrId, data) : encodeResponse(corrId, data),
2051
+ compress2
2052
+ );
2053
+ };
2054
+ const result = userMessage(peer, payload);
2055
+ if (result && typeof result.then === "function") {
2056
+ return result.finally(() => {
2057
+ if (peer.send !== origSend) {
2058
+ peer.send = origSend;
2059
+ }
2060
+ });
2061
+ }
2062
+ if (peer.send !== origSend) {
2063
+ peer.send = origSend;
2064
+ }
2065
+ return result;
2066
+ }
2067
+ // ── Binary message / response (server→server, handle gracefully) ──
2068
+ case MsgType.MESSAGE:
2069
+ case MsgType.RESPONSE:
2070
+ return userMessage(peer, message);
2071
+ }
2072
+ }
2073
+ return userMessage(peer, message);
2074
+ };
2075
+ let wrappedUpgrade;
2076
+ let wrappedOpen;
2077
+ let wrappedClose;
2078
+ let wrappedMessage;
2079
+ if (authenticate) {
2080
+ const authSuccess = (peer, data) => {
2081
+ peer.data = data;
2082
+ peer.send(AUTHOK_BIN);
2083
+ userOpen?.(peer);
2084
+ };
2085
+ const handleAuth = (peer, token) => {
2086
+ const result = authenticate(token);
2087
+ if (result !== null && typeof result.then === "function") {
2088
+ return result.then((data) => {
2089
+ if (data === null || data === void 0) {
2090
+ peer.send(AUTH_FAIL_INVALID_BIN);
2091
+ peer.close(AUTH_CLOSE_CODE, "Invalid token");
2092
+ return;
2093
+ }
2094
+ authSuccess(peer, data);
2095
+ });
2096
+ }
2097
+ if (result === null || result === void 0) {
2098
+ peer.send(AUTH_FAIL_INVALID_BIN);
2099
+ peer.close(AUTH_CLOSE_CODE, "Invalid token");
2100
+ return;
2101
+ }
2102
+ authSuccess(peer, result);
2103
+ };
2104
+ wrappedUpgrade = (req) => {
2105
+ const url = new URL(req.url);
2106
+ const token = url.searchParams.get(authTokenParam);
2107
+ if (token) {
2108
+ const result = authenticate(token);
2109
+ if (result !== null && typeof result.then === "function") {
2110
+ return result.then((data2) => {
2111
+ if (data2 === null || data2 === void 0) {
2112
+ return new Response("Unauthorized", { status: 401 });
2113
+ }
2114
+ if (typeof data2 === "object" && data2 !== null) {
2115
+ preAuthPeers.add(data2);
2116
+ }
2117
+ return data2;
2118
+ });
2119
+ }
2120
+ if (result === null || result === void 0) {
2121
+ return new Response("Unauthorized", { status: 401 });
2122
+ }
2123
+ const data = result;
2124
+ if (typeof data === "object" && data !== null) {
2125
+ preAuthPeers.add(data);
2126
+ }
2127
+ return data;
2128
+ }
2129
+ if (userUpgrade) return userUpgrade(req);
2130
+ return void 0;
2131
+ };
2132
+ wrappedOpen = (peer) => {
2133
+ if (peer.data && typeof peer.data === "object" && preAuthPeers.has(peer.data)) {
2134
+ preAuthPeers.delete(peer.data);
2135
+ userOpen?.(peer);
2136
+ return;
2137
+ }
2138
+ peer.data = UNAUTHED;
2139
+ };
2140
+ wrappedClose = userClose;
2141
+ wrappedMessage = (peer, message) => {
2142
+ if (peer.data !== UNAUTHED) {
2143
+ return handleKbwp(peer, message);
2144
+ }
2145
+ if (typeof message !== "string") {
2146
+ const bytes = message instanceof Uint8Array ? message : new Uint8Array(message);
2147
+ if (bytes.length > 0 && bytes[0] === MsgType.AUTH) {
2148
+ const frame = decode(bytes);
2149
+ if (frame && frame.type === MsgType.AUTH) {
2150
+ return handleAuth(peer, frame.payload);
2151
+ }
2152
+ }
2153
+ }
2154
+ peer.close(AUTH_CLOSE_CODE, "Not authenticated");
2155
+ };
2156
+ } else {
2157
+ wrappedUpgrade = userUpgrade;
2158
+ wrappedOpen = userOpen;
2159
+ wrappedClose = userClose;
2160
+ wrappedMessage = handleKbwp;
2161
+ }
2162
+ const handler = { message: wrappedMessage };
2163
+ if (wrappedUpgrade !== void 0) handler.upgrade = wrappedUpgrade;
2164
+ if (wrappedOpen !== void 0) handler.open = wrappedOpen;
2165
+ if (wrappedClose !== void 0) handler.close = wrappedClose;
2166
+ if (ping !== void 0) handler.ping = ping;
2167
+ if (pong !== void 0) handler.pong = pong;
2168
+ if (error !== void 0) handler.error = error;
2169
+ const wsApp = new App();
2170
+ wsApp.ws("/", handler, options);
2171
+ return wsApp;
2172
+ }
2173
+
2174
+ // src/ws/client.ts
2175
+ var DEFAULT_MAX_RETRIES = Infinity;
2176
+ var DEFAULT_BACKOFF_BASE = 500;
2177
+ var DEFAULT_BACKOFF_MAX = 3e4;
2178
+ var DEFAULT_BACKOFF_FACTOR = 2;
2179
+ var DEFAULT_HEARTBEAT_INTERVAL = 3e4;
2180
+ var DEFAULT_REQUEST_TIMEOUT = 1e4;
2181
+ var DEFAULT_AUTH_TIMEOUT = 1e4;
2182
+ var PING_FRAME = encodePing();
2183
+ function sendBinary(ws, data) {
2184
+ ws.send(data.buffer);
2185
+ }
2186
+ var WsClientState = {
2187
+ CONNECTING: 0,
2188
+ OPEN: 1,
2189
+ CLOSING: 2,
2190
+ CLOSED: 3,
2191
+ RECONNECTING: 4
2192
+ };
2193
+ var WSClient = class {
2194
+ // ── Public state ────────────────────────────────────────────────────────
2195
+ /** Current state of the connection */
2196
+ state = WsClientState.CLOSED;
2197
+ // ── Private fields — all initialized here for V8 monomorphic shape ──────
2198
+ _url;
2199
+ _authTimeout;
2200
+ _maxRetries;
2201
+ _backoffFactor;
2202
+ _backoffMax;
2203
+ _backoffBase;
2204
+ _heartbeatInterval;
2205
+ _requestTimeout;
2206
+ _onConnect;
2207
+ _onDisconnect;
2208
+ _onReconnectAttempt;
2209
+ _onData;
2210
+ _onFrame;
2211
+ /** Active WebSocket instance */
2212
+ _ws = null;
2213
+ /** Number of consecutive failed reconnect attempts */
2214
+ _retries = 0;
2215
+ /** Correlation registry: corrId (uint32) → PendingRequest */
2216
+ _pending = /* @__PURE__ */ new Map();
2217
+ /**
2218
+ * Active pub/sub subscriptions: topic → callback.
2219
+ * Persists across reconnects so topics are re-sent in _onOpen.
2220
+ */
2221
+ _subscriptions = /* @__PURE__ */ new Map();
2222
+ /** Heartbeat timer handle */
2223
+ _heartbeatTimer = null;
2224
+ /** Reconnect delay timer handle */
2225
+ _reconnectTimer = null;
2226
+ /** Whether destroy() / close() was explicitly called */
2227
+ _destroyed = false;
2228
+ /** Post-connect auth resolve/reject for the connect() promise */
2229
+ _authResolve = null;
2230
+ _authReject = null;
2231
+ /** Auth timeout timer handle */
2232
+ _authTimer = null;
2233
+ /** Whether auth is pending (post-connect, waiting for AUTH_OK/AUTH_FAIL) */
2234
+ _authPending = false;
2235
+ /** Pre-encoded AUTH frame — zero per-reconnect allocation */
2236
+ _authFrame;
2237
+ /**
2238
+ * Monotonically increasing uint32 counter for correlation IDs.
2239
+ * Wraps at 0xFFFFFFFF to stay within 4-byte range.
2240
+ */
2241
+ _idCounter = 0;
2242
+ // ── Bound event handlers — created once, reused per connection ───────────
2243
+ _handleOpen;
2244
+ _handleMessage;
2245
+ _handleClose;
2246
+ _handleError;
2247
+ constructor(url, options = {}) {
2248
+ const token = options.token;
2249
+ const authMode = options.authMode ?? "message";
2250
+ this._authTimeout = options.authTimeout ?? DEFAULT_AUTH_TIMEOUT;
2251
+ this._url = token && authMode === "query" ? `${url}${url.includes("?") ? "&" : "?"}token=${encodeURIComponent(token)}` : url;
2252
+ this._maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
2253
+ this._backoffFactor = options.backoffFactor ?? DEFAULT_BACKOFF_FACTOR;
2254
+ this._backoffMax = options.backoffMax ?? DEFAULT_BACKOFF_MAX;
2255
+ this._backoffBase = options.backoffBase ?? DEFAULT_BACKOFF_BASE;
2256
+ this._heartbeatInterval = options.heartbeatInterval ?? DEFAULT_HEARTBEAT_INTERVAL;
2257
+ this._requestTimeout = options.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT;
2258
+ this._onConnect = options.onConnect;
2259
+ this._onDisconnect = options.onDisconnect;
2260
+ this._onReconnectAttempt = options.onReconnectAttempt;
2261
+ this._onData = options.onData;
2262
+ this._onFrame = options.onFrame;
2263
+ const needsAuth = !!(token && authMode === "message");
2264
+ this._authFrame = needsAuth ? encodeAuth(token) : null;
2265
+ this._handleOpen = needsAuth ? this._onOpenAuth.bind(this) : this._onOpenNoAuth.bind(this);
2266
+ this._handleMessage = this._onMessage.bind(this);
2267
+ this._handleClose = this._onClose.bind(this);
2268
+ this._handleError = this._onError.bind(this);
2269
+ }
2270
+ // ── Public API ────────────────────────────────────────────────────────────
2271
+ /**
2272
+ * Connect to the WebSocket server.
2273
+ * Returns a Promise that resolves when the connection is established.
2274
+ * Safe to call multiple times — subsequent calls while already OPEN are no-ops.
2275
+ */
2276
+ connect() {
2277
+ if (this.state === WsClientState.OPEN) return Promise.resolve();
2278
+ return this._openConnection();
2279
+ }
2280
+ /**
2281
+ * Send a raw string or binary message. The connection must be OPEN.
2282
+ * For fire-and-forget use cases.
2283
+ *
2284
+ * @returns true if sent, false if not connected.
2285
+ */
2286
+ send(data) {
2287
+ if (this.state !== WsClientState.OPEN || this._ws === null) return false;
2288
+ this._ws.send(data);
2289
+ return true;
2290
+ }
2291
+ /**
2292
+ * Send a JSON message and wait for a correlated response.
2293
+ *
2294
+ * Uses the binary REQUEST frame format: [0x03][corrId:u32][JSON payload].
2295
+ * The correlation ID is carried in the binary header — NOT mixed into the
2296
+ * user payload — eliminating the previous `_kid` field collision issue.
2297
+ *
2298
+ * The server must respond with a binary RESPONSE frame echoing the same
2299
+ * corrId. When using `defineWsHandler`, this is handled automatically.
2300
+ *
2301
+ * @param data - Object to JSON-serialize and send.
2302
+ * @param timeout - Override the default `requestTimeout` for this request.
2303
+ * @returns Promise resolving with the parsed response payload.
2304
+ * @throws If the connection is not open, the request times out, or the
2305
+ * connection closes before a response arrives.
2306
+ */
2307
+ sendWait(data, timeout2) {
2308
+ if (this.state !== WsClientState.OPEN || this._ws === null) {
2309
+ return Promise.reject(new Error("WSClient: not connected"));
2310
+ }
2311
+ const corrId = this._nextId();
2312
+ const payload = JSON.stringify(data);
2313
+ const frame = encodeRequest(corrId, payload);
2314
+ const ms = timeout2 ?? this._requestTimeout;
2315
+ return new Promise((resolve, reject) => {
2316
+ const timer = setTimeout(() => {
2317
+ this._pending.delete(corrId);
2318
+ reject(new Error(`WSClient: request ${corrId} timed out after ${ms}ms`));
2319
+ }, ms);
2320
+ this._pending.set(corrId, {
2321
+ resolve,
2322
+ reject,
2323
+ timer
2324
+ });
2325
+ sendBinary(this._ws, frame);
2326
+ });
2327
+ }
2328
+ // ── Pub/Sub API ──────────────────────────────────────────────────────────
2329
+ /**
2330
+ * Subscribe to a server-side topic.
2331
+ * Sends `{"type":"subscribe","topic":"..."}` to the server and registers a
2332
+ * local callback invoked for each `{"type":"message","topic":"...","data":...}`
2333
+ * frame received on that topic.
2334
+ *
2335
+ * Safe to call before `connect()` — the subscription is registered locally
2336
+ * and sent once the connection opens (or re-opens after reconnect).
2337
+ *
2338
+ * Calling with the same topic replaces the existing callback.
2339
+ *
2340
+ * @param topic - Topic name to subscribe to
2341
+ * @param callback - Called with the `data` field from each topic message
2342
+ */
2343
+ subscribe(topic, callback) {
2344
+ this._subscriptions.set(topic, callback);
2345
+ if (this.state === WsClientState.OPEN && this._ws !== null) {
2346
+ sendBinary(this._ws, encodeSubscribe(topic));
2347
+ }
2348
+ }
2349
+ /**
2350
+ * Unsubscribe from a server-side topic.
2351
+ * Sends `{"type":"unsubscribe","topic":"..."}` if currently connected.
2352
+ * The local callback is removed regardless of connection state.
2353
+ *
2354
+ * @param topic - Topic name to unsubscribe from
2355
+ */
2356
+ unsubscribe(topic) {
2357
+ if (this._subscriptions.delete(topic)) {
2358
+ if (this.state === WsClientState.OPEN && this._ws !== null) {
2359
+ sendBinary(this._ws, encodeUnsubscribe(topic));
2360
+ }
2361
+ }
2362
+ }
2363
+ /**
2364
+ * Publish a message to a server-side topic.
2365
+ * Sends `{"type":"publish","topic":"...","data":...}` to the server.
2366
+ * The server (when using `defineWsHandler`) re-broadcasts the message to all
2367
+ * topic subscribers as `{"type":"message","topic":"...","data":...}`.
2368
+ *
2369
+ * @param topic - Topic to publish to
2370
+ * @param data - Payload (JSON-serializable)
2371
+ * @returns true if sent, false if not connected
2372
+ */
2373
+ publish(topic, data) {
2374
+ if (this.state !== WsClientState.OPEN || this._ws === null) return false;
2375
+ sendBinary(this._ws, encodePublish(topic, JSON.stringify(data)));
2376
+ return true;
2377
+ }
2378
+ /**
2379
+ * Gracefully close the connection.
2380
+ * Does NOT reconnect. Cleans up all resources.
2381
+ */
2382
+ close(code = 1e3, reason = "") {
2383
+ this._destroyed = true;
2384
+ this._clearReconnectTimer();
2385
+ this._authPending = false;
2386
+ if (this._authTimer !== null) {
2387
+ clearTimeout(this._authTimer);
2388
+ this._authTimer = null;
2389
+ }
2390
+ this._authResolve = null;
2391
+ this._authReject = null;
2392
+ if (this._ws === null || this.state === WsClientState.CLOSED) {
2393
+ this.state = WsClientState.CLOSED;
2394
+ return Promise.resolve();
2395
+ }
2396
+ return new Promise((resolve) => {
2397
+ const ws = this._ws;
2398
+ const onClose = () => {
2399
+ ws.removeEventListener("close", onClose);
2400
+ resolve();
2401
+ };
2402
+ ws.addEventListener("close", onClose);
2403
+ this.state = WsClientState.CLOSING;
2404
+ ws.close(code, reason);
2405
+ });
2406
+ }
2407
+ // ── Private: Connection lifecycle ─────────────────────────────────────────
2408
+ _openConnection() {
2409
+ this._destroyed = false;
2410
+ this.state = WsClientState.CONNECTING;
2411
+ return new Promise((resolve, reject) => {
2412
+ let settled = false;
2413
+ const ws = new WebSocket(this._url);
2414
+ ws.binaryType = "arraybuffer";
2415
+ this._ws = ws;
2416
+ ws.addEventListener("open", this._handleOpen);
2417
+ ws.addEventListener("message", this._handleMessage);
2418
+ ws.addEventListener("close", this._handleClose);
2419
+ ws.addEventListener("error", this._handleError);
2420
+ const onOpenOnce = () => {
2421
+ if (settled) return;
2422
+ settled = true;
2423
+ ws.removeEventListener("open", onOpenOnce);
2424
+ ws.removeEventListener("error", onErrorOnce);
2425
+ if (this._authFrame !== null) {
2426
+ this._authResolve = resolve;
2427
+ this._authReject = reject;
2428
+ return;
2429
+ }
2430
+ resolve();
2431
+ };
2432
+ const onErrorOnce = (e) => {
2433
+ if (settled) return;
2434
+ settled = true;
2435
+ ws.removeEventListener("open", onOpenOnce);
2436
+ ws.removeEventListener("error", onErrorOnce);
2437
+ reject(new Error(`WSClient: connection failed \u2014 ${e.message ?? "unknown error"}`));
2438
+ };
2439
+ ws.addEventListener("open", onOpenOnce);
2440
+ ws.addEventListener("error", onErrorOnce);
2441
+ });
2442
+ }
2443
+ /** Open handler for auth mode — sends pre-encoded AUTH frame */
2444
+ _onOpenAuth() {
2445
+ this.state = WsClientState.OPEN;
2446
+ this._retries = 0;
2447
+ this._authPending = true;
2448
+ sendBinary(this._ws, this._authFrame);
2449
+ this._authTimer = setTimeout(() => {
2450
+ this._authTimer = null;
2451
+ this._authPending = false;
2452
+ const reject = this._authReject;
2453
+ this._authResolve = null;
2454
+ this._authReject = null;
2455
+ reject?.(new Error("WSClient: auth timeout"));
2456
+ this._ws?.close(4001, "Auth timeout");
2457
+ }, this._authTimeout);
2458
+ }
2459
+ /** Open handler for no-auth mode — proceeds directly to _finishOpen */
2460
+ _onOpenNoAuth() {
2461
+ this.state = WsClientState.OPEN;
2462
+ this._retries = 0;
2463
+ this._finishOpen();
2464
+ }
2465
+ /**
2466
+ * Complete the open sequence: re-subscribe, start heartbeat, fire onConnect.
2467
+ * Called directly from _onOpen (no auth) or from _onMessage after AUTH_OK.
2468
+ */
2469
+ _finishOpen() {
2470
+ if (this._subscriptions.size > 0) {
2471
+ for (const topic of this._subscriptions.keys()) {
2472
+ sendBinary(this._ws, encodeSubscribe(topic));
2473
+ }
2474
+ }
2475
+ this._startHeartbeat();
2476
+ this._onConnect?.();
2477
+ }
2478
+ _onMessage(event) {
2479
+ const raw = event.data;
2480
+ this._onData?.(raw);
2481
+ if (raw instanceof ArrayBuffer) {
2482
+ if (raw.byteLength === 0) return;
2483
+ const frame = decode(raw);
2484
+ if (frame === null) return;
2485
+ this._onFrame?.(frame);
2486
+ switch (frame.type) {
2487
+ // ── Auth response handling ────────────────────────────────────
2488
+ case MsgType.AUTH_OK: {
2489
+ if (!this._authPending) return;
2490
+ this._authPending = false;
2491
+ if (this._authTimer !== null) {
2492
+ clearTimeout(this._authTimer);
2493
+ this._authTimer = null;
2494
+ }
2495
+ const resolve = this._authResolve;
2496
+ this._authResolve = null;
2497
+ this._authReject = null;
2498
+ this._finishOpen();
2499
+ resolve?.();
2500
+ return;
2501
+ }
2502
+ case MsgType.AUTH_FAIL: {
2503
+ if (!this._authPending) return;
2504
+ this._authPending = false;
2505
+ if (this._authTimer !== null) {
2506
+ clearTimeout(this._authTimer);
2507
+ this._authTimer = null;
2508
+ }
2509
+ const reject = this._authReject;
2510
+ this._authResolve = null;
2511
+ this._authReject = null;
2512
+ const reason = frame.payload || "Authentication failed";
2513
+ reject?.(new Error(`WSClient: auth failed \u2014 ${reason}`));
2514
+ return;
2515
+ }
2516
+ // ── Response correlation ───────────────────────────────────────
2517
+ case MsgType.RESPONSE: {
2518
+ const pending = this._pending.get(frame.corrId);
2519
+ if (pending !== void 0) {
2520
+ this._pending.delete(frame.corrId);
2521
+ clearTimeout(pending.timer);
2522
+ let parsed;
2523
+ try {
2524
+ parsed = JSON.parse(frame.payload);
2525
+ } catch {
2526
+ parsed = frame.payload;
2527
+ }
2528
+ pending.resolve(parsed);
2529
+ }
2530
+ return;
2531
+ }
2532
+ // ── Pub/sub topic message ─────────────────────────────────────
2533
+ case MsgType.MESSAGE: {
2534
+ const cb = this._subscriptions.get(frame.topic);
2535
+ if (cb !== void 0) {
2536
+ let data;
2537
+ try {
2538
+ data = JSON.parse(frame.payload);
2539
+ } catch {
2540
+ data = frame.payload;
2541
+ }
2542
+ cb(data);
2543
+ }
2544
+ return;
2545
+ }
2546
+ // ── Pong (heartbeat ack) — no action needed ───────────────────
2547
+ case MsgType.PONG:
2548
+ return;
2549
+ // Other binary frame types are not expected client-side
2550
+ default:
2551
+ return;
2552
+ }
2553
+ }
2554
+ if (typeof raw === "string" && this._subscriptions.size > 0) {
2555
+ if (raw.charCodeAt(0) === 123 && raw.includes('"message"')) {
2556
+ let msg = null;
2557
+ try {
2558
+ msg = JSON.parse(raw);
2559
+ } catch {
2560
+ }
2561
+ if (msg?.type === "message" && typeof msg.topic === "string") {
2562
+ const cb = this._subscriptions.get(msg.topic);
2563
+ cb?.(msg.data);
2564
+ }
2565
+ }
2566
+ }
2567
+ }
2568
+ _onClose(event) {
2569
+ this._stopHeartbeat();
2570
+ this._detachHandlers();
2571
+ this._ws = null;
2572
+ this._authPending = false;
2573
+ if (this._authTimer !== null) {
2574
+ clearTimeout(this._authTimer);
2575
+ this._authTimer = null;
2576
+ }
2577
+ if (this._authReject) {
2578
+ const reject = this._authReject;
2579
+ this._authResolve = null;
2580
+ this._authReject = null;
2581
+ reject(new Error(`WSClient: connection closed during auth (${event.code})`));
2582
+ }
2583
+ const code = event.code;
2584
+ const reason = event.reason ?? "";
2585
+ if (this._pending.size > 0) {
2586
+ const err = new Error(`WSClient: connection closed (${code})`);
2587
+ for (const pending of this._pending.values()) {
2588
+ clearTimeout(pending.timer);
2589
+ pending.reject(err);
2590
+ }
2591
+ this._pending.clear();
2592
+ }
2593
+ this._onDisconnect?.(code, reason);
2594
+ if (this._destroyed || code === 1e3 || code === 1001) {
2595
+ this.state = WsClientState.CLOSED;
2596
+ return;
2597
+ }
2598
+ this._scheduleReconnect();
2599
+ }
2600
+ _onError(_event) {
2601
+ }
2602
+ // ── Private: Reconnect ────────────────────────────────────────────────────
2603
+ _scheduleReconnect() {
2604
+ if (this._retries >= this._maxRetries) {
2605
+ this.state = WsClientState.CLOSED;
2606
+ return;
2607
+ }
2608
+ this.state = WsClientState.RECONNECTING;
2609
+ this._retries++;
2610
+ const delay = this._computeBackoff(this._retries);
2611
+ this._onReconnectAttempt?.(this._retries, delay);
2612
+ this._reconnectTimer = setTimeout(async () => {
2613
+ this._reconnectTimer = null;
2614
+ if (this._destroyed) return;
2615
+ try {
2616
+ await this._openConnection();
2617
+ } catch {
2618
+ }
2619
+ }, delay);
2620
+ }
2621
+ /**
2622
+ * Exponential backoff with full jitter to avoid thundering-herd reconnects.
2623
+ * delay = random(0, min(backoffMax, backoffBase * backoffFactor^attempt))
2624
+ */
2625
+ _computeBackoff(attempt) {
2626
+ const cap = Math.min(this._backoffMax, this._backoffBase * Math.pow(this._backoffFactor, attempt - 1));
2627
+ return Math.floor(Math.random() * cap);
2628
+ }
2629
+ _clearReconnectTimer() {
2630
+ if (this._reconnectTimer !== null) {
2631
+ clearTimeout(this._reconnectTimer);
2632
+ this._reconnectTimer = null;
2633
+ }
2634
+ }
2635
+ // ── Private: Heartbeat ────────────────────────────────────────────────────
2636
+ _startHeartbeat() {
2637
+ if (this._heartbeatInterval <= 0) return;
2638
+ this._stopHeartbeat();
2639
+ this._heartbeatTimer = setInterval(() => {
2640
+ if (this.state === WsClientState.OPEN && this._ws !== null) {
2641
+ sendBinary(this._ws, PING_FRAME);
2642
+ }
2643
+ }, this._heartbeatInterval);
2644
+ }
2645
+ _stopHeartbeat() {
2646
+ if (this._heartbeatTimer !== null) {
2647
+ clearInterval(this._heartbeatTimer);
2648
+ this._heartbeatTimer = null;
2649
+ }
2650
+ }
2651
+ // ── Private: Handler cleanup ──────────────────────────────────────────────
2652
+ _detachHandlers() {
2653
+ if (this._ws === null) return;
2654
+ this._ws.removeEventListener("open", this._handleOpen);
2655
+ this._ws.removeEventListener("message", this._handleMessage);
2656
+ this._ws.removeEventListener("close", this._handleClose);
2657
+ this._ws.removeEventListener("error", this._handleError);
2658
+ }
2659
+ // ── Private: Utilities ────────────────────────────────────────────────────
2660
+ /**
2661
+ * Returns a monotonically increasing uint32 correlation ID.
2662
+ * Wraps at 0xFFFFFFFF back to 1 (zero is reserved).
2663
+ * No random component needed — IDs are scoped to a single connection
2664
+ * and the uint32 space (4B values) far exceeds concurrent in-flight requests.
2665
+ */
2666
+ _nextId() {
2667
+ this._idCounter = this._idCounter + 1 & 4294967295;
2668
+ if (this._idCounter === 0) this._idCounter = 1;
2669
+ return this._idCounter;
2670
+ }
2671
+ };
2672
+ export {
2673
+ App,
2674
+ AppServer,
2675
+ MsgType,
2676
+ WSClient,
2677
+ WsClientState,
2678
+ WsReadyState,
2679
+ WsTopicHub,
2680
+ basicAuth,
2681
+ bearerAuth,
2682
+ bodyLimit,
2683
+ cache,
2684
+ compress,
2685
+ compressString,
2686
+ cors,
2687
+ csrf,
2688
+ decode as decodeFrame,
2689
+ decodeJwt,
2690
+ decompressString,
2691
+ decryptString,
2692
+ defaultErrorHandler,
2693
+ encodeMessage,
2694
+ encodePing,
2695
+ encodePong,
2696
+ encodePublish,
2697
+ encodeRequest,
2698
+ encodeResponse,
2699
+ encodeSubscribe,
2700
+ encodeUnsubscribe,
2701
+ encryptString,
2702
+ etag,
2703
+ generateSecretKey,
2704
+ getMimeType,
2705
+ getPathname,
2706
+ ipRestriction,
2707
+ isBun,
2708
+ isDeno,
2709
+ isKenBinaryFrame,
2710
+ isNode,
2711
+ jwk,
2712
+ jwt,
2713
+ listDirectory,
2714
+ logger,
2715
+ memoize,
2716
+ receiveFiles,
2717
+ requestId,
2718
+ saveFile,
2719
+ secureHeaders,
2720
+ sendFile,
2721
+ session,
2722
+ signJwt,
2723
+ timeout,
2724
+ timing,
2725
+ verifyJwt,
2726
+ wsClientProtocol
2727
+ };
2728
+ //# sourceMappingURL=index.js.map