@wooksjs/event-http 0.6.2 → 0.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -51,6 +51,30 @@ app.listen(3000, () => {
51
51
  })
52
52
  ```
53
53
 
54
+ ## AI Agent Skills
55
+
56
+ This package ships with structured skill files for AI coding agents (Claude Code, Cursor, Windsurf, Codex, etc.).
57
+
58
+ ```bash
59
+ # Project-local (recommended — version-locked, commits with your repo)
60
+ npx @wooksjs/event-http setup-skills
61
+
62
+ # Global (available across all your projects)
63
+ npx @wooksjs/event-http setup-skills --global
64
+ ```
65
+
66
+ To keep skills automatically up-to-date, add a postinstall script to your `package.json`:
67
+
68
+ ```json
69
+ {
70
+ "scripts": {
71
+ "postinstall": "npx @wooksjs/event-http setup-skills --postinstall"
72
+ }
73
+ }
74
+ ```
75
+
76
+ This ensures the skill files are refreshed whenever dependencies are installed, without needing a separate command.
77
+
54
78
  ## Documentation
55
79
 
56
80
  To check out docs, visit [wooks.moost.org](https://wooks.moost.org/webapp/).
package/dist/index.cjs CHANGED
@@ -32,6 +32,7 @@ const wooks = __toESM(require("wooks"));
32
32
  const stream = __toESM(require("stream"));
33
33
 
34
34
  //#region packages/event-http/src/event-http.ts
35
+ /** Creates an async event context for an incoming HTTP request/response pair. */
35
36
  function createHttpContext(data, options) {
36
37
  return (0, __wooksjs_event_core.createAsyncEventContext)({
37
38
  event: {
@@ -57,7 +58,7 @@ function escapeRegex(s) {
57
58
  function safeDecode(f, v) {
58
59
  try {
59
60
  return f(v);
60
- } catch (error) {
61
+ } catch {
61
62
  return v;
62
63
  }
63
64
  }
@@ -89,7 +90,12 @@ const units = {
89
90
 
90
91
  //#endregion
91
92
  //#region packages/event-http/src/utils/set-cookie.ts
93
+ const COOKIE_NAME_RE = /^[\w!#$%&'*+\-.^`|~]+$/;
94
+ function sanitizeCookieAttrValue(v) {
95
+ return v.replace(/[;\r\n]/g, "");
96
+ }
92
97
  function renderCookie(key, data) {
98
+ if (!COOKIE_NAME_RE.test(key)) throw new TypeError(`Invalid cookie name "${key}"`);
93
99
  let attrs = "";
94
100
  for (const [a, v] of Object.entries(data.attrs)) {
95
101
  const func = cookieAttrFunc[a];
@@ -103,8 +109,8 @@ function renderCookie(key, data) {
103
109
  const cookieAttrFunc = {
104
110
  expires: (v) => `Expires=${typeof v === "string" || typeof v === "number" ? new Date(v).toUTCString() : v.toUTCString()}`,
105
111
  maxAge: (v) => `Max-Age=${convertTime(v, "s").toString()}`,
106
- domain: (v) => `Domain=${v}`,
107
- path: (v) => `Path=${v}`,
112
+ domain: (v) => `Domain=${sanitizeCookieAttrValue(String(v))}`,
113
+ path: (v) => `Path=${sanitizeCookieAttrValue(String(v))}`,
108
114
  secure: (v) => v ? "Secure" : "",
109
115
  httpOnly: (v) => v ? "HttpOnly" : "",
110
116
  sameSite: (v) => v ? `SameSite=${typeof v === "string" ? v : "Strict"}` : ""
@@ -125,7 +131,7 @@ function encodingSupportsStream(encodings) {
125
131
  }
126
132
  async function uncompressBody(encodings, compressed) {
127
133
  let buf = compressed;
128
- for (const enc of encodings.slice().reverse()) {
134
+ for (const enc of encodings.slice().toReversed()) {
129
135
  const c = compressors[enc];
130
136
  if (!c) throw new Error(`Unsupported compression type "${enc}".`);
131
137
  buf = await c.uncompress(buf);
@@ -135,7 +141,7 @@ async function uncompressBody(encodings, compressed) {
135
141
  async function uncompressBodyStream(encodings, src) {
136
142
  if (!encodingSupportsStream(encodings)) throw new Error("Some encodings lack a streaming decompressor");
137
143
  let out = src;
138
- for (const enc of Array.from(encodings).reverse()) out = await compressors[enc].stream.uncompress(out);
144
+ for (const enc of Array.from(encodings).toReversed()) out = await compressors[enc].stream.uncompress(out);
139
145
  return out;
140
146
  }
141
147
 
@@ -455,8 +461,9 @@ function error_tl_default(ctx) {
455
461
  <title>${statusCode} ${statusMessage}</title>
456
462
  <style>
457
463
  body {
458
- font-family: -apple-system, BlinkMacMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,
459
- Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
464
+ font-family:
465
+ -apple-system, BlinkMacMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell,
466
+ 'Open Sans', 'Helvetica Neue', sans-serif;
460
467
  display: flex;
461
468
  justify-content: center;
462
469
  align-items: flex-start;
@@ -690,11 +697,12 @@ function error_tl_default(ctx) {
690
697
  //#endregion
691
698
  //#region packages/event-http/src/errors/error-renderer.ts
692
699
  let framework = {
693
- version: "0.6.1",
700
+ version: "0.6.2",
694
701
  poweredBy: `wooksjs`,
695
702
  link: `https://wooks.moost.org/`,
696
703
  image: `https://wooks.moost.org/wooks-full-logo.png`
697
704
  };
705
+ /** Renders HTTP error responses in HTML, JSON, or plain text based on the Accept header. */
698
706
  var HttpErrorRenderer = class extends BaseHttpResponseRenderer {
699
707
  constructor(opts) {
700
708
  super();
@@ -766,6 +774,7 @@ function escapeQuotes(s) {
766
774
 
767
775
  //#endregion
768
776
  //#region packages/event-http/src/errors/http-error.ts
777
+ /** Represents an HTTP error with a status code and optional structured body. */
769
778
  var HttpError = class extends Error {
770
779
  name = "HttpError";
771
780
  constructor(code = 500, _body = "") {
@@ -797,15 +806,24 @@ var HttpError = class extends Error {
797
806
  //#endregion
798
807
  //#region packages/event-http/src/composables/request.ts
799
808
  const xForwardedFor = "x-forwarded-for";
809
+ /** Default safety limits for request body reading (size, ratio, timeout). */
800
810
  const DEFAULT_LIMITS = {
801
811
  maxCompressed: 1 * 1024 * 1024,
802
812
  maxInflated: 10 * 1024 * 1024,
803
813
  maxRatio: 100,
804
814
  readTimeoutMs: 1e4
805
815
  };
816
+ /**
817
+ * Provides access to the incoming HTTP request (method, url, headers, body, IP).
818
+ * @example
819
+ * ```ts
820
+ * const { method, url, rawBody, getIp } = useRequest()
821
+ * const body = await rawBody()
822
+ * ```
823
+ */
806
824
  function useRequest() {
807
825
  const { store } = useHttpContext();
808
- const { init, get, set } = store("request");
826
+ const { init } = store("request");
809
827
  const event = store("event");
810
828
  const req = event.get("req");
811
829
  const contentEncoding = req.headers["content-encoding"];
@@ -819,18 +837,33 @@ function useRequest() {
819
837
  ].includes(p)) return true;
820
838
  return false;
821
839
  });
822
- const getMaxCompressed = () => get("maxCompressed") ?? DEFAULT_LIMITS.maxCompressed;
823
- const setMaxCompressed = (limit) => set("maxCompressed", limit);
824
- const getReadTimeoutMs = () => get("readTimeoutMs") ?? DEFAULT_LIMITS.readTimeoutMs;
825
- const setReadTimeoutMs = (limit) => set("readTimeoutMs", limit);
826
- const getMaxInflated = () => get("maxInflated") ?? DEFAULT_LIMITS.maxInflated;
827
- const setMaxInflated = (limit) => set("maxInflated", limit);
840
+ const limits = () => event.get("requestLimits");
841
+ const setLimit = (key, value) => {
842
+ let obj = limits();
843
+ if (!obj?.perRequest) {
844
+ obj = {
845
+ ...obj,
846
+ perRequest: true
847
+ };
848
+ event.set("requestLimits", obj);
849
+ }
850
+ obj[key] = value;
851
+ };
852
+ const getMaxCompressed = () => limits()?.maxCompressed ?? DEFAULT_LIMITS.maxCompressed;
853
+ const setMaxCompressed = (limit) => setLimit("maxCompressed", limit);
854
+ const getMaxInflated = () => limits()?.maxInflated ?? DEFAULT_LIMITS.maxInflated;
855
+ const setMaxInflated = (limit) => setLimit("maxInflated", limit);
856
+ const getMaxRatio = () => limits()?.maxRatio ?? DEFAULT_LIMITS.maxRatio;
857
+ const setMaxRatio = (limit) => setLimit("maxRatio", limit);
858
+ const getReadTimeoutMs = () => limits()?.readTimeoutMs ?? DEFAULT_LIMITS.readTimeoutMs;
859
+ const setReadTimeoutMs = (limit) => setLimit("readTimeoutMs", limit);
828
860
  const rawBody = () => init("rawBody", async () => {
829
861
  const encs = contentEncodings();
830
862
  const isZip = isCompressed();
831
863
  const streamable = isZip && encodingSupportsStream(encs);
832
864
  const maxCompressed = getMaxCompressed();
833
865
  const maxInflated = getMaxInflated();
866
+ const maxRatio = getMaxRatio();
834
867
  const timeoutMs = getReadTimeoutMs();
835
868
  const cl = Number(req.headers["content-length"] ?? 0);
836
869
  const upfrontLimit = isZip ? maxCompressed : maxInflated;
@@ -888,6 +921,7 @@ function useRequest() {
888
921
  inflatedBytes = body.byteLength;
889
922
  if (inflatedBytes > maxInflated) throw new HttpError(413, "Inflated body too large");
890
923
  }
924
+ if (isZip && rawBytes > 0 && inflatedBytes / rawBytes > maxRatio) throw new HttpError(413, "Compression ratio too high");
891
925
  return body;
892
926
  });
893
927
  const reqId = (0, __wooksjs_event_core.useEventId)().getId;
@@ -919,15 +953,32 @@ function useRequest() {
919
953
  getReadTimeoutMs,
920
954
  setReadTimeoutMs,
921
955
  getMaxInflated,
922
- setMaxInflated
956
+ setMaxInflated,
957
+ getMaxRatio,
958
+ setMaxRatio
923
959
  };
924
960
  }
925
961
 
926
962
  //#endregion
927
963
  //#region packages/event-http/src/composables/headers.ts
964
+ /**
965
+ * Returns the incoming request headers.
966
+ * @example
967
+ * ```ts
968
+ * const { host, authorization } = useHeaders()
969
+ * ```
970
+ */
928
971
  function useHeaders() {
929
972
  return useRequest().headers;
930
973
  }
974
+ /**
975
+ * Provides methods to set, get, and remove outgoing response headers.
976
+ * @example
977
+ * ```ts
978
+ * const { setHeader, setContentType, enableCors } = useSetHeaders()
979
+ * setHeader('x-request-id', '123')
980
+ * ```
981
+ */
931
982
  function useSetHeaders() {
932
983
  const { store } = useHttpContext();
933
984
  const setHeaderStore = store("setHeader");
@@ -949,6 +1000,7 @@ function useSetHeaders() {
949
1000
  enableCors
950
1001
  };
951
1002
  }
1003
+ /** Returns a hookable accessor for a single outgoing response header by name. */
952
1004
  function useSetHeader(name) {
953
1005
  const { store } = useHttpContext();
954
1006
  const { hook } = store("setHeader");
@@ -957,6 +1009,14 @@ function useSetHeader(name) {
957
1009
 
958
1010
  //#endregion
959
1011
  //#region packages/event-http/src/composables/cookies.ts
1012
+ /**
1013
+ * Provides access to parsed request cookies.
1014
+ * @example
1015
+ * ```ts
1016
+ * const { getCookie, rawCookies } = useCookies()
1017
+ * const sessionId = getCookie('session_id')
1018
+ * ```
1019
+ */
960
1020
  function useCookies() {
961
1021
  const { store } = useHttpContext();
962
1022
  const { cookie } = useHeaders();
@@ -972,6 +1032,7 @@ function useCookies() {
972
1032
  getCookie
973
1033
  };
974
1034
  }
1035
+ /** Provides methods to set, get, remove, and clear outgoing response cookies. */
975
1036
  function useSetCookies() {
976
1037
  const { store } = useHttpContext();
977
1038
  const cookiesStore = store("setCookies");
@@ -992,6 +1053,7 @@ function useSetCookies() {
992
1053
  cookies
993
1054
  };
994
1055
  }
1056
+ /** Returns a hookable accessor for a single outgoing cookie by name. */
995
1057
  function useSetCookie(name) {
996
1058
  const { setCookie, getCookie } = useSetCookies();
997
1059
  const valueHook = (0, __wooksjs_event_core.attachHook)({
@@ -1013,6 +1075,7 @@ function useSetCookie(name) {
1013
1075
 
1014
1076
  //#endregion
1015
1077
  //#region packages/event-http/src/composables/header-accept.ts
1078
+ /** Provides helpers to check the request's Accept header for supported MIME types. */
1016
1079
  function useAccept() {
1017
1080
  const { store } = useHttpContext();
1018
1081
  const { accept } = useHeaders();
@@ -1033,6 +1096,14 @@ function useAccept() {
1033
1096
 
1034
1097
  //#endregion
1035
1098
  //#region packages/event-http/src/composables/header-authorization.ts
1099
+ /**
1100
+ * Provides parsed access to the Authorization header (type, credentials, Basic decoding).
1101
+ * @example
1102
+ * ```ts
1103
+ * const { isBearer, authRawCredentials, basicCredentials } = useAuthorization()
1104
+ * if (isBearer()) { const token = authRawCredentials() }
1105
+ * ```
1106
+ */
1036
1107
  function useAuthorization() {
1037
1108
  const { store } = useHttpContext();
1038
1109
  const { authorization } = useHeaders();
@@ -1105,6 +1176,7 @@ const cacheControlFunc = {
1105
1176
  const renderAge = (v) => convertTime(v, "s").toString();
1106
1177
  const renderExpires = (v) => typeof v === "string" || typeof v === "number" ? new Date(v).toUTCString() : v.toUTCString();
1107
1178
  const renderPragmaNoCache = (v) => v ? "no-cache" : "";
1179
+ /** Provides helpers to set cache-related response headers (Cache-Control, Expires, Age, Pragma). */
1108
1180
  function useSetCacheControl() {
1109
1181
  const { setHeader } = useSetHeaders();
1110
1182
  const setAge = (value) => {
@@ -1129,6 +1201,14 @@ function useSetCacheControl() {
1129
1201
 
1130
1202
  //#endregion
1131
1203
  //#region packages/event-http/src/composables/response.ts
1204
+ /**
1205
+ * Provides access to the raw HTTP response and status code management.
1206
+ * @example
1207
+ * ```ts
1208
+ * const { status, rawResponse, hasResponded } = useResponse()
1209
+ * status(200)
1210
+ * ```
1211
+ */
1132
1212
  function useResponse() {
1133
1213
  const { store } = useHttpContext();
1134
1214
  const event = store("event");
@@ -1151,6 +1231,7 @@ function useResponse() {
1151
1231
  })
1152
1232
  };
1153
1233
  }
1234
+ /** Returns a hookable accessor for the response status code. */
1154
1235
  function useStatus() {
1155
1236
  const { store } = useHttpContext();
1156
1237
  return store("status").hook("code");
@@ -1158,6 +1239,11 @@ function useStatus() {
1158
1239
 
1159
1240
  //#endregion
1160
1241
  //#region packages/event-http/src/utils/url-search-params.ts
1242
+ const ILLEGAL_KEYS = new Set([
1243
+ "__proto__",
1244
+ "constructor",
1245
+ "prototype"
1246
+ ]);
1161
1247
  var WooksURLSearchParams = class extends url.URLSearchParams {
1162
1248
  toJson() {
1163
1249
  const json = Object.create(null);
@@ -1165,7 +1251,7 @@ var WooksURLSearchParams = class extends url.URLSearchParams {
1165
1251
  const a = json[key] = json[key] || [];
1166
1252
  a.push(value);
1167
1253
  } else {
1168
- if (key === "__proto__") throw new HttpError(400, `Illegal key name "${key}"`);
1254
+ if (ILLEGAL_KEYS.has(key)) throw new HttpError(400, `Illegal key name "${key}"`);
1169
1255
  if (key in json) throw new HttpError(400, `Duplicate key "${key}"`);
1170
1256
  json[key] = value;
1171
1257
  }
@@ -1178,6 +1264,14 @@ function isArrayParam(name) {
1178
1264
 
1179
1265
  //#endregion
1180
1266
  //#region packages/event-http/src/composables/search-params.ts
1267
+ /**
1268
+ * Provides access to URL search (query) parameters from the request.
1269
+ * @example
1270
+ * ```ts
1271
+ * const { urlSearchParams, jsonSearchParams } = useSearchParams()
1272
+ * const page = urlSearchParams().get('page')
1273
+ * ```
1274
+ */
1181
1275
  function useSearchParams() {
1182
1276
  const { store } = useHttpContext();
1183
1277
  const url$1 = useRequest().url || "";
@@ -1352,7 +1446,7 @@ var BaseHttpResponse = class {
1352
1446
  async function respondWithFetch(fetchBody, res) {
1353
1447
  if (fetchBody) try {
1354
1448
  for await (const chunk of fetchBody) res.write(chunk);
1355
- } catch (error) {}
1449
+ } catch {}
1356
1450
  res.end();
1357
1451
  }
1358
1452
 
@@ -1380,6 +1474,7 @@ function createWooksResponder(renderer = new BaseHttpResponseRenderer(), errorRe
1380
1474
 
1381
1475
  //#endregion
1382
1476
  //#region packages/event-http/src/http-adapter.ts
1477
+ /** HTTP adapter for Wooks that provides route registration, server lifecycle, and request handling. */
1383
1478
  var WooksHttp = class extends wooks.WooksAdapterBase {
1384
1479
  logger;
1385
1480
  constructor(opts, wooks$1) {
@@ -1387,27 +1482,35 @@ var WooksHttp = class extends wooks.WooksAdapterBase {
1387
1482
  this.opts = opts;
1388
1483
  this.logger = opts?.logger || this.getLogger(`[wooks-http]`);
1389
1484
  }
1485
+ /** Registers a handler for all HTTP methods on the given path. */
1390
1486
  all(path, handler) {
1391
1487
  return this.on("*", path, handler);
1392
1488
  }
1489
+ /** Registers a GET route handler. */
1393
1490
  get(path, handler) {
1394
1491
  return this.on("GET", path, handler);
1395
1492
  }
1493
+ /** Registers a POST route handler. */
1396
1494
  post(path, handler) {
1397
1495
  return this.on("POST", path, handler);
1398
1496
  }
1497
+ /** Registers a PUT route handler. */
1399
1498
  put(path, handler) {
1400
1499
  return this.on("PUT", path, handler);
1401
1500
  }
1501
+ /** Registers a PATCH route handler. */
1402
1502
  patch(path, handler) {
1403
1503
  return this.on("PATCH", path, handler);
1404
1504
  }
1505
+ /** Registers a DELETE route handler. */
1405
1506
  delete(path, handler) {
1406
1507
  return this.on("DELETE", path, handler);
1407
1508
  }
1509
+ /** Registers a HEAD route handler. */
1408
1510
  head(path, handler) {
1409
1511
  return this.on("HEAD", path, handler);
1410
1512
  }
1513
+ /** Registers an OPTIONS route handler. */
1411
1514
  options(path, handler) {
1412
1515
  return this.on("OPTIONS", path, handler);
1413
1516
  }
@@ -1465,8 +1568,8 @@ var WooksHttp = class extends wooks.WooksAdapterBase {
1465
1568
  }
1466
1569
  responder = createWooksResponder();
1467
1570
  respond(data) {
1468
- this.responder.respond(data)?.catch((e) => {
1469
- this.logger.error("Uncaught response exception", e);
1571
+ this.responder.respond(data)?.catch((error) => {
1572
+ this.logger.error("Uncaught response exception", error);
1470
1573
  });
1471
1574
  }
1472
1575
  /**
@@ -1485,7 +1588,8 @@ var WooksHttp = class extends wooks.WooksAdapterBase {
1485
1588
  return (req, res) => {
1486
1589
  const runInContext = createHttpContext({
1487
1590
  req,
1488
- res
1591
+ res,
1592
+ requestLimits: this.opts?.requestLimits
1489
1593
  }, this.mergeEventOptions(this.opts?.eventOptions));
1490
1594
  runInContext(async () => {
1491
1595
  const { handlers } = this.wooks.lookup(req.method, req.url);
@@ -1525,10 +1629,13 @@ var WooksHttp = class extends wooks.WooksAdapterBase {
1525
1629
  }
1526
1630
  };
1527
1631
  /**
1528
- * Factory for WooksHttp App
1529
- * @param opts TWooksHttpOptions
1530
- * @param wooks Wooks | WooksAdapterBase
1531
- * @returns WooksHttp
1632
+ * Creates a new WooksHttp application instance.
1633
+ * @example
1634
+ * ```ts
1635
+ * const app = createHttpApp()
1636
+ * app.get('/hello', () => 'Hello World!')
1637
+ * app.listen(3000)
1638
+ * ```
1532
1639
  */
1533
1640
  function createHttpApp(opts, wooks$1) {
1534
1641
  return new WooksHttp(opts, wooks$1);