@wooksjs/event-http 0.6.6 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,31 +1,21 @@
1
- import { attachHook, createAsyncEventContext, useAsyncEventContext, useEventId, useEventLogger, useEventLogger as useEventLogger$1, useRouteParams } from "@wooksjs/event-core";
1
+ import { EventContext, cached, cachedBy, current, defineEventKind, defineWook, routeParamsKey, run, slot, useEventId, useLogger, useRouteParams } from "@wooksjs/event-core";
2
2
  import { Buffer as Buffer$1 } from "buffer";
3
3
  import { Readable, pipeline } from "node:stream";
4
4
  import { promisify } from "node:util";
5
5
  import { createBrotliCompress, createBrotliDecompress, createDeflate, createGunzip, createGzip, createInflate } from "node:zlib";
6
6
  import { URLSearchParams } from "url";
7
- import http from "http";
8
- import { WooksAdapterBase } from "wooks";
9
7
  import { Readable as Readable$1 } from "stream";
8
+ import http, { IncomingMessage, ServerResponse } from "http";
9
+ import { WooksAdapterBase } from "wooks";
10
+ import { Socket } from "net";
10
11
 
11
- //#region packages/event-http/src/event-http.ts
12
- /** Creates an async event context for an incoming HTTP request/response pair. */
13
- function createHttpContext(data, options) {
14
- return createAsyncEventContext({
15
- event: {
16
- ...data,
17
- type: "HTTP"
18
- },
19
- options
20
- });
21
- }
22
- /**
23
- * Wrapper on useEventContext with HTTP event types
24
- * @returns set of hooks { getCtx, restoreCtx, clearCtx, hookStore, getStore, setStore }
25
- */
26
- function useHttpContext() {
27
- return useAsyncEventContext("HTTP");
28
- }
12
+ //#region packages/event-http/src/http-kind.ts
13
+ /** Event kind definition for HTTP requests. Provides typed context slots for `req`, `response`, and `requestLimits`. */
14
+ const httpKind = defineEventKind("http", {
15
+ req: slot(),
16
+ response: slot(),
17
+ requestLimits: slot()
18
+ });
29
19
 
30
20
  //#endregion
31
21
  //#region packages/event-http/src/utils/helpers.ts
@@ -45,53 +35,114 @@ function safeDecodeURIComponent(uri) {
45
35
  }
46
36
 
47
37
  //#endregion
48
- //#region packages/event-http/src/utils/time.ts
49
- function convertTime(time, unit = "ms") {
50
- if (typeof time === "number") return time / units[unit];
51
- const rg = /(\d+)(\w+)/gu;
52
- let t = 0;
53
- let r;
54
- while (r = rg.exec(time)) t += Number(r[1]) * (units[r[2]] || 0);
55
- return t / units[unit];
38
+ //#region packages/event-http/src/composables/cookies.ts
39
+ const cookieRegExpCache = /* @__PURE__ */ new Map();
40
+ function getCookieRegExp(name) {
41
+ let re = cookieRegExpCache.get(name);
42
+ if (!re) {
43
+ re = new RegExp(`(?:^|; )${escapeRegex(name)}=(.*?)(?:;?$|; )`, "i");
44
+ cookieRegExpCache.set(name, re);
45
+ }
46
+ return re;
56
47
  }
57
- const units = {
58
- ms: 1,
59
- s: 1e3,
60
- m: 1e3 * 60,
61
- h: 1e3 * 60 * 60,
62
- d: 1e3 * 60 * 60 * 24,
63
- w: 1e3 * 60 * 60 * 24 * 7,
64
- M: 1e3 * 60 * 60 * 24 * 30,
65
- Y: 1e3 * 60 * 60 * 24 * 365
48
+ const parseCookieValue = cachedBy((name, ctx) => {
49
+ const cookie = ctx.get(httpKind.keys.req).headers.cookie;
50
+ if (cookie) {
51
+ const result = getCookieRegExp(name).exec(cookie);
52
+ return result?.[1] ? safeDecodeURIComponent(result[1]) : null;
53
+ }
54
+ return null;
55
+ });
56
+ /**
57
+ * Provides access to parsed request cookies.
58
+ * @example
59
+ * ```ts
60
+ * const { getCookie, raw } = useCookies()
61
+ * const sessionId = getCookie('session_id')
62
+ * ```
63
+ */
64
+ const useCookies = defineWook((ctx) => ({
65
+ raw: ctx.get(httpKind.keys.req).headers.cookie,
66
+ getCookie: (name) => parseCookieValue(name, ctx)
67
+ }));
68
+
69
+ //#endregion
70
+ //#region packages/event-http/src/composables/header-accept.ts
71
+ const ACCEPT_TYPE_MAP = {
72
+ json: "application/json",
73
+ html: "text/html",
74
+ xml: "application/xml",
75
+ text: "text/plain"
66
76
  };
77
+ const acceptsMime = cachedBy((type, ctx) => {
78
+ const accept = ctx.get(httpKind.keys.req).headers.accept;
79
+ const mime = ACCEPT_TYPE_MAP[type] || type;
80
+ return !!(accept && (accept === "*/*" || accept.includes(mime)));
81
+ });
82
+ /** Provides helpers to check the request's Accept header for supported MIME types. */
83
+ const useAccept = defineWook((ctx) => {
84
+ const accept = ctx.get(httpKind.keys.req).headers.accept;
85
+ return {
86
+ accept,
87
+ has: (type) => acceptsMime(type, ctx)
88
+ };
89
+ });
67
90
 
68
91
  //#endregion
69
- //#region packages/event-http/src/utils/set-cookie.ts
70
- const COOKIE_NAME_RE = /^[\w!#$%&'*+\-.^`|~]+$/;
71
- function sanitizeCookieAttrValue(v) {
72
- return v.replace(/[;\r\n]/g, "");
73
- }
74
- function renderCookie(key, data) {
75
- if (!COOKIE_NAME_RE.test(key)) throw new TypeError(`Invalid cookie name "${key}"`);
76
- let attrs = "";
77
- for (const [a, v] of Object.entries(data.attrs)) {
78
- const func = cookieAttrFunc[a];
79
- if (typeof func === "function") {
80
- const val = func(v);
81
- attrs += val ? `; ${val}` : "";
82
- } else throw new TypeError(`Unknown Set-Cookie attribute ${a}`);
92
+ //#region packages/event-http/src/composables/header-authorization.ts
93
+ const authTypeSlot = cached((ctx) => {
94
+ const authorization = ctx.get(httpKind.keys.req).headers.authorization;
95
+ if (authorization) {
96
+ const space = authorization.indexOf(" ");
97
+ return authorization.slice(0, space);
98
+ }
99
+ return null;
100
+ });
101
+ const authCredentialsSlot = cached((ctx) => {
102
+ const authorization = ctx.get(httpKind.keys.req).headers.authorization;
103
+ if (authorization) {
104
+ const space = authorization.indexOf(" ");
105
+ return authorization.slice(space + 1);
106
+ }
107
+ return null;
108
+ });
109
+ const basicCredentialsSlot = cached((ctx) => {
110
+ const authorization = ctx.get(httpKind.keys.req).headers.authorization;
111
+ if (authorization) {
112
+ const type = ctx.get(authTypeSlot);
113
+ if (type?.toLocaleLowerCase() === "basic") {
114
+ const creds = Buffer$1.from(ctx.get(authCredentialsSlot) || "", "base64").toString("ascii");
115
+ const [username, password] = creds.split(":");
116
+ return {
117
+ username,
118
+ password
119
+ };
120
+ }
83
121
  }
84
- return `${key}=${encodeURIComponent(data.value)}${attrs}`;
85
- }
86
- const cookieAttrFunc = {
87
- expires: (v) => `Expires=${typeof v === "string" || typeof v === "number" ? new Date(v).toUTCString() : v.toUTCString()}`,
88
- maxAge: (v) => `Max-Age=${convertTime(v, "s").toString()}`,
89
- domain: (v) => `Domain=${sanitizeCookieAttrValue(String(v))}`,
90
- path: (v) => `Path=${sanitizeCookieAttrValue(String(v))}`,
91
- secure: (v) => v ? "Secure" : "",
92
- httpOnly: (v) => v ? "HttpOnly" : "",
93
- sameSite: (v) => v ? `SameSite=${typeof v === "string" ? v : "Strict"}` : ""
94
- };
122
+ return null;
123
+ });
124
+ const authIsSlot = cachedBy((type, ctx) => {
125
+ const authType = ctx.get(authTypeSlot);
126
+ return authType?.toLowerCase() === type.toLowerCase();
127
+ });
128
+ /**
129
+ * Provides parsed access to the Authorization header (type, credentials, Basic decoding).
130
+ * @example
131
+ * ```ts
132
+ * const { is, credentials, basicCredentials } = useAuthorization()
133
+ * if (is('bearer')) { const token = credentials() }
134
+ * ```
135
+ */
136
+ const useAuthorization = defineWook((ctx) => {
137
+ const authorization = ctx.get(httpKind.keys.req).headers.authorization;
138
+ return {
139
+ authorization,
140
+ type: () => ctx.get(authTypeSlot),
141
+ credentials: () => ctx.get(authCredentialsSlot),
142
+ is: (type) => authIsSlot(type, ctx),
143
+ basicCredentials: () => ctx.get(basicCredentialsSlot)
144
+ };
145
+ });
95
146
 
96
147
  //#endregion
97
148
  //#region packages/event-http/src/compressor/body-compressor.ts
@@ -169,26 +220,9 @@ compressors.deflate.uncompress = async (b) => (await zlib()).inflate(b);
169
220
  compressors.br.compress = async (b) => (await zlib()).brotliCompress(b);
170
221
  compressors.br.uncompress = async (b) => (await zlib()).brotliDecompress(b);
171
222
 
172
- //#endregion
173
- //#region packages/event-http/src/response/renderer.ts
174
- var BaseHttpResponseRenderer = class {
175
- render(response) {
176
- if (typeof response.body === "string" || typeof response.body === "boolean" || typeof response.body === "number") {
177
- if (!response.getContentType()) response.setContentType("text/plain");
178
- return response.body.toString();
179
- }
180
- if (response.body === void 0) return "";
181
- if (response.body instanceof Uint8Array) return response.body;
182
- if (typeof response.body === "object") {
183
- if (!response.getContentType()) response.setContentType("application/json");
184
- return JSON.stringify(response.body);
185
- }
186
- throw new Error(`Unsupported body format "${typeof response.body}"`);
187
- }
188
- };
189
-
190
223
  //#endregion
191
224
  //#region packages/event-http/src/utils/status-codes.ts
225
+ /** Maps numeric HTTP status codes to their human-readable descriptions. */
192
226
  const httpStatusCodes = {
193
227
  100: "Continue",
194
228
  101: "Switching protocols",
@@ -254,6 +288,7 @@ const httpStatusCodes = {
254
288
  510: "Not Extended",
255
289
  511: "Network Authentication Required"
256
290
  };
291
+ /** Enum of all standard HTTP status codes (100–511). */
257
292
  let EHttpStatusCode = /* @__PURE__ */ function(EHttpStatusCode$1) {
258
293
  EHttpStatusCode$1[EHttpStatusCode$1["Continue"] = 100] = "Continue";
259
294
  EHttpStatusCode$1[EHttpStatusCode$1["SwitchingProtocols"] = 101] = "SwitchingProtocols";
@@ -322,45 +357,707 @@ let EHttpStatusCode = /* @__PURE__ */ function(EHttpStatusCode$1) {
322
357
  }({});
323
358
 
324
359
  //#endregion
325
- //#region packages/event-http/src/errors/403.tl.svg
326
- function _403_tl_default(ctx) {
327
- return `<svg height="64" viewBox="0 4 100 96" fill="none" xmlns="http://www.w3.org/2000/svg" stroke="#888888" stroke-width="2">
328
- <path d="M50 90.625C64.4042 87.1875 83.5751 69.8667 83.5751 48.6937V24.0854L50 9.375L16.425 24.0833V48.6937C16.425 69.8667 35.5959 87.1875 50 90.625Z" fill="#ff000050">
329
- <animate attributeName="fill" dur="2s" repeatCount="indefinite"
330
- values="#ff000000;#ff000050;#ff000000" />
331
- </path>
360
+ //#region packages/event-http/src/errors/http-error.ts
361
+ /** Represents an HTTP error with a status code and optional structured body. */
362
+ var HttpError = class extends Error {
363
+ name = "HttpError";
364
+ constructor(code = 500, _body = "") {
365
+ const prev = Error.stackTraceLimit;
366
+ Error.stackTraceLimit = 0;
367
+ super(typeof _body === "string" ? _body : _body.message);
368
+ this.code = code;
369
+ this._body = _body;
370
+ Error.stackTraceLimit = prev;
371
+ }
372
+ get body() {
373
+ return typeof this._body === "string" ? {
374
+ statusCode: this.code,
375
+ message: this.message,
376
+ error: httpStatusCodes[this.code]
377
+ } : {
378
+ ...this._body,
379
+ statusCode: this.code,
380
+ message: this.message,
381
+ error: httpStatusCodes[this.code]
382
+ };
383
+ }
384
+ };
332
385
 
333
- <path d="M61.5395 46.0812H38.4604C37.1061 46.0812 36.0083 47.1791 36.0083 48.5333V65.075C36.0083 66.4292 37.1061 67.5271 38.4604 67.5271H61.5395C62.8938 67.5271 63.9916 66.4292 63.9916 65.075V48.5333C63.9916 47.1791 62.8938 46.0812 61.5395 46.0812Z" />
386
+ //#endregion
387
+ //#region packages/event-http/src/composables/request.ts
388
+ const xForwardedFor = "x-forwarded-for";
389
+ /** Default safety limits for request body reading (size, ratio, timeout). */
390
+ const DEFAULT_LIMITS = {
391
+ maxCompressed: 1 * 1024 * 1024,
392
+ maxInflated: 10 * 1024 * 1024,
393
+ maxRatio: 100,
394
+ readTimeoutMs: 1e4
395
+ };
396
+ const contentEncodingsSlot = cached((ctx) => {
397
+ const req = ctx.get(httpKind.keys.req);
398
+ const contentEncoding = req.headers["content-encoding"];
399
+ return (contentEncoding || "").split(",").map((p) => p.trim()).filter((p) => !!p);
400
+ });
401
+ const isCompressedSlot = cached((ctx) => {
402
+ const parts = ctx.get(contentEncodingsSlot);
403
+ for (const p of parts) if ([
404
+ "deflate",
405
+ "gzip",
406
+ "br"
407
+ ].includes(p)) return true;
408
+ return false;
409
+ });
410
+ /** @internal Exported for test pre-seeding via `ctx.set(rawBodySlot, ...)`. */
411
+ const rawBodySlot = cached(async (ctx) => {
412
+ const req = ctx.get(httpKind.keys.req);
413
+ const encs = ctx.get(contentEncodingsSlot);
414
+ const isZip = ctx.get(isCompressedSlot);
415
+ const streamable = isZip && encodingSupportsStream(encs);
416
+ const limits = ctx.get(httpKind.keys.requestLimits);
417
+ const maxCompressed = limits?.maxCompressed ?? DEFAULT_LIMITS.maxCompressed;
418
+ const maxInflated = limits?.maxInflated ?? DEFAULT_LIMITS.maxInflated;
419
+ const maxRatio = limits?.maxRatio ?? DEFAULT_LIMITS.maxRatio;
420
+ const timeoutMs = limits?.readTimeoutMs ?? DEFAULT_LIMITS.readTimeoutMs;
421
+ const cl = Number(req.headers["content-length"] ?? 0);
422
+ const upfrontLimit = isZip ? maxCompressed : maxInflated;
423
+ if (cl && cl > upfrontLimit) throw new HttpError(413, "Payload Too Large");
424
+ for (const enc of encs) if (!compressors[enc]) throw new HttpError(415, `Unsupported Content-Encoding "${enc}"`);
425
+ let timer = null;
426
+ function resetTimer() {
427
+ if (timeoutMs === 0) return;
428
+ clearTimer();
429
+ timer = setTimeout(() => {
430
+ clearTimer();
431
+ req.destroy();
432
+ }, timeoutMs);
433
+ }
434
+ function clearTimer() {
435
+ if (timer) {
436
+ clearTimeout(timer);
437
+ timer = null;
438
+ }
439
+ }
440
+ let rawBytes = 0;
441
+ async function* limitedCompressed() {
442
+ resetTimer();
443
+ try {
444
+ for await (const chunk of req) {
445
+ rawBytes += chunk.length;
446
+ if (rawBytes > upfrontLimit) {
447
+ req.destroy();
448
+ throw new HttpError(413, "Payload Too Large");
449
+ }
450
+ resetTimer();
451
+ yield chunk;
452
+ }
453
+ } finally {
454
+ clearTimer();
455
+ }
456
+ }
457
+ let stream = limitedCompressed();
458
+ if (streamable) stream = await uncompressBodyStream(encs, stream);
459
+ const chunks = [];
460
+ let inflatedBytes = 0;
461
+ try {
462
+ for await (const chunk of stream) {
463
+ inflatedBytes += chunk.length;
464
+ if (inflatedBytes > maxInflated) throw new HttpError(413, "Inflated body too large");
465
+ chunks.push(chunk);
466
+ }
467
+ } catch (error) {
468
+ if (error instanceof HttpError) throw error;
469
+ throw new HttpError(408, "Request body timeout");
470
+ }
471
+ let body = Buffer$1.concat(chunks);
472
+ if (!streamable && isZip) {
473
+ body = await uncompressBody(encs, body);
474
+ inflatedBytes = body.byteLength;
475
+ if (inflatedBytes > maxInflated) throw new HttpError(413, "Inflated body too large");
476
+ }
477
+ if (isZip && rawBytes > 0 && inflatedBytes / rawBytes > maxRatio) throw new HttpError(413, "Compression ratio too high");
478
+ return body;
479
+ });
480
+ const forwardedIpSlot = cached((ctx) => {
481
+ const req = ctx.get(httpKind.keys.req);
482
+ if (typeof req.headers[xForwardedFor] === "string" && req.headers[xForwardedFor]) return req.headers[xForwardedFor].split(",").shift()?.trim();
483
+ return "";
484
+ });
485
+ const remoteIpSlot = cached((ctx) => {
486
+ const req = ctx.get(httpKind.keys.req);
487
+ return req.socket.remoteAddress || req.connection.remoteAddress || "";
488
+ });
489
+ const ipListSlot = cached((ctx) => {
490
+ const req = ctx.get(httpKind.keys.req);
491
+ return {
492
+ remoteIp: req.socket.remoteAddress || req.connection.remoteAddress || "",
493
+ forwarded: (req.headers[xForwardedFor] || "").split(",").map((s) => s.trim())
494
+ };
495
+ });
496
+ /**
497
+ * Provides access to the incoming HTTP request (method, url, headers, body, IP).
498
+ * @example
499
+ * ```ts
500
+ * const { method, url, raw, rawBody, getIp } = useRequest()
501
+ * const body = await rawBody()
502
+ * ```
503
+ */
504
+ const useRequest = defineWook((ctx) => {
505
+ const req = ctx.get(httpKind.keys.req);
506
+ const limits = () => ctx.get(httpKind.keys.requestLimits);
507
+ const setLimit = (limitKey, value) => {
508
+ let obj = limits();
509
+ if (!obj?.perRequest) {
510
+ obj = {
511
+ ...obj,
512
+ perRequest: true
513
+ };
514
+ ctx.set(httpKind.keys.requestLimits, obj);
515
+ }
516
+ obj[limitKey] = value;
517
+ };
518
+ const getMaxCompressed = () => limits()?.maxCompressed ?? DEFAULT_LIMITS.maxCompressed;
519
+ const setMaxCompressed = (limit) => setLimit("maxCompressed", limit);
520
+ const getMaxInflated = () => limits()?.maxInflated ?? DEFAULT_LIMITS.maxInflated;
521
+ const setMaxInflated = (limit) => setLimit("maxInflated", limit);
522
+ const getMaxRatio = () => limits()?.maxRatio ?? DEFAULT_LIMITS.maxRatio;
523
+ const setMaxRatio = (limit) => setLimit("maxRatio", limit);
524
+ const getReadTimeoutMs = () => limits()?.readTimeoutMs ?? DEFAULT_LIMITS.readTimeoutMs;
525
+ const setReadTimeoutMs = (limit) => setLimit("readTimeoutMs", limit);
526
+ function getIp(options) {
527
+ if (options?.trustProxy) return ctx.get(forwardedIpSlot) || ctx.get(remoteIpSlot);
528
+ return ctx.get(remoteIpSlot);
529
+ }
530
+ return {
531
+ raw: req,
532
+ url: req.url,
533
+ method: req.method,
534
+ headers: req.headers,
535
+ rawBody: () => ctx.get(rawBodySlot),
536
+ reqId: useEventId(ctx).getId,
537
+ getIp,
538
+ getIpList: () => ctx.get(ipListSlot),
539
+ isCompressed: () => ctx.get(isCompressedSlot),
540
+ getMaxCompressed,
541
+ setMaxCompressed,
542
+ getReadTimeoutMs,
543
+ setReadTimeoutMs,
544
+ getMaxInflated,
545
+ setMaxInflated,
546
+ getMaxRatio,
547
+ setMaxRatio
548
+ };
549
+ });
334
550
 
335
- <path d="M41.7834 46.0834V39.6813C41.7834 37.5021 42.6491 35.4121 44.1901 33.8712C45.731 32.3303 47.8209 31.4646 50.0001 31.4646C52.1793 31.4646 54.2693 32.3303 55.8102 33.8712C57.3511 35.4121 58.2168 37.5021 58.2168 39.6813V46.0813" />
336
- </svg>
337
- `;
551
+ //#endregion
552
+ //#region packages/event-http/src/composables/headers.ts
553
+ /**
554
+ * Returns the incoming request headers.
555
+ * @example
556
+ * ```ts
557
+ * const { host, authorization } = useHeaders()
558
+ * ```
559
+ */
560
+ function useHeaders(ctx) {
561
+ return useRequest(ctx).headers;
338
562
  }
339
563
 
340
564
  //#endregion
341
- //#region packages/event-http/src/errors/404.tl.svg
342
- function _404_tl_default(ctx) {
343
- return `<svg height="64" viewBox="0 20 100 64" fill="none" xmlns="http://www.w3.org/2000/svg">
344
- <defs>
345
- <path id="sheet" d="M86.5 36.5V47.5H97.5V92.5H52.5V36.5H86.5ZM63 68.5H87V67.5H63V68.5ZM63 63.5H87V62.5H63V63.5ZM63 58.5H87V57.5H63V58.5ZM96.793 46.5H87.5V37.207L96.793 46.5Z" fill="#88888833" stroke="#888888aa"/>
346
- </defs>
565
+ //#region packages/event-http/src/composables/response.ts
566
+ /**
567
+ * Returns the HttpResponse instance for the current request.
568
+ * All response operations (status, headers, cookies, cache control, sending)
569
+ * are methods on the returned object.
570
+ *
571
+ * @example
572
+ * ```ts
573
+ * const response = useResponse()
574
+ * response.status = 200
575
+ * response.setHeader('x-custom', 'value')
576
+ * response.setCookie('session', 'abc', { httpOnly: true })
577
+ * ```
578
+ */
579
+ function useResponse(ctx) {
580
+ return (ctx ?? current()).get(httpKind.keys.response);
581
+ }
347
582
 
348
- <g id="queue" transform="translate(-5 -10)">
349
- <use href="#sheet" opacity="0">
350
- <animateTransform attributeName="transform" type="translate"
351
- dur="3s" repeatCount="indefinite"
352
- keyTimes="0;0.1;0.32;0.42;1"
353
- values="30 0; -20 0; -20 0; -70 0; -70 0" />
354
- <animate attributeName="opacity"
355
- dur="3s" repeatCount="indefinite"
356
- keyTimes="0;0.1;0.32;0.42;1"
357
- values="0;1;1;0;0" />
358
- </use>
583
+ //#endregion
584
+ //#region packages/event-http/src/utils/url-search-params.ts
585
+ const ILLEGAL_KEYS = new Set([
586
+ "__proto__",
587
+ "constructor",
588
+ "prototype"
589
+ ]);
590
+ /**
591
+ * Extended `URLSearchParams` with safe JSON conversion.
592
+ *
593
+ * Rejects prototype-pollution keys (`__proto__`, `constructor`, `prototype`) and duplicate non-array keys.
594
+ * Array parameters are detected by a trailing `[]` in the key name (e.g. `tags[]=a&tags[]=b`).
595
+ */
596
+ var WooksURLSearchParams = class extends URLSearchParams {
597
+ /** Converts query parameters to a plain object. Array params (keys ending with `[]`) become `string[]`. */
598
+ toJson() {
599
+ const json = Object.create(null);
600
+ for (const [key, value] of this.entries()) if (isArrayParam(key)) {
601
+ const a = json[key] = json[key] || [];
602
+ a.push(value);
603
+ } else {
604
+ if (ILLEGAL_KEYS.has(key)) throw new HttpError(400, `Illegal key name "${key}"`);
605
+ if (key in json) throw new HttpError(400, `Duplicate key "${key}"`);
606
+ json[key] = value;
607
+ }
608
+ return json;
609
+ }
610
+ };
611
+ function isArrayParam(name) {
612
+ return name.endsWith("[]");
613
+ }
359
614
 
360
- <use href="#sheet" opacity="0">
361
- <animateTransform attributeName="transform" type="translate"
362
- dur="3s" begin="1s" repeatCount="indefinite"
363
- keyTimes="0;0.1;0.32;0.42;1"
615
+ //#endregion
616
+ //#region packages/event-http/src/composables/search-params.ts
617
+ const rawSearchParamsSlot = cached((ctx) => {
618
+ const url = ctx.get(httpKind.keys.req).url || "";
619
+ const i = url.indexOf("?");
620
+ return i >= 0 ? url.slice(i) : "";
621
+ });
622
+ const urlSearchParamsSlot = cached((ctx) => new WooksURLSearchParams(ctx.get(rawSearchParamsSlot)));
623
+ /**
624
+ * Provides access to URL search (query) parameters from the request.
625
+ * @example
626
+ * ```ts
627
+ * const { params, toJson } = useUrlParams()
628
+ * const page = params().get('page')
629
+ * ```
630
+ */
631
+ const useUrlParams = defineWook((ctx) => ({
632
+ raw: () => ctx.get(rawSearchParamsSlot),
633
+ params: () => ctx.get(urlSearchParamsSlot),
634
+ toJson: () => ctx.get(urlSearchParamsSlot).toJson()
635
+ }));
636
+
637
+ //#endregion
638
+ //#region packages/event-http/src/utils/time.ts
639
+ function convertTime(time, unit = "ms") {
640
+ if (typeof time === "number") return time / units[unit];
641
+ const rg = /(\d+)(\w+)/gu;
642
+ let t = 0;
643
+ let r;
644
+ while (r = rg.exec(time)) t += Number(r[1]) * (units[r[2]] || 0);
645
+ return t / units[unit];
646
+ }
647
+ const units = {
648
+ ms: 1,
649
+ s: 1e3,
650
+ m: 1e3 * 60,
651
+ h: 1e3 * 60 * 60,
652
+ d: 1e3 * 60 * 60 * 24,
653
+ w: 1e3 * 60 * 60 * 24 * 7,
654
+ M: 1e3 * 60 * 60 * 24 * 30,
655
+ Y: 1e3 * 60 * 60 * 24 * 365
656
+ };
657
+
658
+ //#endregion
659
+ //#region packages/event-http/src/utils/cache-control.ts
660
+ /** Renders a `TCacheControl` object into a `Cache-Control` header string. */
661
+ function renderCacheControl(data) {
662
+ let attrs = "";
663
+ for (const [a, v] of Object.entries(data)) {
664
+ if (v === void 0) continue;
665
+ const func = cacheControlFunc[a];
666
+ if (typeof func === "function") {
667
+ const val = func(v);
668
+ if (val) attrs += attrs ? `, ${val}` : val;
669
+ } else throw new TypeError(`Unknown Cache-Control attribute ${a}`);
670
+ }
671
+ return attrs;
672
+ }
673
+ const cacheControlFunc = {
674
+ mustRevalidate: (v) => v ? "must-revalidate" : "",
675
+ noCache: (v) => v ? typeof v === "string" ? `no-cache="${v}"` : "no-cache" : "",
676
+ noStore: (v) => v ? "no-store" : "",
677
+ noTransform: (v) => v ? "no-transform" : "",
678
+ public: (v) => v ? "public" : "",
679
+ private: (v) => v ? typeof v === "string" ? `private="${v}"` : "private" : "",
680
+ proxyRevalidate: (v) => v ? "proxy-revalidate" : "",
681
+ maxAge: (v) => `max-age=${convertTime(v, "s").toString()}`,
682
+ sMaxage: (v) => `s-maxage=${convertTime(v, "s").toString()}`
683
+ };
684
+
685
+ //#endregion
686
+ //#region packages/event-http/src/utils/set-cookie.ts
687
+ const COOKIE_NAME_RE = /^[\w!#$%&'*+\-.^`|~]+$/;
688
+ function sanitizeCookieAttrValue(v) {
689
+ return v.replace(/[;\r\n]/g, "");
690
+ }
691
+ function renderCookie(key, data) {
692
+ if (!COOKIE_NAME_RE.test(key)) throw new TypeError(`Invalid cookie name "${key}"`);
693
+ let attrs = "";
694
+ for (const [a, v] of Object.entries(data.attrs)) {
695
+ const func = cookieAttrFunc[a];
696
+ if (typeof func === "function") {
697
+ const val = func(v);
698
+ attrs += val ? `; ${val}` : "";
699
+ } else throw new TypeError(`Unknown Set-Cookie attribute ${a}`);
700
+ }
701
+ return `${key}=${encodeURIComponent(data.value)}${attrs}`;
702
+ }
703
+ const cookieAttrFunc = {
704
+ expires: (v) => `Expires=${typeof v === "string" || typeof v === "number" ? new Date(v).toUTCString() : v.toUTCString()}`,
705
+ maxAge: (v) => `Max-Age=${convertTime(v, "s").toString()}`,
706
+ domain: (v) => `Domain=${sanitizeCookieAttrValue(String(v))}`,
707
+ path: (v) => `Path=${sanitizeCookieAttrValue(String(v))}`,
708
+ secure: (v) => v ? "Secure" : "",
709
+ httpOnly: (v) => v ? "HttpOnly" : "",
710
+ sameSite: (v) => v ? `SameSite=${typeof v === "string" ? v : "Strict"}` : ""
711
+ };
712
+
713
+ //#endregion
714
+ //#region packages/event-http/src/response/http-response.ts
715
+ const hasFetchResponse = typeof globalThis.Response === "function";
716
+ const defaultStatus = {
717
+ GET: EHttpStatusCode.OK,
718
+ POST: EHttpStatusCode.Created,
719
+ PUT: EHttpStatusCode.Created,
720
+ PATCH: EHttpStatusCode.Accepted,
721
+ DELETE: EHttpStatusCode.Accepted
722
+ };
723
+ /**
724
+ * Manages response status, headers, cookies, cache control, and body for an HTTP request.
725
+ *
726
+ * All header mutations are accumulated in memory and flushed in a single `writeHead()` call
727
+ * when `send()` is invoked. Setter methods are chainable.
728
+ *
729
+ * @example
730
+ * ```ts
731
+ * const response = useResponse()
732
+ * response.setStatus(200).setHeader('x-custom', 'value')
733
+ * response.setCookie('session', 'abc', { httpOnly: true })
734
+ * ```
735
+ */
736
+ var HttpResponse = class {
737
+ /**
738
+ * @param _res - The underlying Node.js `ServerResponse`.
739
+ * @param _req - The underlying Node.js `IncomingMessage`.
740
+ * @param _logger - Logger instance for error reporting.
741
+ * @param defaultHeaders - Optional headers to pre-populate on this response (e.g. from `securityHeaders()`).
742
+ */
743
+ constructor(_res, _req, _logger, defaultHeaders) {
744
+ this._res = _res;
745
+ this._req = _req;
746
+ this._logger = _logger;
747
+ if (defaultHeaders) for (const key in defaultHeaders) this._headers[key] = defaultHeaders[key];
748
+ }
749
+ _status = 0;
750
+ _body = void 0;
751
+ _headers = {};
752
+ _cookies = {};
753
+ _rawCookies = [];
754
+ _hasCookies = false;
755
+ _responded = false;
756
+ /** The HTTP status code. If not set, it is inferred automatically when `send()` is called. */
757
+ get status() {
758
+ return this._status;
759
+ }
760
+ set status(value) {
761
+ this._status = value;
762
+ }
763
+ /** Sets the HTTP status code (chainable). */
764
+ setStatus(value) {
765
+ this._status = value;
766
+ return this;
767
+ }
768
+ /** The response body. Automatically serialized by `send()` (objects → JSON, strings → text). */
769
+ get body() {
770
+ return this._body;
771
+ }
772
+ set body(value) {
773
+ this._body = value;
774
+ }
775
+ /** Sets the response body (chainable). */
776
+ setBody(value) {
777
+ this._body = value;
778
+ return this;
779
+ }
780
+ /** Sets a single response header (chainable). Arrays produce multi-value headers. */
781
+ setHeader(name, value) {
782
+ this._headers[name] = Array.isArray(value) ? value : value.toString();
783
+ return this;
784
+ }
785
+ /** Batch-sets multiple response headers from a record (chainable). Existing keys are overwritten. */
786
+ setHeaders(headers) {
787
+ for (const key in headers) this._headers[key] = headers[key];
788
+ return this;
789
+ }
790
+ /** Returns the value of a response header, or `undefined` if not set. */
791
+ getHeader(name) {
792
+ return this._headers[name];
793
+ }
794
+ /** Removes a response header (chainable). */
795
+ removeHeader(name) {
796
+ delete this._headers[name];
797
+ return this;
798
+ }
799
+ /** Returns a read-only snapshot of all response headers. */
800
+ headers() {
801
+ return this._headers;
802
+ }
803
+ /** Sets the `Content-Type` response header (chainable). */
804
+ setContentType(value) {
805
+ this._headers["content-type"] = value;
806
+ return this;
807
+ }
808
+ /** Returns the current `Content-Type` header value. */
809
+ getContentType() {
810
+ return this._headers["content-type"];
811
+ }
812
+ /** Sets the `Access-Control-Allow-Origin` header (chainable). Defaults to `'*'`. */
813
+ enableCors(origin = "*") {
814
+ this._headers["access-control-allow-origin"] = origin;
815
+ return this;
816
+ }
817
+ /** Sets an outgoing `Set-Cookie` header with optional attributes (chainable). */
818
+ setCookie(name, value, attrs) {
819
+ this._cookies[name] = {
820
+ value,
821
+ attrs: attrs || {}
822
+ };
823
+ this._hasCookies = true;
824
+ return this;
825
+ }
826
+ /** Returns a previously set cookie's data, or `undefined` if not set. */
827
+ getCookie(name) {
828
+ return this._cookies[name];
829
+ }
830
+ /** Removes a cookie from the outgoing set list (chainable). */
831
+ removeCookie(name) {
832
+ delete this._cookies[name];
833
+ return this;
834
+ }
835
+ /** Removes all outgoing cookies (chainable). */
836
+ clearCookies() {
837
+ this._cookies = {};
838
+ this._rawCookies = [];
839
+ this._hasCookies = false;
840
+ return this;
841
+ }
842
+ /** Appends a raw `Set-Cookie` header string (chainable). Use when you need full control over the cookie format. */
843
+ setCookieRaw(rawValue) {
844
+ this._rawCookies.push(rawValue);
845
+ this._hasCookies = true;
846
+ return this;
847
+ }
848
+ /** Sets the `Cache-Control` header from a directive object (chainable). */
849
+ setCacheControl(data) {
850
+ this._headers["cache-control"] = renderCacheControl(data);
851
+ return this;
852
+ }
853
+ /** Sets the `Age` header in seconds (chainable). Accepts a number or time string (e.g. `'2h 15m'`). */
854
+ setAge(value) {
855
+ this._headers.age = convertTime(value, "s").toString();
856
+ return this;
857
+ }
858
+ /** Sets the `Expires` header (chainable). Accepts a `Date`, date string, or timestamp. */
859
+ setExpires(value) {
860
+ this._headers.expires = typeof value === "string" || typeof value === "number" ? new Date(value).toUTCString() : value.toUTCString();
861
+ return this;
862
+ }
863
+ /** Sets or clears the `Pragma: no-cache` header (chainable). */
864
+ setPragmaNoCache(value = true) {
865
+ this._headers.pragma = value ? "no-cache" : "";
866
+ return this;
867
+ }
868
+ /**
869
+ * Returns the underlying Node.js `ServerResponse`.
870
+ * @param passthrough - If `true`, the framework still manages the response lifecycle. If `false` (default), the response is marked as "responded" and the framework will not touch it.
871
+ */
872
+ getRawRes(passthrough) {
873
+ if (!passthrough) this._responded = true;
874
+ return this._res;
875
+ }
876
+ /** Whether the response has already been sent (or the underlying stream is no longer writable). */
877
+ get responded() {
878
+ return this._responded || !this._res.writable || this._res.writableEnded;
879
+ }
880
+ renderBody() {
881
+ const body = this._body;
882
+ if (body === void 0 || body === null) return "";
883
+ if (typeof body === "string") {
884
+ if (!this._headers["content-type"]) this._headers["content-type"] = "text/plain";
885
+ return body;
886
+ }
887
+ if (typeof body === "boolean" || typeof body === "number") {
888
+ if (!this._headers["content-type"]) this._headers["content-type"] = "text/plain";
889
+ return body.toString();
890
+ }
891
+ if (body instanceof Uint8Array) return body;
892
+ if (typeof body === "object") {
893
+ if (!this._headers["content-type"]) this._headers["content-type"] = "application/json";
894
+ return JSON.stringify(body);
895
+ }
896
+ throw new Error(`Unsupported body format "${typeof body}"`);
897
+ }
898
+ renderError(data, _ctx) {
899
+ this._status = data.statusCode || 500;
900
+ this._headers["content-type"] = "application/json";
901
+ this._body = JSON.stringify(data);
902
+ }
903
+ /** Renders and sends an HTTP error response. Called automatically by the framework when a handler throws an `HttpError`. */
904
+ sendError(error, ctx) {
905
+ const data = error.body;
906
+ this.renderError(data, ctx);
907
+ return this.send();
908
+ }
909
+ /**
910
+ * Finalizes and sends the response.
911
+ *
912
+ * Flushes all accumulated headers (including cookies) in a single `writeHead()` call,
913
+ * then writes the body. Supports `Readable` streams, `fetch` `Response` objects, and regular values.
914
+ *
915
+ * @throws Error if the response was already sent.
916
+ */
917
+ send() {
918
+ if (this._responded) {
919
+ const err = /* @__PURE__ */ new Error("The response was already sent.");
920
+ this._logger.error(err.message, err);
921
+ throw err;
922
+ }
923
+ this._responded = true;
924
+ this.finalizeCookies();
925
+ const body = this._body;
926
+ const method = this._req.method;
927
+ if (body instanceof Readable$1) return this.sendStream(body, method);
928
+ if (hasFetchResponse && body instanceof Response) return this.sendFetchResponse(body, method);
929
+ this.sendRegular(method);
930
+ }
931
+ finalizeCookies() {
932
+ if (!this._hasCookies) return;
933
+ const entries = Object.entries(this._cookies);
934
+ const rendered = [];
935
+ for (const [name, data] of entries) if (data) rendered.push(renderCookie(name, data));
936
+ if (this._rawCookies.length > 0) rendered.push(...this._rawCookies);
937
+ if (rendered.length > 0) {
938
+ const existing = this._headers["set-cookie"];
939
+ if (existing) this._headers["set-cookie"] = [...Array.isArray(existing) ? existing : [existing], ...rendered];
940
+ else this._headers["set-cookie"] = rendered;
941
+ }
942
+ }
943
+ autoStatus(hasBody) {
944
+ if (this._status) return;
945
+ if (!hasBody) {
946
+ this._status = EHttpStatusCode.NoContent;
947
+ return;
948
+ }
949
+ this._status = defaultStatus[this._req.method] || EHttpStatusCode.OK;
950
+ }
951
+ sendStream(stream, method) {
952
+ this.autoStatus(true);
953
+ this._res.writeHead(this._status, this._headers);
954
+ this._req.once("close", () => {
955
+ stream.destroy();
956
+ });
957
+ if (method === "HEAD") {
958
+ stream.destroy();
959
+ this._res.end();
960
+ return Promise.resolve();
961
+ }
962
+ return new Promise((resolve, reject) => {
963
+ stream.on("error", (e) => {
964
+ this._logger.error("Stream error", e);
965
+ stream.destroy();
966
+ this._res.end();
967
+ reject(e);
968
+ });
969
+ stream.on("close", () => {
970
+ stream.destroy();
971
+ resolve();
972
+ });
973
+ stream.pipe(this._res);
974
+ });
975
+ }
976
+ async sendFetchResponse(fetchResponse, method) {
977
+ this._status = this._status || fetchResponse.status;
978
+ const fetchContentLength = fetchResponse.headers.get("content-length");
979
+ if (fetchContentLength) this._headers["content-length"] = fetchContentLength;
980
+ const fetchContentType = fetchResponse.headers.get("content-type");
981
+ if (fetchContentType) this._headers["content-type"] = fetchContentType;
982
+ this._res.writeHead(this._status, this._headers);
983
+ if (method === "HEAD") {
984
+ this._res.end();
985
+ return;
986
+ }
987
+ const fetchBody = fetchResponse.body;
988
+ if (fetchBody) try {
989
+ for await (const chunk of fetchBody) this._res.write(chunk);
990
+ } catch (error) {
991
+ this._logger.error("Error streaming fetch response body", error);
992
+ }
993
+ if (!this._res.writableEnded) this._res.end();
994
+ }
995
+ sendRegular(method) {
996
+ const renderedBody = this.renderBody();
997
+ this.autoStatus(!!renderedBody);
998
+ const contentLength = typeof renderedBody === "string" ? Buffer.byteLength(renderedBody) : renderedBody.byteLength;
999
+ this._headers["content-length"] = contentLength.toString();
1000
+ this._res.writeHead(this._status, this._headers).end(method === "HEAD" ? "" : renderedBody);
1001
+ }
1002
+ };
1003
+
1004
+ //#endregion
1005
+ //#region packages/event-http/src/event-http.ts
1006
+ /** Creates an async event context for an incoming HTTP request/response pair. */
1007
+ function createHttpContext(data, options, ResponseClass = HttpResponse) {
1008
+ const ctx = new EventContext(options);
1009
+ const response = new ResponseClass(data.res, data.req, ctx.logger);
1010
+ return (fn) => run(ctx, () => ctx.seed(httpKind, {
1011
+ req: data.req,
1012
+ response,
1013
+ requestLimits: data.requestLimits
1014
+ }, fn));
1015
+ }
1016
+ /** Returns the current HTTP event context. */
1017
+ function useHttpContext(ctx) {
1018
+ return ctx ?? current();
1019
+ }
1020
+
1021
+ //#endregion
1022
+ //#region packages/event-http/src/errors/403.tl.svg
1023
+ function _403_tl_default(ctx) {
1024
+ return `<svg height="64" viewBox="0 4 100 96" fill="none" xmlns="http://www.w3.org/2000/svg" stroke="#888888" stroke-width="2">
1025
+ <path d="M50 90.625C64.4042 87.1875 83.5751 69.8667 83.5751 48.6937V24.0854L50 9.375L16.425 24.0833V48.6937C16.425 69.8667 35.5959 87.1875 50 90.625Z" fill="#ff000050">
1026
+ <animate attributeName="fill" dur="2s" repeatCount="indefinite"
1027
+ values="#ff000000;#ff000050;#ff000000" />
1028
+ </path>
1029
+
1030
+ <path d="M61.5395 46.0812H38.4604C37.1061 46.0812 36.0083 47.1791 36.0083 48.5333V65.075C36.0083 66.4292 37.1061 67.5271 38.4604 67.5271H61.5395C62.8938 67.5271 63.9916 66.4292 63.9916 65.075V48.5333C63.9916 47.1791 62.8938 46.0812 61.5395 46.0812Z" />
1031
+
1032
+ <path d="M41.7834 46.0834V39.6813C41.7834 37.5021 42.6491 35.4121 44.1901 33.8712C45.731 32.3303 47.8209 31.4646 50.0001 31.4646C52.1793 31.4646 54.2693 32.3303 55.8102 33.8712C57.3511 35.4121 58.2168 37.5021 58.2168 39.6813V46.0813" />
1033
+ </svg>
1034
+ `;
1035
+ }
1036
+
1037
+ //#endregion
1038
+ //#region packages/event-http/src/errors/404.tl.svg
1039
+ function _404_tl_default(ctx) {
1040
+ return `<svg height="64" viewBox="0 20 100 64" fill="none" xmlns="http://www.w3.org/2000/svg">
1041
+ <defs>
1042
+ <path id="sheet" d="M86.5 36.5V47.5H97.5V92.5H52.5V36.5H86.5ZM63 68.5H87V67.5H63V68.5ZM63 63.5H87V62.5H63V63.5ZM63 58.5H87V57.5H63V58.5ZM96.793 46.5H87.5V37.207L96.793 46.5Z" fill="#88888833" stroke="#888888aa"/>
1043
+ </defs>
1044
+
1045
+ <g id="queue" transform="translate(-5 -10)">
1046
+ <use href="#sheet" opacity="0">
1047
+ <animateTransform attributeName="transform" type="translate"
1048
+ dur="3s" repeatCount="indefinite"
1049
+ keyTimes="0;0.1;0.32;0.42;1"
1050
+ values="30 0; -20 0; -20 0; -70 0; -70 0" />
1051
+ <animate attributeName="opacity"
1052
+ dur="3s" repeatCount="indefinite"
1053
+ keyTimes="0;0.1;0.32;0.42;1"
1054
+ values="0;1;1;0;0" />
1055
+ </use>
1056
+
1057
+ <use href="#sheet" opacity="0">
1058
+ <animateTransform attributeName="transform" type="translate"
1059
+ dur="3s" begin="1s" repeatCount="indefinite"
1060
+ keyTimes="0;0.1;0.32;0.42;1"
364
1061
  values="30 0; -20 0; -20 0; -70 0; -70 0" />
365
1062
  <animate attributeName="opacity"
366
1063
  dur="3s" begin="1s" repeatCount="indefinite"
@@ -672,799 +1369,90 @@ function error_tl_default(ctx) {
672
1369
  }
673
1370
 
674
1371
  //#endregion
675
- //#region packages/event-http/src/errors/error-renderer.ts
1372
+ //#region packages/event-http/src/response/wooks-http-response.ts
676
1373
  let framework = {
677
- version: "0.6.5",
678
- poweredBy: `wooksjs`,
679
- link: `https://wooks.moost.org/`,
680
- image: `https://wooks.moost.org/wooks-full-logo.png`
1374
+ version: "0.7.0",
1375
+ poweredBy: "wooksjs",
1376
+ link: "https://wooks.moost.org/",
1377
+ image: "https://wooks.moost.org/wooks-full-logo.png"
681
1378
  };
682
- /** Renders HTTP error responses in HTML, JSON, or plain text based on the Accept header. */
683
- var HttpErrorRenderer = class extends BaseHttpResponseRenderer {
684
- constructor(opts) {
685
- super();
686
- this.opts = opts;
687
- }
688
- icons = {
689
- 401: typeof _403_tl_default === "function" ? _403_tl_default({}) : "",
690
- 403: typeof _403_tl_default === "function" ? _403_tl_default({}) : "",
691
- 404: typeof _404_tl_default === "function" ? _404_tl_default({}) : "",
692
- 500: typeof _500_tl_default === "function" ? _500_tl_default({}) : ""
693
- };
1379
+ const icons = {
1380
+ 401: typeof _403_tl_default === "function" ? _403_tl_default({}) : "",
1381
+ 403: typeof _403_tl_default === "function" ? _403_tl_default({}) : "",
1382
+ 404: typeof _404_tl_default === "function" ? _404_tl_default({}) : "",
1383
+ 500: typeof _500_tl_default === "function" ? _500_tl_default({}) : ""
1384
+ };
1385
+ /**
1386
+ * Default `HttpResponse` subclass used by `createHttpApp`.
1387
+ *
1388
+ * Overrides error rendering to produce content-negotiated responses (JSON, HTML, or plain text)
1389
+ * based on the request's `Accept` header. HTML error pages include SVG icons and framework branding.
1390
+ */
1391
+ var WooksHttpResponse = class extends HttpResponse {
1392
+ /** Registers framework metadata (name, version, link, logo) used in HTML error pages. */
694
1393
  static registerFramework(opts) {
695
1394
  framework = opts;
696
1395
  }
697
- renderHtml(response) {
698
- const data = response.body || {};
699
- response.setContentType("text/html");
700
- const hasDetails = Object.keys(data).length > 3;
701
- const icon = data.statusCode >= 500 ? this.icons[500] : this.icons[data.statusCode] || "";
702
- return typeof error_tl_default === "function" ? error_tl_default({
703
- icon,
704
- statusCode: data.statusCode,
705
- statusMessage: httpStatusCodes[data.statusCode],
706
- message: data.message,
707
- details: hasDetails ? JSON.stringify(data, null, " ") : "",
708
- version: (this.opts || framework).version,
709
- poweredBy: (this.opts || framework).poweredBy,
710
- link: (this.opts || framework).link,
711
- image: (this.opts || framework).image
712
- }) : JSON.stringify(data, null, " ");
713
- }
714
- renderText(response) {
715
- const data = response.body || {};
716
- response.setContentType("text/plain");
717
- const keys = Object.keys(data).filter((key) => ![
718
- "statusCode",
719
- "error",
720
- "message"
721
- ].includes(key));
722
- return `${data.statusCode} ${httpStatusCodes[data.statusCode]}\n${data.message}\n\n${keys.length > 0 ? `${JSON.stringify({
723
- ...data,
724
- statusCode: void 0,
725
- message: void 0,
726
- error: void 0
727
- }, null, " ")}` : ""}`;
728
- }
729
- renderJson(response) {
730
- response.setContentType("application/json");
731
- return JSON.stringify(response.body || {});
732
- }
733
- render(response) {
734
- const { acceptsJson, acceptsText, acceptsHtml } = useAccept();
735
- response.status = response.body?.statusCode || 500;
736
- if (acceptsJson()) return this.renderJson(response);
737
- else if (acceptsHtml()) return this.renderHtml(response);
738
- else if (acceptsText()) return this.renderText(response);
739
- else return this.renderJson(response);
1396
+ renderError(data, ctx) {
1397
+ this._status = data.statusCode || 500;
1398
+ const { has } = useAccept(ctx);
1399
+ if (has("json")) {
1400
+ this._headers["content-type"] = "application/json";
1401
+ this._body = JSON.stringify(data);
1402
+ } else if (has("html")) {
1403
+ this._headers["content-type"] = "text/html";
1404
+ this._body = renderErrorHtml(data);
1405
+ } else if (has("text")) {
1406
+ this._headers["content-type"] = "text/plain";
1407
+ this._body = renderErrorText(data);
1408
+ } else {
1409
+ this._headers["content-type"] = "application/json";
1410
+ this._body = JSON.stringify(data);
1411
+ }
740
1412
  }
741
1413
  };
742
-
743
- //#endregion
744
- //#region packages/event-http/src/errors/http-error.ts
745
- /** Represents an HTTP error with a status code and optional structured body. */
746
- var HttpError = class extends Error {
747
- name = "HttpError";
748
- constructor(code = 500, _body = "") {
749
- super(typeof _body === "string" ? _body : _body.message);
750
- this.code = code;
751
- this._body = _body;
752
- }
753
- get body() {
754
- return typeof this._body === "string" ? {
755
- statusCode: this.code,
756
- message: this.message,
757
- error: httpStatusCodes[this.code]
758
- } : {
759
- ...this._body,
760
- statusCode: this.code,
761
- message: this.message,
762
- error: httpStatusCodes[this.code]
763
- };
764
- }
765
- renderer;
766
- attachRenderer(renderer) {
767
- this.renderer = renderer;
768
- }
769
- getRenderer() {
770
- return this.renderer;
771
- }
772
- };
773
-
774
- //#endregion
775
- //#region packages/event-http/src/composables/request.ts
776
- const xForwardedFor = "x-forwarded-for";
777
- /** Default safety limits for request body reading (size, ratio, timeout). */
778
- const DEFAULT_LIMITS = {
779
- maxCompressed: 1 * 1024 * 1024,
780
- maxInflated: 10 * 1024 * 1024,
781
- maxRatio: 100,
782
- readTimeoutMs: 1e4
783
- };
784
- /**
785
- * Provides access to the incoming HTTP request (method, url, headers, body, IP).
786
- * @example
787
- * ```ts
788
- * const { method, url, rawBody, getIp } = useRequest()
789
- * const body = await rawBody()
790
- * ```
791
- */
792
- function useRequest() {
793
- const { store } = useHttpContext();
794
- const { init } = store("request");
795
- const event = store("event");
796
- const req = event.get("req");
797
- const contentEncoding = req.headers["content-encoding"];
798
- const contentEncodings = () => init("contentEncodings", () => (contentEncoding || "").split(",").map((p) => p.trim()).filter((p) => !!p));
799
- const isCompressed = () => init("isCompressed", () => {
800
- const parts = contentEncodings();
801
- for (const p of parts) if ([
802
- "deflate",
803
- "gzip",
804
- "br"
805
- ].includes(p)) return true;
806
- return false;
807
- });
808
- const limits = () => event.get("requestLimits");
809
- const setLimit = (key, value) => {
810
- let obj = limits();
811
- if (!obj?.perRequest) {
812
- obj = {
813
- ...obj,
814
- perRequest: true
815
- };
816
- event.set("requestLimits", obj);
817
- }
818
- obj[key] = value;
819
- };
820
- const getMaxCompressed = () => limits()?.maxCompressed ?? DEFAULT_LIMITS.maxCompressed;
821
- const setMaxCompressed = (limit) => setLimit("maxCompressed", limit);
822
- const getMaxInflated = () => limits()?.maxInflated ?? DEFAULT_LIMITS.maxInflated;
823
- const setMaxInflated = (limit) => setLimit("maxInflated", limit);
824
- const getMaxRatio = () => limits()?.maxRatio ?? DEFAULT_LIMITS.maxRatio;
825
- const setMaxRatio = (limit) => setLimit("maxRatio", limit);
826
- const getReadTimeoutMs = () => limits()?.readTimeoutMs ?? DEFAULT_LIMITS.readTimeoutMs;
827
- const setReadTimeoutMs = (limit) => setLimit("readTimeoutMs", limit);
828
- const rawBody = () => init("rawBody", async () => {
829
- const encs = contentEncodings();
830
- const isZip = isCompressed();
831
- const streamable = isZip && encodingSupportsStream(encs);
832
- const maxCompressed = getMaxCompressed();
833
- const maxInflated = getMaxInflated();
834
- const maxRatio = getMaxRatio();
835
- const timeoutMs = getReadTimeoutMs();
836
- const cl = Number(req.headers["content-length"] ?? 0);
837
- const upfrontLimit = isZip ? maxCompressed : maxInflated;
838
- if (cl && cl > upfrontLimit) throw new HttpError(413, "Payload Too Large");
839
- for (const enc of encs) if (!compressors[enc]) throw new HttpError(415, `Unsupported Content-Encoding "${enc}"`);
840
- let timer = null;
841
- function resetTimer() {
842
- if (timeoutMs === 0) return;
843
- clearTimer();
844
- timer = setTimeout(() => {
845
- clearTimer();
846
- req.destroy();
847
- }, timeoutMs);
848
- }
849
- function clearTimer() {
850
- if (timer) {
851
- clearTimeout(timer);
852
- timer = null;
853
- }
854
- }
855
- let rawBytes = 0;
856
- async function* limitedCompressed() {
857
- resetTimer();
858
- try {
859
- for await (const chunk of req) {
860
- rawBytes += chunk.length;
861
- if (rawBytes > upfrontLimit) {
862
- req.destroy();
863
- throw new HttpError(413, "Payload Too Large");
864
- }
865
- resetTimer();
866
- yield chunk;
867
- }
868
- } finally {
869
- clearTimer();
870
- }
871
- }
872
- let stream = limitedCompressed();
873
- if (streamable) stream = await uncompressBodyStream(encs, stream);
874
- const chunks = [];
875
- let inflatedBytes = 0;
876
- try {
877
- for await (const chunk of stream) {
878
- inflatedBytes += chunk.length;
879
- if (inflatedBytes > maxInflated) throw new HttpError(413, "Inflated body too large");
880
- chunks.push(chunk);
881
- }
882
- } catch (error) {
883
- if (error instanceof HttpError) throw error;
884
- throw new HttpError(408, "Request body timeout");
885
- }
886
- let body = Buffer$1.concat(chunks);
887
- if (!streamable && isZip) {
888
- body = await uncompressBody(encs, body);
889
- inflatedBytes = body.byteLength;
890
- if (inflatedBytes > maxInflated) throw new HttpError(413, "Inflated body too large");
891
- }
892
- if (isZip && rawBytes > 0 && inflatedBytes / rawBytes > maxRatio) throw new HttpError(413, "Compression ratio too high");
893
- return body;
894
- });
895
- const reqId = useEventId().getId;
896
- const forwardedIp = () => init("forwardedIp", () => {
897
- if (typeof req.headers[xForwardedFor] === "string" && req.headers[xForwardedFor]) return req.headers[xForwardedFor].split(",").shift()?.trim();
898
- else return "";
899
- });
900
- const remoteIp = () => init("remoteIp", () => req.socket.remoteAddress || req.connection.remoteAddress || "");
901
- function getIp(options) {
902
- if (options?.trustProxy) return forwardedIp() || getIp();
903
- else return remoteIp();
904
- }
905
- const getIpList = () => init("ipList", () => ({
906
- remoteIp: req.socket.remoteAddress || req.connection.remoteAddress || "",
907
- forwarded: (req.headers[xForwardedFor] || "").split(",").map((s) => s.trim())
908
- }));
909
- return {
910
- rawRequest: req,
911
- url: req.url,
912
- method: req.method,
913
- headers: req.headers,
914
- rawBody,
915
- reqId,
916
- getIp,
917
- getIpList,
918
- isCompressed,
919
- getMaxCompressed,
920
- setMaxCompressed,
921
- getReadTimeoutMs,
922
- setReadTimeoutMs,
923
- getMaxInflated,
924
- setMaxInflated,
925
- getMaxRatio,
926
- setMaxRatio
927
- };
928
- }
929
-
930
- //#endregion
931
- //#region packages/event-http/src/composables/headers.ts
932
- /**
933
- * Returns the incoming request headers.
934
- * @example
935
- * ```ts
936
- * const { host, authorization } = useHeaders()
937
- * ```
938
- */
939
- function useHeaders() {
940
- return useRequest().headers;
941
- }
942
- /**
943
- * Provides methods to set, get, and remove outgoing response headers.
944
- * @example
945
- * ```ts
946
- * const { setHeader, setContentType, enableCors } = useSetHeaders()
947
- * setHeader('x-request-id', '123')
948
- * ```
949
- */
950
- function useSetHeaders() {
951
- const { store } = useHttpContext();
952
- const setHeaderStore = store("setHeader");
953
- function setHeader(name, value) {
954
- setHeaderStore.set(name, value.toString());
955
- }
956
- function setContentType(value) {
957
- setHeader("content-type", value);
958
- }
959
- function enableCors(origin = "*") {
960
- setHeader("access-control-allow-origin", origin);
961
- }
962
- return {
963
- setHeader,
964
- getHeader: setHeaderStore.get,
965
- removeHeader: setHeaderStore.del,
966
- setContentType,
967
- headers: () => setHeaderStore.value || {},
968
- enableCors
969
- };
970
- }
971
- /** Returns a hookable accessor for a single outgoing response header by name. */
972
- function useSetHeader(name) {
973
- const { store } = useHttpContext();
974
- const { hook } = store("setHeader");
975
- return hook(name);
976
- }
977
-
978
- //#endregion
979
- //#region packages/event-http/src/composables/cookies.ts
980
- const cookieRegExpCache = /* @__PURE__ */ new Map();
981
- function getCookieRegExp(name) {
982
- let re = cookieRegExpCache.get(name);
983
- if (!re) {
984
- re = new RegExp(`(?:^|; )${escapeRegex(name)}=(.*?)(?:;?$|; )`, "i");
985
- cookieRegExpCache.set(name, re);
986
- }
987
- return re;
988
- }
989
- /**
990
- * Provides access to parsed request cookies.
991
- * @example
992
- * ```ts
993
- * const { getCookie, rawCookies } = useCookies()
994
- * const sessionId = getCookie('session_id')
995
- * ```
996
- */
997
- function useCookies() {
998
- const { store } = useHttpContext();
999
- const { cookie } = useHeaders();
1000
- const { init } = store("cookies");
1001
- const getCookie = (name) => init(name, () => {
1002
- if (cookie) {
1003
- const result = getCookieRegExp(name).exec(cookie);
1004
- return result?.[1] ? safeDecodeURIComponent(result[1]) : null;
1005
- } else return null;
1006
- });
1007
- return {
1008
- rawCookies: cookie,
1009
- getCookie
1010
- };
1011
- }
1012
- /** Provides methods to set, get, remove, and clear outgoing response cookies. */
1013
- function useSetCookies() {
1014
- const { store } = useHttpContext();
1015
- const cookiesStore = store("setCookies");
1016
- function setCookie(name, value, attrs) {
1017
- cookiesStore.set(name, {
1018
- value,
1019
- attrs: attrs || {}
1020
- });
1021
- }
1022
- function cookies() {
1023
- const entries = cookiesStore.entries();
1024
- if (entries.length === 0) return [];
1025
- return entries.filter((a) => !!a[1]).map(([key, value]) => renderCookie(key, value));
1026
- }
1027
- return {
1028
- setCookie,
1029
- getCookie: cookiesStore.get,
1030
- removeCookie: cookiesStore.del,
1031
- clearCookies: cookiesStore.clear,
1032
- cookies
1033
- };
1034
- }
1035
- /** Returns a hookable accessor for a single outgoing cookie by name. */
1036
- function useSetCookie(name) {
1037
- const { setCookie, getCookie } = useSetCookies();
1038
- const valueHook = attachHook({
1039
- name,
1040
- type: "cookie"
1041
- }, {
1042
- get: () => getCookie(name)?.value,
1043
- set: (value) => {
1044
- setCookie(name, value, getCookie(name)?.attrs);
1045
- }
1046
- });
1047
- return attachHook(valueHook, {
1048
- get: () => getCookie(name)?.attrs,
1049
- set: (attrs) => {
1050
- setCookie(name, getCookie(name)?.value || "", attrs);
1051
- }
1052
- }, "attrs");
1053
- }
1054
-
1055
- //#endregion
1056
- //#region packages/event-http/src/composables/header-accept.ts
1057
- /** Provides helpers to check the request's Accept header for supported MIME types. */
1058
- function useAccept() {
1059
- const { store } = useHttpContext();
1060
- const { accept } = useHeaders();
1061
- const accepts = (mime) => {
1062
- const { set, get, has } = store("accept");
1063
- if (!has(mime)) return set(mime, !!(accept && (accept === "*/*" || accept.includes(mime))));
1064
- return get(mime);
1065
- };
1066
- return {
1067
- accept,
1068
- accepts,
1069
- acceptsJson: () => accepts("application/json"),
1070
- acceptsXml: () => accepts("application/xml"),
1071
- acceptsText: () => accepts("text/plain"),
1072
- acceptsHtml: () => accepts("text/html")
1073
- };
1074
- }
1075
-
1076
- //#endregion
1077
- //#region packages/event-http/src/composables/header-authorization.ts
1078
- /**
1079
- * Provides parsed access to the Authorization header (type, credentials, Basic decoding).
1080
- * @example
1081
- * ```ts
1082
- * const { isBearer, authRawCredentials, basicCredentials } = useAuthorization()
1083
- * if (isBearer()) { const token = authRawCredentials() }
1084
- * ```
1085
- */
1086
- function useAuthorization() {
1087
- const { store } = useHttpContext();
1088
- const { authorization } = useHeaders();
1089
- const { init } = store("authorization");
1090
- const authType = () => init("type", () => {
1091
- if (authorization) {
1092
- const space = authorization.indexOf(" ");
1093
- return authorization.slice(0, space);
1094
- }
1095
- return null;
1096
- });
1097
- const authRawCredentials = () => init("credentials", () => {
1098
- if (authorization) {
1099
- const space = authorization.indexOf(" ");
1100
- return authorization.slice(space + 1);
1101
- }
1102
- return null;
1103
- });
1104
- return {
1105
- authorization,
1106
- authType,
1107
- authRawCredentials,
1108
- isBasic: () => authType()?.toLocaleLowerCase() === "basic",
1109
- isBearer: () => authType()?.toLocaleLowerCase() === "bearer",
1110
- basicCredentials: () => init("basicCredentials", () => {
1111
- if (authorization) {
1112
- const type = authType();
1113
- if (type?.toLocaleLowerCase() === "basic") {
1114
- const creds = Buffer$1.from(authRawCredentials() || "", "base64").toString("ascii");
1115
- const [username, password] = creds.split(":");
1116
- return {
1117
- username,
1118
- password
1119
- };
1120
- }
1121
- }
1122
- return null;
1123
- })
1124
- };
1125
- }
1126
-
1127
- //#endregion
1128
- //#region packages/event-http/src/utils/cache-control.ts
1129
- function renderCacheControl(data) {
1130
- let attrs = "";
1131
- for (const [a, v] of Object.entries(data)) {
1132
- if (v === void 0) continue;
1133
- const func = cacheControlFunc[a];
1134
- if (typeof func === "function") {
1135
- const val = func(v);
1136
- if (val) attrs += attrs ? `, ${val}` : val;
1137
- } else throw new TypeError(`Unknown Cache-Control attribute ${a}`);
1138
- }
1139
- return attrs;
1140
- }
1141
- const cacheControlFunc = {
1142
- mustRevalidate: (v) => v ? "must-revalidate" : "",
1143
- noCache: (v) => v ? typeof v === "string" ? `no-cache="${v}"` : "no-cache" : "",
1144
- noStore: (v) => v ? "no-store" : "",
1145
- noTransform: (v) => v ? "no-transform" : "",
1146
- public: (v) => v ? "public" : "",
1147
- private: (v) => v ? typeof v === "string" ? `private="${v}"` : "private" : "",
1148
- proxyRevalidate: (v) => v ? "proxy-revalidate" : "",
1149
- maxAge: (v) => `max-age=${convertTime(v, "s").toString()}`,
1150
- sMaxage: (v) => `s-maxage=${convertTime(v, "s").toString()}`
1151
- };
1152
-
1153
- //#endregion
1154
- //#region packages/event-http/src/composables/header-set-cache-control.ts
1155
- const renderAge = (v) => convertTime(v, "s").toString();
1156
- const renderExpires = (v) => typeof v === "string" || typeof v === "number" ? new Date(v).toUTCString() : v.toUTCString();
1157
- const renderPragmaNoCache = (v) => v ? "no-cache" : "";
1158
- /** Provides helpers to set cache-related response headers (Cache-Control, Expires, Age, Pragma). */
1159
- function useSetCacheControl() {
1160
- const { setHeader } = useSetHeaders();
1161
- const setAge = (value) => {
1162
- setHeader("age", renderAge(value));
1163
- };
1164
- const setExpires = (value) => {
1165
- setHeader("expires", renderExpires(value));
1166
- };
1167
- const setPragmaNoCache = (value = true) => {
1168
- setHeader("pragma", renderPragmaNoCache(value));
1169
- };
1170
- const setCacheControl = (data) => {
1171
- setHeader("cache-control", renderCacheControl(data));
1172
- };
1173
- return {
1174
- setExpires,
1175
- setAge,
1176
- setPragmaNoCache,
1177
- setCacheControl
1178
- };
1179
- }
1180
-
1181
- //#endregion
1182
- //#region packages/event-http/src/composables/response.ts
1183
- /**
1184
- * Provides access to the raw HTTP response and status code management.
1185
- * @example
1186
- * ```ts
1187
- * const { status, rawResponse, hasResponded } = useResponse()
1188
- * status(200)
1189
- * ```
1190
- */
1191
- function useResponse() {
1192
- const { store } = useHttpContext();
1193
- const event = store("event");
1194
- const res = event.get("res");
1195
- const responded = store("response").hook("responded");
1196
- const statusCode = store("status").hook("code");
1197
- function status(code) {
1198
- return statusCode.value = code ? code : statusCode.value;
1199
- }
1200
- const rawResponse = (options) => {
1201
- if (!options || !options.passthrough) responded.value = true;
1202
- return res;
1203
- };
1204
- return {
1205
- rawResponse,
1206
- hasResponded: () => responded.value || !res.writable || res.writableEnded,
1207
- status: attachHook(status, {
1208
- get: () => statusCode.value,
1209
- set: (code) => statusCode.value = code
1210
- })
1211
- };
1212
- }
1213
- /** Returns a hookable accessor for the response status code. */
1214
- function useStatus() {
1215
- const { store } = useHttpContext();
1216
- return store("status").hook("code");
1217
- }
1218
-
1219
- //#endregion
1220
- //#region packages/event-http/src/utils/url-search-params.ts
1221
- const ILLEGAL_KEYS = new Set([
1222
- "__proto__",
1223
- "constructor",
1224
- "prototype"
1225
- ]);
1226
- var WooksURLSearchParams = class extends URLSearchParams {
1227
- toJson() {
1228
- const json = Object.create(null);
1229
- for (const [key, value] of this.entries()) if (isArrayParam(key)) {
1230
- const a = json[key] = json[key] || [];
1231
- a.push(value);
1232
- } else {
1233
- if (ILLEGAL_KEYS.has(key)) throw new HttpError(400, `Illegal key name "${key}"`);
1234
- if (key in json) throw new HttpError(400, `Duplicate key "${key}"`);
1235
- json[key] = value;
1236
- }
1237
- return json;
1238
- }
1239
- };
1240
- function isArrayParam(name) {
1241
- return name.endsWith("[]");
1242
- }
1243
-
1244
- //#endregion
1245
- //#region packages/event-http/src/composables/search-params.ts
1246
- /**
1247
- * Provides access to URL search (query) parameters from the request.
1248
- * @example
1249
- * ```ts
1250
- * const { urlSearchParams, jsonSearchParams } = useSearchParams()
1251
- * const page = urlSearchParams().get('page')
1252
- * ```
1253
- */
1254
- function useSearchParams() {
1255
- const { store } = useHttpContext();
1256
- const url = useRequest().url || "";
1257
- const { init } = store("searchParams");
1258
- const rawSearchParams = () => init("raw", () => {
1259
- const i = url.indexOf("?");
1260
- return i >= 0 ? url.slice(i) : "";
1261
- });
1262
- const urlSearchParams = () => init("urlSearchParams", () => new WooksURLSearchParams(rawSearchParams()));
1263
- return {
1264
- rawSearchParams,
1265
- urlSearchParams,
1266
- jsonSearchParams: () => urlSearchParams().toJson()
1267
- };
1268
- }
1269
-
1270
- //#endregion
1271
- //#region packages/event-http/src/response/core.ts
1272
- const defaultStatus = {
1273
- GET: EHttpStatusCode.OK,
1274
- POST: EHttpStatusCode.Created,
1275
- PUT: EHttpStatusCode.Created,
1276
- PATCH: EHttpStatusCode.Accepted,
1277
- DELETE: EHttpStatusCode.Accepted
1278
- };
1279
- const baseRenderer = new BaseHttpResponseRenderer();
1280
- var BaseHttpResponse = class {
1281
- constructor(renderer = baseRenderer) {
1282
- this.renderer = renderer;
1283
- }
1284
- _status = 0;
1285
- _body;
1286
- _headers = {};
1287
- get status() {
1288
- return this._status;
1289
- }
1290
- set status(value) {
1291
- this._status = value;
1292
- }
1293
- get body() {
1294
- return this._body;
1295
- }
1296
- set body(value) {
1297
- this._body = value;
1298
- }
1299
- setStatus(value) {
1300
- this.status = value;
1301
- return this;
1302
- }
1303
- setBody(value) {
1304
- this.body = value;
1305
- return this;
1306
- }
1307
- getContentType() {
1308
- return this._headers["content-type"];
1309
- }
1310
- setContentType(value) {
1311
- this._headers["content-type"] = value;
1312
- return this;
1313
- }
1314
- enableCors(origin = "*") {
1315
- this._headers["Access-Control-Allow-Origin"] = origin;
1316
- return this;
1317
- }
1318
- setCookie(name, value, attrs) {
1319
- const cookies = this._headers["set-cookie"] = this._headers["set-cookie"] || [];
1320
- cookies.push(renderCookie(name, {
1321
- value,
1322
- attrs: attrs || {}
1323
- }));
1324
- return this;
1325
- }
1326
- setCacheControl(data) {
1327
- this.setHeader("cache-control", renderCacheControl(data));
1328
- }
1329
- setCookieRaw(rawValue) {
1330
- const cookies = this._headers["set-cookie"] = this._headers["set-cookie"] || [];
1331
- cookies.push(rawValue);
1332
- return this;
1333
- }
1334
- header(name, value) {
1335
- this._headers[name] = value;
1336
- return this;
1337
- }
1338
- setHeader(name, value) {
1339
- return this.header(name, value);
1340
- }
1341
- getHeader(name) {
1342
- return this._headers[name];
1343
- }
1344
- mergeHeaders() {
1345
- const { headers } = useSetHeaders();
1346
- const { cookies, removeCookie } = useSetCookies();
1347
- const newCookies = this._headers["set-cookie"] || [];
1348
- for (const cookie of newCookies) removeCookie(cookie.slice(0, cookie.indexOf("=")));
1349
- const composableHeaders = headers();
1350
- for (const key in composableHeaders) if (!(key in this._headers)) this._headers[key] = composableHeaders[key];
1351
- const renderedCookies = cookies();
1352
- if (newCookies.length > 0 || renderedCookies.length > 0) this._headers["set-cookie"] = newCookies.length > 0 && renderedCookies.length > 0 ? [...newCookies, ...renderedCookies] : newCookies.length > 0 ? newCookies : renderedCookies;
1353
- return this;
1354
- }
1355
- mergeStatus(renderedBody) {
1356
- this.status = this.status || useResponse().status();
1357
- if (!this.status) {
1358
- const { method } = useRequest();
1359
- this.status = renderedBody ? defaultStatus[method] || EHttpStatusCode.OK : EHttpStatusCode.NoContent;
1360
- }
1361
- return this;
1362
- }
1363
- mergeFetchStatus(fetchStatus) {
1364
- this.status = this.status || useResponse().status() || fetchStatus;
1365
- }
1366
- panic(text, logger) {
1367
- const error = new Error(text);
1368
- logger.error(error);
1369
- throw error;
1370
- }
1371
- async respond() {
1372
- const { rawResponse, hasResponded } = useResponse();
1373
- const { method, rawRequest } = useRequest();
1374
- const logger = useEventLogger$1("http-response") || console;
1375
- if (hasResponded()) this.panic("The response was already sent.", logger);
1376
- this.mergeHeaders();
1377
- const res = rawResponse();
1378
- if (this.body instanceof Readable$1) {
1379
- const stream = this.body;
1380
- this.mergeStatus(true);
1381
- res.writeHead(this.status, { ...this._headers });
1382
- rawRequest.once("close", () => {
1383
- stream.destroy();
1384
- });
1385
- if (method === "HEAD") {
1386
- stream.destroy();
1387
- res.end();
1388
- } else return new Promise((resolve, reject) => {
1389
- stream.on("error", (e) => {
1390
- stream.destroy();
1391
- res.end();
1392
- reject(e);
1393
- });
1394
- stream.on("close", () => {
1395
- stream.destroy();
1396
- resolve(void 0);
1397
- });
1398
- stream.pipe(res);
1399
- });
1400
- } else if (globalThis.Response && this.body instanceof Response) {
1401
- this.mergeFetchStatus(this.body.status);
1402
- const additionalHeaders = {};
1403
- const fetchContentLength = this.body.headers.get("content-length");
1404
- if (fetchContentLength) additionalHeaders["content-length"] = fetchContentLength;
1405
- const fetchContentType = this.body.headers.get("content-type");
1406
- if (fetchContentType) additionalHeaders["content-type"] = fetchContentType;
1407
- res.writeHead(this.status, {
1408
- ...this._headers,
1409
- ...additionalHeaders
1410
- });
1411
- if (method === "HEAD") res.end();
1412
- else await respondWithFetch(this.body.body, res, logger);
1413
- } else {
1414
- const renderedBody = this.renderer.render(this);
1415
- this.mergeStatus(renderedBody);
1416
- const contentLength = typeof renderedBody === "string" ? Buffer.byteLength(renderedBody) : renderedBody.byteLength;
1417
- return new Promise((resolve) => {
1418
- res.writeHead(this.status, {
1419
- "content-length": contentLength,
1420
- ...this._headers
1421
- }).end(method === "HEAD" ? "" : renderedBody, resolve);
1422
- });
1423
- }
1424
- }
1425
- };
1426
- async function respondWithFetch(fetchBody, res, logger) {
1427
- if (fetchBody) try {
1428
- for await (const chunk of fetchBody) res.write(chunk);
1429
- } catch (error) {
1430
- logger.error("Error streaming fetch response body", error);
1431
- }
1432
- if (!res.writableEnded) res.end();
1433
- }
1434
-
1435
- //#endregion
1436
- //#region packages/event-http/src/response/factory.ts
1437
- function createWooksResponder(renderer = new BaseHttpResponseRenderer(), errorRenderer = new HttpErrorRenderer()) {
1438
- function createResponse(data) {
1439
- const { hasResponded } = useResponse();
1440
- if (hasResponded()) return null;
1441
- if (data instanceof Error) {
1442
- const r = new BaseHttpResponse(errorRenderer);
1443
- let httpError;
1444
- if (data instanceof HttpError) httpError = data;
1445
- else httpError = new HttpError(500, data.message);
1446
- r.setBody(httpError.body);
1447
- return r;
1448
- } else if (data instanceof BaseHttpResponse) return data;
1449
- else return new BaseHttpResponse(renderer).setBody(data);
1450
- }
1451
- return {
1452
- createResponse,
1453
- respond: (data) => createResponse(data)?.respond()
1454
- };
1455
- }
1414
+ function renderErrorHtml(data) {
1415
+ const hasDetails = Object.keys(data).length > 3;
1416
+ const icon = data.statusCode >= 500 ? icons[500] : icons[data.statusCode] || "";
1417
+ return typeof error_tl_default === "function" ? error_tl_default({
1418
+ icon,
1419
+ statusCode: data.statusCode,
1420
+ statusMessage: httpStatusCodes[data.statusCode],
1421
+ message: data.message,
1422
+ details: hasDetails ? JSON.stringify(data, null, " ") : "",
1423
+ version: framework.version,
1424
+ poweredBy: framework.poweredBy,
1425
+ link: framework.link,
1426
+ image: framework.image
1427
+ }) : JSON.stringify(data, null, " ");
1428
+ }
1429
+ function renderErrorText(data) {
1430
+ const keys = Object.keys(data).filter((key) => ![
1431
+ "statusCode",
1432
+ "error",
1433
+ "message"
1434
+ ].includes(key));
1435
+ return `${data.statusCode} ${httpStatusCodes[data.statusCode]}\n${data.message}\n\n${keys.length > 0 ? JSON.stringify({
1436
+ ...data,
1437
+ statusCode: void 0,
1438
+ message: void 0,
1439
+ error: void 0
1440
+ }, null, " ") : ""}`;
1441
+ }
1456
1442
 
1457
1443
  //#endregion
1458
1444
  //#region packages/event-http/src/http-adapter.ts
1459
1445
  /** HTTP adapter for Wooks that provides route registration, server lifecycle, and request handling. */
1460
1446
  var WooksHttp = class extends WooksAdapterBase {
1461
1447
  logger;
1462
- _cachedEventOptions;
1448
+ ResponseClass;
1449
+ eventContextOptions;
1463
1450
  constructor(opts, wooks) {
1464
1451
  super(wooks, opts?.logger, opts?.router);
1465
1452
  this.opts = opts;
1466
1453
  this.logger = opts?.logger || this.getLogger(`[wooks-http]`);
1467
- this._cachedEventOptions = this.mergeEventOptions(opts?.eventOptions);
1454
+ this.ResponseClass = opts?.responseClass ?? WooksHttpResponse;
1455
+ this.eventContextOptions = this.getEventContextOptions();
1468
1456
  }
1469
1457
  /** Registers a handler for all HTTP methods on the given path. */
1470
1458
  all(path, handler) {
@@ -1498,9 +1486,22 @@ var WooksHttp = class extends WooksAdapterBase {
1498
1486
  options(path, handler) {
1499
1487
  return this.on("OPTIONS", path, handler);
1500
1488
  }
1489
+ /** Registers an UPGRADE route handler for WebSocket upgrade requests. */
1490
+ upgrade(path, handler) {
1491
+ return this.on("UPGRADE", path, handler);
1492
+ }
1493
+ wsHandler;
1494
+ /** Register a WebSocket upgrade handler that implements the WooksUpgradeHandler contract. */
1495
+ ws(handler) {
1496
+ this.wsHandler = handler;
1497
+ }
1501
1498
  server;
1502
1499
  async listen(port, hostname, backlog, listeningListener) {
1503
1500
  const server = this.server = http.createServer(this.getServerCb());
1501
+ if (this.wsHandler) {
1502
+ const upgradeCb = this.getUpgradeCb();
1503
+ server.on("upgrade", upgradeCb);
1504
+ }
1504
1505
  return new Promise((resolve, reject) => {
1505
1506
  server.once("listening", resolve);
1506
1507
  server.once("error", reject);
@@ -1550,11 +1551,14 @@ var WooksHttp = class extends WooksAdapterBase {
1550
1551
  attachServer(server) {
1551
1552
  this.server = server;
1552
1553
  }
1553
- responder = createWooksResponder();
1554
- respond(data) {
1555
- return this.responder.respond(data)?.catch((error) => {
1556
- this.logger.error("Uncaught response exception", error);
1557
- });
1554
+ respond(data, response, ctx) {
1555
+ if (response.responded) return;
1556
+ if (data instanceof Error) {
1557
+ const httpError = data instanceof HttpError ? data : new HttpError(500, data.message);
1558
+ return response.sendError(httpError, ctx);
1559
+ }
1560
+ if (data !== response) response.body = data;
1561
+ return response.send();
1558
1562
  }
1559
1563
  /**
1560
1564
  * Returns server callback function
@@ -1569,47 +1573,131 @@ var WooksHttp = class extends WooksAdapterBase {
1569
1573
  * ```
1570
1574
  */
1571
1575
  getServerCb() {
1576
+ const ctxOptions = this.eventContextOptions;
1577
+ const RequestLimits = this.opts?.requestLimits;
1578
+ const notFoundHandler = this.opts?.onNotFound;
1579
+ const defaultHeaders = this.opts?.defaultHeaders;
1572
1580
  return (req, res) => {
1573
- const runInContext = createHttpContext({
1574
- req,
1575
- res,
1576
- requestLimits: this.opts?.requestLimits
1577
- }, this._cachedEventOptions);
1581
+ const ctx = new EventContext(ctxOptions);
1582
+ const response = new this.ResponseClass(res, req, ctx.logger, defaultHeaders);
1578
1583
  const method = req.method || "";
1579
1584
  const url = req.url || "";
1580
- runInContext(async () => {
1581
- const notFoundHandler = this.opts?.onNotFound;
1582
- const { handlers } = this.wooks.lookup(method, url);
1583
- if (handlers || notFoundHandler) try {
1584
- return await this.processHandlers(handlers || [notFoundHandler]);
1585
- } catch (error) {
1586
- this.logger.error("Internal error, please report", error);
1587
- await this.respond(error);
1588
- return error;
1589
- }
1590
- else {
1585
+ run(ctx, () => {
1586
+ ctx.seed(httpKind, {
1587
+ req,
1588
+ response,
1589
+ requestLimits: RequestLimits
1590
+ });
1591
+ const handlers = this.wooks.lookupHandlers(method, url, ctx);
1592
+ if (handlers || notFoundHandler) {
1593
+ const result = this.processHandlers(handlers || [notFoundHandler], ctx, response);
1594
+ if (result !== null && result !== void 0 && typeof result.then === "function") result.catch((error) => {
1595
+ this.logger.error("Internal error, please report", error);
1596
+ this.respond(error, response, ctx);
1597
+ });
1598
+ } else {
1591
1599
  this.logger.debug(`404 Not found (${method})${url}`);
1592
1600
  const error = new HttpError(404);
1593
- await this.respond(error);
1594
- return error;
1601
+ this.respond(error, response, ctx);
1595
1602
  }
1596
1603
  });
1597
1604
  };
1598
1605
  }
1599
- async processHandlers(handlers) {
1600
- const { store } = useHttpContext();
1606
+ /**
1607
+ * Returns upgrade callback function for the HTTP server's 'upgrade' event.
1608
+ * Creates an HTTP context, seeds it with upgrade data, and routes as method 'UPGRADE'.
1609
+ */
1610
+ getUpgradeCb() {
1611
+ const ctxOptions = this.eventContextOptions;
1612
+ const requestLimits = this.opts?.requestLimits;
1613
+ const wsHandler = this.wsHandler;
1614
+ return (req, socket, head) => {
1615
+ if (!wsHandler) {
1616
+ socket.destroy();
1617
+ return;
1618
+ }
1619
+ const ctx = new EventContext(ctxOptions);
1620
+ const url = req.url || "";
1621
+ run(ctx, () => {
1622
+ ctx.seed(httpKind, {
1623
+ req,
1624
+ response: void 0,
1625
+ requestLimits
1626
+ });
1627
+ ctx.set(wsHandler.reqKey, req);
1628
+ ctx.set(wsHandler.socketKey, socket);
1629
+ ctx.set(wsHandler.headKey, head);
1630
+ const handlers = this.wooks.lookupHandlers("UPGRADE", url, ctx);
1631
+ if (handlers) this.processUpgradeHandlers(handlers, ctx, socket);
1632
+ else wsHandler.handleUpgrade(req, socket, head);
1633
+ });
1634
+ };
1635
+ }
1636
+ processUpgradeHandlers(handlers, ctx, socket) {
1637
+ for (let i = 0; i < handlers.length; i++) {
1638
+ const handler = handlers[i];
1639
+ const isLastHandler = i === handlers.length - 1;
1640
+ try {
1641
+ const result = handler();
1642
+ if (result !== null && result !== void 0 && typeof result.then === "function") {
1643
+ result.catch((error) => {
1644
+ this.logger.error(`Upgrade handler error: ${ctx.get(httpKind.keys.req)?.url || ""}`, error);
1645
+ socket.destroy();
1646
+ });
1647
+ return;
1648
+ }
1649
+ return;
1650
+ } catch (error) {
1651
+ if (!(error instanceof HttpError)) this.logger.error(`Upgrade handler error: ${ctx.get(httpKind.keys.req)?.url || ""}`, error);
1652
+ if (isLastHandler) {
1653
+ socket.destroy();
1654
+ return;
1655
+ }
1656
+ }
1657
+ }
1658
+ }
1659
+ processHandlers(handlers, ctx, response) {
1601
1660
  for (let i = 0; i < handlers.length; i++) {
1602
1661
  const handler = handlers[i];
1603
1662
  const isLastHandler = i === handlers.length - 1;
1604
1663
  try {
1605
- const promise = handler();
1606
- const result = await promise;
1607
- await this.respond(result);
1664
+ const result = handler();
1665
+ if (result !== null && result !== void 0 && typeof result.then === "function") return this.processAsyncResult(result, handlers, i, ctx, response);
1666
+ this.respond(result, response, ctx);
1667
+ return;
1668
+ } catch (error) {
1669
+ if (!(error instanceof HttpError)) this.logger.error(`Uncaught route handler exception: ${ctx.get(httpKind.keys.req)?.url || ""}`, error);
1670
+ if (isLastHandler) {
1671
+ this.respond(error, response, ctx);
1672
+ return;
1673
+ }
1674
+ }
1675
+ }
1676
+ }
1677
+ async processAsyncResult(promise, handlers, startIndex, ctx, response) {
1678
+ try {
1679
+ const result = await promise;
1680
+ await this.respond(result, response, ctx);
1681
+ return result;
1682
+ } catch (error) {
1683
+ const isLastHandler = startIndex === handlers.length - 1;
1684
+ if (!(error instanceof HttpError)) this.logger.error(`Uncaught route handler exception: ${ctx.get(httpKind.keys.req)?.url || ""}`, error);
1685
+ if (isLastHandler) {
1686
+ await this.respond(error, response, ctx);
1687
+ return error;
1688
+ }
1689
+ }
1690
+ for (let i = startIndex + 1; i < handlers.length; i++) {
1691
+ const handler = handlers[i];
1692
+ const isLastHandler = i === handlers.length - 1;
1693
+ try {
1694
+ const result = await handler();
1695
+ await this.respond(result, response, ctx);
1608
1696
  return result;
1609
1697
  } catch (error) {
1610
- if (error instanceof HttpError) {} else this.logger.error(`Uncaught route handler exception: ${store("event").get("req")?.url || ""}`, error);
1698
+ if (!(error instanceof HttpError)) this.logger.error(`Uncaught route handler exception: ${ctx.get(httpKind.keys.req)?.url || ""}`, error);
1611
1699
  if (isLastHandler) {
1612
- await this.respond(error);
1700
+ await this.respond(error, response, ctx);
1613
1701
  return error;
1614
1702
  }
1615
1703
  }
@@ -1630,4 +1718,100 @@ function createHttpApp(opts, wooks) {
1630
1718
  }
1631
1719
 
1632
1720
  //#endregion
1633
- export { BaseHttpResponse, BaseHttpResponseRenderer, DEFAULT_LIMITS, EHttpStatusCode, HttpError, HttpErrorRenderer, WooksHttp, WooksURLSearchParams, createHttpApp, createHttpContext, createWooksResponder, httpStatusCodes, renderCacheControl, useAccept, useAuthorization, useCookies, useEventLogger, useHeaders, useHttpContext, useRequest, useResponse, useRouteParams, useSearchParams, useSetCacheControl, useSetCookie, useSetCookies, useSetHeader, useSetHeaders, useStatus };
1721
+ //#region packages/event-http/src/utils/security-headers.ts
1722
+ const HEADER_MAP = [
1723
+ [
1724
+ "contentSecurityPolicy",
1725
+ "content-security-policy",
1726
+ "default-src 'self'; base-uri 'self'; form-action 'self'; frame-ancestors 'self'"
1727
+ ],
1728
+ [
1729
+ "crossOriginOpenerPolicy",
1730
+ "cross-origin-opener-policy",
1731
+ "same-origin"
1732
+ ],
1733
+ [
1734
+ "crossOriginResourcePolicy",
1735
+ "cross-origin-resource-policy",
1736
+ "same-origin"
1737
+ ],
1738
+ [
1739
+ "referrerPolicy",
1740
+ "referrer-policy",
1741
+ "no-referrer"
1742
+ ],
1743
+ [
1744
+ "strictTransportSecurity",
1745
+ "strict-transport-security",
1746
+ void 0
1747
+ ],
1748
+ [
1749
+ "xContentTypeOptions",
1750
+ "x-content-type-options",
1751
+ "nosniff"
1752
+ ],
1753
+ [
1754
+ "xFrameOptions",
1755
+ "x-frame-options",
1756
+ "SAMEORIGIN"
1757
+ ]
1758
+ ];
1759
+ /**
1760
+ * Returns a record of recommended HTTP security headers.
1761
+ *
1762
+ * Each option accepts a `string` (override value) or `false` (disable).
1763
+ * Omitting an option uses the default value.
1764
+ *
1765
+ * `strictTransportSecurity` is opt-in only (no default) — HSTS is dangerous if not on HTTPS.
1766
+ */
1767
+ function securityHeaders(opts) {
1768
+ const result = {};
1769
+ for (const [optKey, headerName, defaultValue] of HEADER_MAP) {
1770
+ const value = opts?.[optKey];
1771
+ if (value === false) continue;
1772
+ if (typeof value === "string") result[headerName] = value;
1773
+ else if (defaultValue !== void 0) result[headerName] = defaultValue;
1774
+ }
1775
+ return result;
1776
+ }
1777
+
1778
+ //#endregion
1779
+ //#region packages/event-http/src/testing.ts
1780
+ /**
1781
+ * Creates a fully initialized HTTP event context for testing.
1782
+ *
1783
+ * Sets up an `EventContext` with a fake `IncomingMessage`, `HttpResponse`, route params,
1784
+ * and optional pre-seeded body. Returns a runner function that executes callbacks inside the context scope.
1785
+ *
1786
+ * @example
1787
+ * ```ts
1788
+ * const run = prepareTestHttpContext({ url: '/users/42', params: { id: '42' } })
1789
+ * run(() => {
1790
+ * const { params } = useRouteParams()
1791
+ * expect(params.id).toBe('42')
1792
+ * })
1793
+ * ```
1794
+ */
1795
+ function prepareTestHttpContext(options) {
1796
+ const req = new IncomingMessage(new Socket({}));
1797
+ req.method = options.method || "GET";
1798
+ req.headers = options.headers || {};
1799
+ req.url = options.url;
1800
+ const res = new ServerResponse(req);
1801
+ const response = new HttpResponse(res, req, console, options.defaultHeaders);
1802
+ const ctx = new EventContext({ logger: console });
1803
+ ctx.seed(httpKind, {
1804
+ req,
1805
+ response,
1806
+ requestLimits: options.requestLimits
1807
+ });
1808
+ if (options.params) ctx.set(routeParamsKey, options.params);
1809
+ if (options.rawBody !== void 0) {
1810
+ const buf = Buffer$1.isBuffer(options.rawBody) ? options.rawBody : Buffer$1.from(options.rawBody);
1811
+ ctx.set(rawBodySlot, Promise.resolve(buf));
1812
+ }
1813
+ return (cb) => run(ctx, cb);
1814
+ }
1815
+
1816
+ //#endregion
1817
+ export { DEFAULT_LIMITS, EHttpStatusCode, HttpError, HttpResponse, WooksHttp, WooksHttpResponse, WooksURLSearchParams, createHttpApp, createHttpContext, httpKind, httpStatusCodes, prepareTestHttpContext, rawBodySlot, renderCacheControl, securityHeaders, useAccept, useAuthorization, useCookies, useHeaders, useHttpContext, useLogger, useRequest, useResponse, useRouteParams, useUrlParams };