@wooksjs/http-body 0.7.25 → 0.7.27

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.cjs CHANGED
@@ -1,6 +1,7 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
2
  let _wooksjs_event_core = require("@wooksjs/event-core");
3
3
  let _wooksjs_event_http = require("@wooksjs/event-http");
4
+ let buffer = require("buffer");
4
5
 
5
6
  //#region packages/http-body/src/utils/safe-json.ts
6
7
  const ILLEGAL_KEYS = [
@@ -8,9 +9,15 @@ const ILLEGAL_KEYS = [
8
9
  "constructor",
9
10
  "prototype"
10
11
  ];
12
+ /**
13
+ * `JSON.parse` that rejects `__proto__` / `constructor` / `prototype` keys (400).
14
+ *
15
+ * A parsed key can only spell an illegal name when the source contains that name literally
16
+ * or uses a `\u` escape, so the key walk is skipped for sources that contain neither.
17
+ */
11
18
  function safeJsonParse(src) {
12
19
  const parsed = JSON.parse(src);
13
- assertNoProtoKeys(parsed);
20
+ if (src.includes("__proto__") || src.includes("constructor") || src.includes("prototype") || src.includes("\\u")) assertNoProtoKeys(parsed);
14
21
  return parsed;
15
22
  }
16
23
  function assertNoProtoKeys(obj) {
@@ -37,14 +44,11 @@ const CONTENT_TYPE_MAP = {
37
44
  "form-data": "multipart/form-data",
38
45
  urlencoded: "application/x-www-form-urlencoded"
39
46
  };
40
- const contentIsSlot = (0, _wooksjs_event_core.cachedBy)((type, ctx) => {
41
- const contentType = (0, _wooksjs_event_http.useHeaders)(ctx)["content-type"] || "";
42
- const mime = CONTENT_TYPE_MAP[type] || type;
43
- return contentType.includes(mime);
44
- });
47
+ /** The body's content type: the request's `content-type` header, or what {@link seedBody} set. */
48
+ const contentTypeSlot = (0, _wooksjs_event_core.cached)((ctx) => (0, _wooksjs_event_http.useHeaders)(ctx)["content-type"] || "");
45
49
  const parsedBodySlot = (0, _wooksjs_event_core.cached)(async (ctx) => {
46
50
  const { rawBody } = (0, _wooksjs_event_http.useRequest)(ctx);
47
- const contentType = (0, _wooksjs_event_http.useHeaders)(ctx)["content-type"] || "";
51
+ const contentType = ctx.get(contentTypeSlot);
48
52
  const contentIs = (type) => contentType.includes(type);
49
53
  const sBody = (await rawBody()).toString();
50
54
  if (contentIs("application/json")) return jsonParser(sBody);
@@ -131,14 +135,66 @@ function urlEncodedParser(v) {
131
135
  *
132
136
  * @returns Object with `is(type)` checker, `parseBody` function, and `rawBody` accessor.
133
137
  */
134
- const useBody = (0, _wooksjs_event_core.defineWook)((ctx) => {
138
+ const useBody = (0, _wooksjs_event_core.defineWook)((ctx) => bodyApi(ctx, false));
139
+ /** `ownParse`: parse in `ctx` itself, never reading a parent's parsed body through. */
140
+ function bodyApi(ctx, ownParse) {
135
141
  const { rawBody } = (0, _wooksjs_event_http.useRequest)(ctx);
136
142
  return {
137
- is: (type) => contentIsSlot(type, ctx),
138
- parseBody: () => ctx.get(parsedBodySlot),
143
+ is: (type) => ctx.get(contentTypeSlot).includes(CONTENT_TYPE_MAP[type] || type),
144
+ parseBody: () => ownParse ? ctx.getOwn(parsedBodySlot) : ctx.get(parsedBodySlot),
139
145
  rawBody
140
146
  };
141
- });
147
+ }
148
+ /**
149
+ * Seeds the request body of `ctx` with an already parsed value: `useBody(ctx).parseBody()`
150
+ * resolves to `body`, `useBody(ctx).rawBody()` / `useRequest(ctx).rawBody()` to its raw bytes,
151
+ * and `useBody(ctx).is()` checks the seeded content type — the incoming request stream is never
152
+ * read.
153
+ *
154
+ * Use it for a child event context (`new EventContext({ logger, parent })`) that runs a handler
155
+ * with its own payload: everything else (request, headers, auth) is still read through the
156
+ * parent, the body never is. Call it before anything in the child reads the body.
157
+ *
158
+ * @param ctx - The context to seed — usually a child of the current HTTP event
159
+ * @param body - The parsed body value
160
+ * @param opts - Raw bytes and content type (see {@link TSeedBodyOptions})
161
+ *
162
+ * @example
163
+ * ```ts
164
+ * const child = new EventContext({ logger: parent.logger, parent })
165
+ * seedBody(child, { ids: [1, 2] })
166
+ * await run(child, async () => {
167
+ * await useBody().parseBody() // { ids: [1, 2] }
168
+ * useBody().is('json') // true
169
+ * })
170
+ * ```
171
+ */
172
+ function seedBody(ctx, body, opts) {
173
+ const defaults = seedDefaults(body, opts?.raw);
174
+ (0, _wooksjs_event_http.seedRawBody)(ctx, defaults.raw);
175
+ const contentType = opts?.contentType ?? defaults.contentType;
176
+ if (contentType !== void 0) ctx.setOwn(contentTypeSlot, contentType);
177
+ const parseRaw = body === void 0 && opts?.raw !== void 0;
178
+ if (!parseRaw) ctx.setOwn(parsedBodySlot, Promise.resolve(body));
179
+ if (parseRaw || ctx.parent) ctx.setOwn(useBody._slot, bodyApi(ctx, true));
180
+ }
181
+ /** The raw bytes (`raw`, else derived from `body`) and default content type of a seeded body. */
182
+ function seedDefaults(body, raw) {
183
+ if (body === void 0) return { raw: raw ?? "" };
184
+ if (typeof body === "string") return {
185
+ raw: raw ?? body,
186
+ contentType: CONTENT_TYPE_MAP.text
187
+ };
188
+ if (buffer.Buffer.isBuffer(body)) return {
189
+ raw: raw ?? body,
190
+ contentType: CONTENT_TYPE_MAP.binary
191
+ };
192
+ return {
193
+ raw: raw ?? JSON.stringify(body) ?? "",
194
+ contentType: CONTENT_TYPE_MAP.json
195
+ };
196
+ }
142
197
 
143
198
  //#endregion
199
+ exports.seedBody = seedBody;
144
200
  exports.useBody = useBody;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  import * as _wooksjs_event_core from '@wooksjs/event-core';
2
+ import { EventContext } from '@wooksjs/event-core';
3
+ import { Buffer } from 'buffer';
2
4
 
3
5
  /** Short names for common Content-Type values. */
4
6
  type KnownContentType = 'json' | 'html' | 'xml' | 'text' | 'binary' | 'form-data' | 'urlencoded';
@@ -23,6 +25,47 @@ declare const useBody: _wooksjs_event_core.WookComposable<{
23
25
  parseBody: <T>() => Promise<T>;
24
26
  rawBody: () => Promise<Buffer<ArrayBufferLike>>;
25
27
  }>;
28
+ /** Options for {@link seedBody}. */
29
+ interface TSeedBodyOptions {
30
+ /**
31
+ * The raw bytes `rawBody()` returns. Default: `body` itself when it is a string or a
32
+ * `Buffer`, otherwise `JSON.stringify(body)`. With `body` `undefined`, `parseBody()` parses
33
+ * these bytes by the content type (as for a real request).
34
+ */
35
+ raw?: Buffer | string;
36
+ /**
37
+ * The content type `useBody().is()` checks against (and `parseBody()` parses `raw` by).
38
+ * Default: `'text/plain'` for a string, `'application/octet-stream'` for a `Buffer`,
39
+ * `'application/json'` for other values; with `body` `undefined`, the request's
40
+ * `Content-Type`. Request headers (`useHeaders()`) are not changed.
41
+ */
42
+ contentType?: string;
43
+ }
44
+ /**
45
+ * Seeds the request body of `ctx` with an already parsed value: `useBody(ctx).parseBody()`
46
+ * resolves to `body`, `useBody(ctx).rawBody()` / `useRequest(ctx).rawBody()` to its raw bytes,
47
+ * and `useBody(ctx).is()` checks the seeded content type — the incoming request stream is never
48
+ * read.
49
+ *
50
+ * Use it for a child event context (`new EventContext({ logger, parent })`) that runs a handler
51
+ * with its own payload: everything else (request, headers, auth) is still read through the
52
+ * parent, the body never is. Call it before anything in the child reads the body.
53
+ *
54
+ * @param ctx - The context to seed — usually a child of the current HTTP event
55
+ * @param body - The parsed body value
56
+ * @param opts - Raw bytes and content type (see {@link TSeedBodyOptions})
57
+ *
58
+ * @example
59
+ * ```ts
60
+ * const child = new EventContext({ logger: parent.logger, parent })
61
+ * seedBody(child, { ids: [1, 2] })
62
+ * await run(child, async () => {
63
+ * await useBody().parseBody() // { ids: [1, 2] }
64
+ * useBody().is('json') // true
65
+ * })
66
+ * ```
67
+ */
68
+ declare function seedBody(ctx: EventContext, body: unknown, opts?: TSeedBodyOptions): void;
26
69
 
27
- export { useBody };
28
- export type { KnownContentType };
70
+ export { seedBody, useBody };
71
+ export type { KnownContentType, TSeedBodyOptions };
package/dist/index.mjs CHANGED
@@ -1,5 +1,6 @@
1
- import { cached, cachedBy, defineWook } from "@wooksjs/event-core";
2
- import { EHttpStatusCode, HttpError, WooksURLSearchParams, useHeaders, useRequest } from "@wooksjs/event-http";
1
+ import { cached, defineWook } from "@wooksjs/event-core";
2
+ import { EHttpStatusCode, HttpError, WooksURLSearchParams, seedRawBody, useHeaders, useRequest } from "@wooksjs/event-http";
3
+ import { Buffer } from "buffer";
3
4
 
4
5
  //#region packages/http-body/src/utils/safe-json.ts
5
6
  const ILLEGAL_KEYS = [
@@ -7,9 +8,15 @@ const ILLEGAL_KEYS = [
7
8
  "constructor",
8
9
  "prototype"
9
10
  ];
11
+ /**
12
+ * `JSON.parse` that rejects `__proto__` / `constructor` / `prototype` keys (400).
13
+ *
14
+ * A parsed key can only spell an illegal name when the source contains that name literally
15
+ * or uses a `\u` escape, so the key walk is skipped for sources that contain neither.
16
+ */
10
17
  function safeJsonParse(src) {
11
18
  const parsed = JSON.parse(src);
12
- assertNoProtoKeys(parsed);
19
+ if (src.includes("__proto__") || src.includes("constructor") || src.includes("prototype") || src.includes("\\u")) assertNoProtoKeys(parsed);
13
20
  return parsed;
14
21
  }
15
22
  function assertNoProtoKeys(obj) {
@@ -36,14 +43,11 @@ const CONTENT_TYPE_MAP = {
36
43
  "form-data": "multipart/form-data",
37
44
  urlencoded: "application/x-www-form-urlencoded"
38
45
  };
39
- const contentIsSlot = cachedBy((type, ctx) => {
40
- const contentType = useHeaders(ctx)["content-type"] || "";
41
- const mime = CONTENT_TYPE_MAP[type] || type;
42
- return contentType.includes(mime);
43
- });
46
+ /** The body's content type: the request's `content-type` header, or what {@link seedBody} set. */
47
+ const contentTypeSlot = cached((ctx) => useHeaders(ctx)["content-type"] || "");
44
48
  const parsedBodySlot = cached(async (ctx) => {
45
49
  const { rawBody } = useRequest(ctx);
46
- const contentType = useHeaders(ctx)["content-type"] || "";
50
+ const contentType = ctx.get(contentTypeSlot);
47
51
  const contentIs = (type) => contentType.includes(type);
48
52
  const sBody = (await rawBody()).toString();
49
53
  if (contentIs("application/json")) return jsonParser(sBody);
@@ -130,14 +134,65 @@ function urlEncodedParser(v) {
130
134
  *
131
135
  * @returns Object with `is(type)` checker, `parseBody` function, and `rawBody` accessor.
132
136
  */
133
- const useBody = defineWook((ctx) => {
137
+ const useBody = defineWook((ctx) => bodyApi(ctx, false));
138
+ /** `ownParse`: parse in `ctx` itself, never reading a parent's parsed body through. */
139
+ function bodyApi(ctx, ownParse) {
134
140
  const { rawBody } = useRequest(ctx);
135
141
  return {
136
- is: (type) => contentIsSlot(type, ctx),
137
- parseBody: () => ctx.get(parsedBodySlot),
142
+ is: (type) => ctx.get(contentTypeSlot).includes(CONTENT_TYPE_MAP[type] || type),
143
+ parseBody: () => ownParse ? ctx.getOwn(parsedBodySlot) : ctx.get(parsedBodySlot),
138
144
  rawBody
139
145
  };
140
- });
146
+ }
147
+ /**
148
+ * Seeds the request body of `ctx` with an already parsed value: `useBody(ctx).parseBody()`
149
+ * resolves to `body`, `useBody(ctx).rawBody()` / `useRequest(ctx).rawBody()` to its raw bytes,
150
+ * and `useBody(ctx).is()` checks the seeded content type — the incoming request stream is never
151
+ * read.
152
+ *
153
+ * Use it for a child event context (`new EventContext({ logger, parent })`) that runs a handler
154
+ * with its own payload: everything else (request, headers, auth) is still read through the
155
+ * parent, the body never is. Call it before anything in the child reads the body.
156
+ *
157
+ * @param ctx - The context to seed — usually a child of the current HTTP event
158
+ * @param body - The parsed body value
159
+ * @param opts - Raw bytes and content type (see {@link TSeedBodyOptions})
160
+ *
161
+ * @example
162
+ * ```ts
163
+ * const child = new EventContext({ logger: parent.logger, parent })
164
+ * seedBody(child, { ids: [1, 2] })
165
+ * await run(child, async () => {
166
+ * await useBody().parseBody() // { ids: [1, 2] }
167
+ * useBody().is('json') // true
168
+ * })
169
+ * ```
170
+ */
171
+ function seedBody(ctx, body, opts) {
172
+ const defaults = seedDefaults(body, opts?.raw);
173
+ seedRawBody(ctx, defaults.raw);
174
+ const contentType = opts?.contentType ?? defaults.contentType;
175
+ if (contentType !== void 0) ctx.setOwn(contentTypeSlot, contentType);
176
+ const parseRaw = body === void 0 && opts?.raw !== void 0;
177
+ if (!parseRaw) ctx.setOwn(parsedBodySlot, Promise.resolve(body));
178
+ if (parseRaw || ctx.parent) ctx.setOwn(useBody._slot, bodyApi(ctx, true));
179
+ }
180
+ /** The raw bytes (`raw`, else derived from `body`) and default content type of a seeded body. */
181
+ function seedDefaults(body, raw) {
182
+ if (body === void 0) return { raw: raw ?? "" };
183
+ if (typeof body === "string") return {
184
+ raw: raw ?? body,
185
+ contentType: CONTENT_TYPE_MAP.text
186
+ };
187
+ if (Buffer.isBuffer(body)) return {
188
+ raw: raw ?? body,
189
+ contentType: CONTENT_TYPE_MAP.binary
190
+ };
191
+ return {
192
+ raw: raw ?? JSON.stringify(body) ?? "",
193
+ contentType: CONTENT_TYPE_MAP.json
194
+ };
195
+ }
141
196
 
142
197
  //#endregion
143
- export { useBody };
198
+ export { seedBody, useBody };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wooksjs/http-body",
3
- "version": "0.7.25",
3
+ "version": "0.7.27",
4
4
  "description": "@wooksjs/http-body",
5
5
  "keywords": [
6
6
  "api",
@@ -42,12 +42,12 @@
42
42
  "devDependencies": {
43
43
  "typescript": "^5.9.3",
44
44
  "vitest": "^3.2.7",
45
- "@wooksjs/event-core": "^0.7.25",
46
- "@wooksjs/event-http": "^0.7.25"
45
+ "@wooksjs/event-core": "^0.7.27",
46
+ "@wooksjs/event-http": "^0.7.27"
47
47
  },
48
48
  "peerDependencies": {
49
- "@wooksjs/event-core": "^0.7.25",
50
- "@wooksjs/event-http": "^0.7.25"
49
+ "@wooksjs/event-http": "^0.7.27",
50
+ "@wooksjs/event-core": "^0.7.27"
51
51
  },
52
52
  "scripts": {
53
53
  "build": "rolldown -c ../../rolldown.config.mjs"