hono 3.5.6 → 3.5.8

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.
@@ -51,18 +51,35 @@ const DEFAULT_OPTIONS = {
51
51
  };
52
52
  const secureHeaders = (customOptions) => {
53
53
  const options = { ...DEFAULT_OPTIONS, ...customOptions };
54
- const headersToSet = Object.entries(HEADERS_MAP).filter(([key]) => options[key]).map(([, value]) => value);
54
+ const headersToSet = Object.entries(HEADERS_MAP).filter(([key]) => options[key]).map(([key, defaultValue]) => {
55
+ const overrideValue = options[key];
56
+ if (typeof overrideValue === "string")
57
+ return [defaultValue[0], overrideValue];
58
+ return defaultValue;
59
+ });
60
+ if (options.contentSecurityPolicy) {
61
+ const cspDirectives = Object.entries(options.contentSecurityPolicy).map(([directive, value]) => {
62
+ directive = directive.replace(
63
+ /[A-Z]+(?![a-z])|[A-Z]/g,
64
+ (match, offset) => (offset ? "-" : "") + match.toLowerCase()
65
+ );
66
+ return `${directive} ${Array.isArray(value) ? value.join(" ") : value}`;
67
+ }).join("; ");
68
+ headersToSet.push(["Content-Security-Policy", cspDirectives]);
69
+ }
70
+ if (options.reportingEndpoints) {
71
+ const reportingEndpoints = options.reportingEndpoints.map((endpoint) => `${endpoint.name}="${endpoint.url}"`).join(", ");
72
+ headersToSet.push(["Reporting-Endpoints", reportingEndpoints]);
73
+ }
74
+ if (options.reportTo) {
75
+ const reportToOptions = options.reportTo.map((option) => JSON.stringify(option)).join(", ");
76
+ headersToSet.push(["Report-To", reportToOptions]);
77
+ }
55
78
  return async (ctx, next) => {
56
79
  await next();
57
80
  headersToSet.forEach(([header, value]) => {
58
81
  ctx.res.headers.set(header, value);
59
82
  });
60
- if (options.contentSecurityPolicy) {
61
- const cspDirectives = Object.entries(options.contentSecurityPolicy).map(([directive, sources]) => {
62
- return `${directive} ${sources.join(" ")}`;
63
- }).join("; ");
64
- ctx.res.headers.set("Content-Security-Policy", cspDirectives);
65
- }
66
83
  ctx.res.headers.delete("X-Powered-By");
67
84
  };
68
85
  };
@@ -32,6 +32,11 @@ class HonoRequest {
32
32
  const cachedBody = bodyCache[key];
33
33
  if (cachedBody)
34
34
  return cachedBody;
35
+ if (bodyCache.arrayBuffer) {
36
+ return (async () => {
37
+ return await new Response(bodyCache.arrayBuffer)[key]();
38
+ })();
39
+ }
35
40
  return bodyCache[key] = raw[key]();
36
41
  };
37
42
  this.raw = request;
@@ -84,7 +89,11 @@ class HonoRequest {
84
89
  }
85
90
  }
86
91
  async parseBody() {
87
- return await (0, import_body.parseBody)(this);
92
+ if (this.bodyCache.parsedBody)
93
+ return this.bodyCache.parsedBody;
94
+ const parsedBody = await (0, import_body.parseBody)(this);
95
+ this.bodyCache.parsedBody = parsedBody;
96
+ return parsedBody;
88
97
  }
89
98
  json() {
90
99
  return this.cachedBody("json");
@@ -21,15 +21,18 @@ __export(body_exports, {
21
21
  parseBody: () => parseBody
22
22
  });
23
23
  module.exports = __toCommonJS(body_exports);
24
- const parseBody = async (r) => {
24
+ const parseBody = async (request) => {
25
25
  let body = {};
26
- const contentType = r.headers.get("Content-Type");
26
+ const contentType = request.headers.get("Content-Type");
27
27
  if (contentType && (contentType.startsWith("multipart/form-data") || contentType.startsWith("application/x-www-form-urlencoded"))) {
28
- const form = {};
29
- (await r.formData()).forEach((value, key) => {
30
- form[key] = value;
31
- });
32
- body = form;
28
+ const formData = await request.formData();
29
+ if (formData) {
30
+ const form = {};
31
+ formData.forEach((value, key) => {
32
+ form[key] = value;
33
+ });
34
+ body = form;
35
+ }
33
36
  }
34
37
  return body;
35
38
  };
@@ -18,6 +18,7 @@ var __copyProps = (to, from, except, desc) => {
18
18
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
19
  var buffer_exports = {};
20
20
  __export(buffer_exports, {
21
+ bufferToFormData: () => bufferToFormData,
21
22
  bufferToString: () => bufferToString,
22
23
  equal: () => equal,
23
24
  timingSafeEqual: () => timingSafeEqual
@@ -59,8 +60,35 @@ const bufferToString = (buffer) => {
59
60
  }
60
61
  return buffer;
61
62
  };
63
+ const _decodeURIComponent = (str) => decodeURIComponent(str.replace(/\+/g, " "));
64
+ const bufferToFormData = (arrayBuffer, contentType) => {
65
+ const decoder = new TextDecoder("utf-8");
66
+ const content = decoder.decode(arrayBuffer);
67
+ const formData = new FormData();
68
+ const boundaryMatch = contentType.match(/boundary=(.+)/);
69
+ const boundary = boundaryMatch ? boundaryMatch[1] : "";
70
+ if (contentType.startsWith("multipart/form-data") && boundary) {
71
+ const parts = content.split("--" + boundary).slice(1, -1);
72
+ for (const part of parts) {
73
+ const [header, body] = part.split("\r\n\r\n");
74
+ const nameMatch = header.match(/name="([^"]+)"/);
75
+ if (nameMatch) {
76
+ const name = nameMatch[1];
77
+ formData.append(name, body.trim());
78
+ }
79
+ }
80
+ } else if (contentType.startsWith("application/x-www-form-urlencoded")) {
81
+ const pairs = content.split("&");
82
+ for (const pair of pairs) {
83
+ const [key, value] = pair.split("=");
84
+ formData.append(_decodeURIComponent(key), _decodeURIComponent(value));
85
+ }
86
+ }
87
+ return formData;
88
+ };
62
89
  // Annotate the CommonJS export names for ESM import in node:
63
90
  0 && (module.exports = {
91
+ bufferToFormData,
64
92
  bufferToString,
65
93
  equal,
66
94
  timingSafeEqual
@@ -22,13 +22,17 @@ __export(validator_exports, {
22
22
  });
23
23
  module.exports = __toCommonJS(validator_exports);
24
24
  var import_cookie = require("../helper/cookie");
25
+ var import_buffer = require("../utils/buffer");
25
26
  const validator = (target, validationFunc) => {
26
27
  return async (c, next) => {
27
28
  let value = {};
28
29
  switch (target) {
29
30
  case "json":
30
31
  try {
31
- value = await c.req.json();
32
+ const arrayBuffer = c.req.bodyCache.arrayBuffer ?? await c.req.raw.arrayBuffer();
33
+ value = await new Response(arrayBuffer).json();
34
+ c.req.bodyCache.json = value;
35
+ c.req.bodyCache.arrayBuffer = arrayBuffer;
32
36
  } catch {
33
37
  console.error("Error: Malformed JSON in request body");
34
38
  return c.json(
@@ -40,9 +44,21 @@ const validator = (target, validationFunc) => {
40
44
  );
41
45
  }
42
46
  break;
43
- case "form":
44
- value = await c.req.parseBody();
47
+ case "form": {
48
+ const contentType = c.req.header("Content-Type");
49
+ if (contentType) {
50
+ const arrayBuffer = c.req.bodyCache.arrayBuffer ?? await c.req.raw.arrayBuffer();
51
+ const formData = (0, import_buffer.bufferToFormData)(arrayBuffer, contentType);
52
+ const form = {};
53
+ formData.forEach((value2, key) => {
54
+ form[key] = value2;
55
+ });
56
+ value = form;
57
+ c.req.bodyCache.formData = formData;
58
+ c.req.bodyCache.arrayBuffer = arrayBuffer;
59
+ }
45
60
  break;
61
+ }
46
62
  case "query":
47
63
  value = Object.fromEntries(
48
64
  Object.entries(c.req.queries()).map(([k, v]) => {
@@ -29,18 +29,35 @@ var DEFAULT_OPTIONS = {
29
29
  };
30
30
  var secureHeaders = (customOptions) => {
31
31
  const options = { ...DEFAULT_OPTIONS, ...customOptions };
32
- const headersToSet = Object.entries(HEADERS_MAP).filter(([key]) => options[key]).map(([, value]) => value);
32
+ const headersToSet = Object.entries(HEADERS_MAP).filter(([key]) => options[key]).map(([key, defaultValue]) => {
33
+ const overrideValue = options[key];
34
+ if (typeof overrideValue === "string")
35
+ return [defaultValue[0], overrideValue];
36
+ return defaultValue;
37
+ });
38
+ if (options.contentSecurityPolicy) {
39
+ const cspDirectives = Object.entries(options.contentSecurityPolicy).map(([directive, value]) => {
40
+ directive = directive.replace(
41
+ /[A-Z]+(?![a-z])|[A-Z]/g,
42
+ (match, offset) => (offset ? "-" : "") + match.toLowerCase()
43
+ );
44
+ return `${directive} ${Array.isArray(value) ? value.join(" ") : value}`;
45
+ }).join("; ");
46
+ headersToSet.push(["Content-Security-Policy", cspDirectives]);
47
+ }
48
+ if (options.reportingEndpoints) {
49
+ const reportingEndpoints = options.reportingEndpoints.map((endpoint) => `${endpoint.name}="${endpoint.url}"`).join(", ");
50
+ headersToSet.push(["Reporting-Endpoints", reportingEndpoints]);
51
+ }
52
+ if (options.reportTo) {
53
+ const reportToOptions = options.reportTo.map((option) => JSON.stringify(option)).join(", ");
54
+ headersToSet.push(["Report-To", reportToOptions]);
55
+ }
33
56
  return async (ctx, next) => {
34
57
  await next();
35
58
  headersToSet.forEach(([header, value]) => {
36
59
  ctx.res.headers.set(header, value);
37
60
  });
38
- if (options.contentSecurityPolicy) {
39
- const cspDirectives = Object.entries(options.contentSecurityPolicy).map(([directive, sources]) => {
40
- return `${directive} ${sources.join(" ")}`;
41
- }).join("; ");
42
- ctx.res.headers.set("Content-Security-Policy", cspDirectives);
43
- }
44
61
  ctx.res.headers.delete("X-Powered-By");
45
62
  };
46
63
  };
package/dist/request.js CHANGED
@@ -10,6 +10,11 @@ var HonoRequest = class {
10
10
  const cachedBody = bodyCache[key];
11
11
  if (cachedBody)
12
12
  return cachedBody;
13
+ if (bodyCache.arrayBuffer) {
14
+ return (async () => {
15
+ return await new Response(bodyCache.arrayBuffer)[key]();
16
+ })();
17
+ }
13
18
  return bodyCache[key] = raw[key]();
14
19
  };
15
20
  this.raw = request;
@@ -62,7 +67,11 @@ var HonoRequest = class {
62
67
  }
63
68
  }
64
69
  async parseBody() {
65
- return await parseBody(this);
70
+ if (this.bodyCache.parsedBody)
71
+ return this.bodyCache.parsedBody;
72
+ const parsedBody = await parseBody(this);
73
+ this.bodyCache.parsedBody = parsedBody;
74
+ return parsedBody;
66
75
  }
67
76
  json() {
68
77
  return this.cachedBody("json");
@@ -35,7 +35,7 @@ declare class Hono<E extends Env = Env, S extends Schema = {}, BasePath extends
35
35
  private clone;
36
36
  private notFoundHandler;
37
37
  private errorHandler;
38
- route<SubPath extends string, SubEnv extends Env, SubSchema extends Schema, SubBasePath extends string>(path: SubPath, app: Hono<SubEnv, SubSchema, SubBasePath>): Hono<E, MergeSchemaPath<SubSchema, SubPath> & S, BasePath>;
38
+ route<SubPath extends string, SubEnv extends Env, SubSchema extends Schema, SubBasePath extends string>(path: SubPath, app: Hono<SubEnv, SubSchema, SubBasePath>): Hono<E, MergeSchemaPath<SubSchema, MergePath<BasePath, SubPath>> & S, BasePath>;
39
39
  /** @description
40
40
  * Use `basePath` instead of `route` when passing **one** argument, such as `app.route('/api')`.
41
41
  * The use of `route` with **one** argument has been removed in v4.
@@ -28,4 +28,4 @@ export declare const memo: <T>(component: FC<T>, propsAreEqual?: (prevProps: Rea
28
28
  export declare const Fragment: (props: {
29
29
  key?: string;
30
30
  children?: Child[];
31
- }) => JSXNode;
31
+ }) => HtmlEscapedString;
@@ -2,29 +2,56 @@ import type { MiddlewareHandler } from '../../types';
2
2
  interface ContentSecurityPolicyOptions {
3
3
  defaultSrc?: string[];
4
4
  baseUri?: string[];
5
+ childSrc?: string[];
6
+ connectSrc?: string[];
5
7
  fontSrc?: string[];
8
+ formAction?: string[];
6
9
  frameAncestors?: string[];
10
+ frameSrc?: string[];
7
11
  imgSrc?: string[];
12
+ manifestSrc?: string[];
13
+ mediaSrc?: string[];
8
14
  objectSrc?: string[];
15
+ reportTo?: string;
16
+ sandbox?: string[];
9
17
  scriptSrc?: string[];
10
18
  scriptSrcAttr?: string[];
19
+ scriptSrcElem?: string[];
11
20
  styleSrc?: string[];
21
+ styleSrcAttr?: string[];
22
+ styleSrcElem?: string[];
12
23
  upgradeInsecureRequests?: string[];
24
+ workerSrc?: string[];
13
25
  }
26
+ interface ReportToOptions {
27
+ group: string;
28
+ max_age: number;
29
+ endpoints: ReportToEndpoint[];
30
+ }
31
+ interface ReportToEndpoint {
32
+ url: string;
33
+ }
34
+ interface ReportingEndpointOptions {
35
+ name: string;
36
+ url: string;
37
+ }
38
+ declare type overridableHeader = boolean | string;
14
39
  interface SecureHeadersOptions {
15
40
  contentSecurityPolicy?: ContentSecurityPolicyOptions;
16
- crossOriginEmbedderPolicy?: boolean;
17
- crossOriginResourcePolicy?: boolean;
18
- crossOriginOpenerPolicy?: boolean;
19
- originAgentCluster: boolean;
20
- referrerPolicy?: boolean;
21
- strictTransportSecurity?: boolean;
22
- xContentTypeOptions?: boolean;
23
- xDnsPrefetchControl?: boolean;
24
- xDownloadOptions?: boolean;
25
- xFrameOptions?: boolean;
26
- xPermittedCrossDomainPolicies?: boolean;
27
- xXssProtection?: boolean;
41
+ crossOriginEmbedderPolicy?: overridableHeader;
42
+ crossOriginResourcePolicy?: overridableHeader;
43
+ crossOriginOpenerPolicy?: overridableHeader;
44
+ originAgentCluster: overridableHeader;
45
+ referrerPolicy?: overridableHeader;
46
+ reportingEndpoints?: ReportingEndpointOptions[];
47
+ reportTo?: ReportToOptions[];
48
+ strictTransportSecurity?: overridableHeader;
49
+ xContentTypeOptions?: overridableHeader;
50
+ xDnsPrefetchControl?: overridableHeader;
51
+ xDownloadOptions?: overridableHeader;
52
+ xFrameOptions?: overridableHeader;
53
+ xPermittedCrossDomainPolicies?: overridableHeader;
54
+ xXssProtection?: overridableHeader;
28
55
  }
29
56
  export declare const secureHeaders: (customOptions?: Partial<SecureHeadersOptions>) => MiddlewareHandler;
30
57
  export {};
@@ -2,12 +2,22 @@ import type { Input, InputToDataByTarget, ParamKeys, ParamKeyToRecord, RemoveQue
2
2
  import type { BodyData } from './utils/body';
3
3
  import type { Cookie } from './utils/cookie';
4
4
  import type { UnionToIntersection } from './utils/types';
5
+ declare type Body = {
6
+ json: any;
7
+ text: string;
8
+ arrayBuffer: ArrayBuffer;
9
+ blob: Blob;
10
+ formData: FormData;
11
+ };
12
+ declare type BodyCache = Partial<Body & {
13
+ parsedBody: BodyData;
14
+ }>;
5
15
  export declare class HonoRequest<P extends string = '/', I extends Input['out'] = {}> {
6
16
  raw: Request;
7
17
  private paramData;
8
18
  private vData;
9
19
  path: string;
10
- private bodyCache;
20
+ bodyCache: BodyCache;
11
21
  constructor(request: Request, path?: string, paramData?: Record<string, string> | undefined);
12
22
  param<P2 extends string = P>(key: RemoveQuestion<ParamKeys<P2>>): UndefinedIfHavingQuestion<ParamKeys<P2>>;
13
23
  param<P2 extends string = P>(): UnionToIntersection<ParamKeyToRecord<ParamKeys<P2>>>;
@@ -56,3 +66,4 @@ export declare class HonoRequest<P extends string = '/', I extends Input['out']
56
66
  get referrer(): string;
57
67
  get signal(): AbortSignal;
58
68
  }
69
+ export {};
@@ -1,3 +1,3 @@
1
1
  import type { HonoRequest } from '../request';
2
2
  export declare type BodyData = Record<string, string | File>;
3
- export declare const parseBody: <T extends BodyData = BodyData>(r: HonoRequest | Request) => Promise<T>;
3
+ export declare const parseBody: <T extends BodyData = BodyData>(request: HonoRequest | Request) => Promise<T>;
@@ -1,3 +1,4 @@
1
1
  export declare const equal: (a: ArrayBuffer, b: ArrayBuffer) => boolean;
2
2
  export declare const timingSafeEqual: (a: string | object | boolean, b: string | object | boolean, hashFunction?: Function) => Promise<boolean>;
3
3
  export declare const bufferToString: (buffer: ArrayBuffer) => string;
4
+ export declare const bufferToFormData: (arrayBuffer: ArrayBuffer, contentType: string) => FormData;
@@ -1,13 +1,16 @@
1
1
  // src/utils/body.ts
2
- var parseBody = async (r) => {
2
+ var parseBody = async (request) => {
3
3
  let body = {};
4
- const contentType = r.headers.get("Content-Type");
4
+ const contentType = request.headers.get("Content-Type");
5
5
  if (contentType && (contentType.startsWith("multipart/form-data") || contentType.startsWith("application/x-www-form-urlencoded"))) {
6
- const form = {};
7
- (await r.formData()).forEach((value, key) => {
8
- form[key] = value;
9
- });
10
- body = form;
6
+ const formData = await request.formData();
7
+ if (formData) {
8
+ const form = {};
9
+ formData.forEach((value, key) => {
10
+ form[key] = value;
11
+ });
12
+ body = form;
13
+ }
11
14
  }
12
15
  return body;
13
16
  };
@@ -35,7 +35,34 @@ var bufferToString = (buffer) => {
35
35
  }
36
36
  return buffer;
37
37
  };
38
+ var _decodeURIComponent = (str) => decodeURIComponent(str.replace(/\+/g, " "));
39
+ var bufferToFormData = (arrayBuffer, contentType) => {
40
+ const decoder = new TextDecoder("utf-8");
41
+ const content = decoder.decode(arrayBuffer);
42
+ const formData = new FormData();
43
+ const boundaryMatch = contentType.match(/boundary=(.+)/);
44
+ const boundary = boundaryMatch ? boundaryMatch[1] : "";
45
+ if (contentType.startsWith("multipart/form-data") && boundary) {
46
+ const parts = content.split("--" + boundary).slice(1, -1);
47
+ for (const part of parts) {
48
+ const [header, body] = part.split("\r\n\r\n");
49
+ const nameMatch = header.match(/name="([^"]+)"/);
50
+ if (nameMatch) {
51
+ const name = nameMatch[1];
52
+ formData.append(name, body.trim());
53
+ }
54
+ }
55
+ } else if (contentType.startsWith("application/x-www-form-urlencoded")) {
56
+ const pairs = content.split("&");
57
+ for (const pair of pairs) {
58
+ const [key, value] = pair.split("=");
59
+ formData.append(_decodeURIComponent(key), _decodeURIComponent(value));
60
+ }
61
+ }
62
+ return formData;
63
+ };
38
64
  export {
65
+ bufferToFormData,
39
66
  bufferToString,
40
67
  equal,
41
68
  timingSafeEqual
@@ -1,12 +1,16 @@
1
1
  // src/validator/validator.ts
2
2
  import { getCookie } from "../helper/cookie/index.js";
3
+ import { bufferToFormData } from "../utils/buffer.js";
3
4
  var validator = (target, validationFunc) => {
4
5
  return async (c, next) => {
5
6
  let value = {};
6
7
  switch (target) {
7
8
  case "json":
8
9
  try {
9
- value = await c.req.json();
10
+ const arrayBuffer = c.req.bodyCache.arrayBuffer ?? await c.req.raw.arrayBuffer();
11
+ value = await new Response(arrayBuffer).json();
12
+ c.req.bodyCache.json = value;
13
+ c.req.bodyCache.arrayBuffer = arrayBuffer;
10
14
  } catch {
11
15
  console.error("Error: Malformed JSON in request body");
12
16
  return c.json(
@@ -18,9 +22,21 @@ var validator = (target, validationFunc) => {
18
22
  );
19
23
  }
20
24
  break;
21
- case "form":
22
- value = await c.req.parseBody();
25
+ case "form": {
26
+ const contentType = c.req.header("Content-Type");
27
+ if (contentType) {
28
+ const arrayBuffer = c.req.bodyCache.arrayBuffer ?? await c.req.raw.arrayBuffer();
29
+ const formData = bufferToFormData(arrayBuffer, contentType);
30
+ const form = {};
31
+ formData.forEach((value2, key) => {
32
+ form[key] = value2;
33
+ });
34
+ value = form;
35
+ c.req.bodyCache.formData = formData;
36
+ c.req.bodyCache.arrayBuffer = arrayBuffer;
37
+ }
23
38
  break;
39
+ }
24
40
  case "query":
25
41
  value = Object.fromEntries(
26
42
  Object.entries(c.req.queries()).map(([k, v]) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hono",
3
- "version": "3.5.6",
3
+ "version": "3.5.8",
4
4
  "description": "Ultrafast web framework for the Edges",
5
5
  "main": "dist/cjs/index.js",
6
6
  "type": "module",