h3 1.4.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,13 +1,13 @@
1
- [![npm downloads](https://img.shields.io/npm/dm/h3.svg?style=flat-square)](https://npmjs.com/package/h3)
2
- [![version](https://img.shields.io/npm/v/h3/latest.svg?style=flat-square)](https://npmjs.com/package/h3)
3
- [![bundlephobia](https://img.shields.io/bundlephobia/min/h3/latest.svg?style=flat-square)](https://bundlephobia.com/result?p=h3)
4
- [![build status](https://img.shields.io/github/workflow/status/unjs/h3/ci/main?style=flat-square)](https://github.com/unjs/h3/actions)
5
- [![coverage](https://img.shields.io/codecov/c/gh/unjs/h3/main?style=flat-square)](https://codecov.io/gh/unjs/h3)
6
- [![jsDocs.io](https://img.shields.io/badge/jsDocs.io-reference-blue?style=flat-square)](https://www.jsdocs.io/package/h3)
1
+ # H3
7
2
 
8
- > H3 is a minimal h(ttp) framework built for high performance and portability
3
+ [![npm version][npm-version-src]][npm-version-href]
4
+ [![npm downloads][npm-downloads-src]][npm-downloads-href]
5
+ [![bundle][bundle-src]][bundle-href]
6
+ [![Codecov][codecov-src]][codecov-href]
7
+ [![License][license-src]][license-href]
8
+ [![JSDocs][jsdocs-src]][jsdocs-href]
9
9
 
10
- <!-- ![h3 - Tiny JavaScript Server](.github/banner.svg) -->
10
+ H3 is a minimal h(ttp) framework built for high performance and portability.
11
11
 
12
12
  ## Features
13
13
 
@@ -110,6 +110,19 @@ app.use(eventHandler(() => '<h1>Hello world!</h1>'))
110
110
  app.use('/1', eventHandler(() => '<h1>Hello world!</h1>'))
111
111
  .use('/2', eventHandler(() => '<h1>Goodbye!</h1>'))
112
112
 
113
+ // We can proxy requests and rewrite cookie's domain and path
114
+ app.use('/api', eventHandler((event) => proxyRequest('https://example.com', {
115
+ // f.e. keep one domain unchanged, rewrite one domain and remove other domains
116
+ cookieDomainRewrite: {
117
+ "example.com": "example.com",
118
+ "example.com": "somecompany.co.uk",
119
+ "*": "",
120
+ },
121
+ cookiePathRewrite: {
122
+ "/": "/api"
123
+ },
124
+ }))
125
+
113
126
  // Legacy middleware with 3rd argument are automatically promisified
114
127
  app.use(fromNodeMiddleware((req, res, next) => { req.setHeader('x-foo', 'bar'); next() }))
115
128
 
@@ -146,8 +159,8 @@ H3 has a concept of composable utilities that accept `event` (from `eventHandler
146
159
  - `isMethod(event, expected, allowHead?)`
147
160
  - `assertMethod(event, expected, allowHead?)`
148
161
  - `createError({ statusCode, statusMessage, data? })`
149
- - `sendProxy(event, { target, headers?, fetchOptions?, fetch?, sendStream? })`
150
- - `proxyRequest(event, { target, headers?, fetchOptions?, fetch?, sendStream? })`
162
+ - `sendProxy(event, { target, ...options })`
163
+ - `proxyRequest(event, { target, ...options })`
151
164
  - `fetchWithEvent(event, req, init, { fetch? }?)`
152
165
  - `getProxyRequestHeaders(event)`
153
166
  - `sendNoContent(event, code = 204)`
@@ -161,6 +174,14 @@ H3 has a concept of composable utilities that accept `event` (from `eventHandler
161
174
  - `clearSession(event, config)`
162
175
  - `sealSession(event, config)`
163
176
  - `unsealSession(event, config, sealed)`
177
+ - `handleCors(options)` (see [h3-cors](https://github.com/NozomuIkuta/h3-cors) for more detail about options)
178
+ - `isPreflightRequest(event)`
179
+ - `isCorsOriginAllowed(event)`
180
+ - `appendCorsHeaders(event, options)` (see [h3-cors](https://github.com/NozomuIkuta/h3-cors) for more detail about options)
181
+ - `appendCorsPreflightHeaders(event, options)` (see [h3-cors](https://github.com/NozomuIkuta/h3-cors) for more detail about options)
182
+ - `getRequestHost(event)`
183
+ - `getRequestProtocol(event)`
184
+ - `getRequestURL(event)`
164
185
 
165
186
  👉 You can learn more about usage in [JSDocs Documentation](https://www.jsdocs.io/package/h3#package-functions).
166
187
 
@@ -172,9 +193,6 @@ Please check their READMEs for more details.
172
193
 
173
194
  PRs are welcome to add your packages.
174
195
 
175
- - [h3-cors](https://github.com/NozomuIkuta/h3-cors)
176
- - `defineCorsEventHandler(options)`
177
- - `isPreflight(event)`
178
196
  - [h3-typebox](https://github.com/kevinmarrec/h3-typebox)
179
197
  - `validateBody(event, schema)`
180
198
  - `validateQuery(event, schema)`
@@ -185,3 +203,18 @@ PRs are welcome to add your packages.
185
203
  ## License
186
204
 
187
205
  MIT
206
+
207
+ <!-- Badges -->
208
+
209
+ [npm-version-src]: https://img.shields.io/npm/v/h3?style=flat&colorA=18181B&colorB=F0DB4F
210
+ [npm-version-href]: https://npmjs.com/package/h3
211
+ [npm-downloads-src]: https://img.shields.io/npm/dm/h3?style=flat&colorA=18181B&colorB=F0DB4F
212
+ [npm-downloads-href]: https://npmjs.com/package/h3
213
+ [codecov-src]: https://img.shields.io/codecov/c/gh/unjs/h3/main?style=flat&colorA=18181B&colorB=F0DB4F
214
+ [codecov-href]: https://codecov.io/gh/unjs/h3
215
+ [bundle-src]: https://img.shields.io/bundlephobia/minzip/h3?style=flat&colorA=18181B&colorB=F0DB4F
216
+ [bundle-href]: https://bundlephobia.com/result?p=h3
217
+ [license-src]: https://img.shields.io/github/license/unjs/h3.svg?style=flat&colorA=18181B&colorB=F0DB4F
218
+ [license-href]: https://github.com/unjs/h3/blob/main/LICENSE
219
+ [jsdocs-src]: https://img.shields.io/badge/jsDocs.io-reference-18181B?style=flat&colorA=18181B&colorB=F0DB4F
220
+ [jsdocs-href]: https://www.jsdocs.io/package/h3
package/dist/index.cjs CHANGED
@@ -6,6 +6,7 @@ const destr = require('destr');
6
6
  const cookieEs = require('cookie-es');
7
7
  const crypto = require('uncrypto');
8
8
  const ironWebcrypto = require('iron-webcrypto');
9
+ const defu = require('defu');
9
10
 
10
11
  function useBase(base, handler) {
11
12
  base = ufo.withoutTrailingSlash(base);
@@ -247,6 +248,24 @@ function getRequestHeader(event, name) {
247
248
  return value;
248
249
  }
249
250
  const getHeader = getRequestHeader;
251
+ function getRequestHost(event) {
252
+ const xForwardedHost = event.node.req.headers["x-forwarded-host"];
253
+ if (xForwardedHost) {
254
+ return xForwardedHost;
255
+ }
256
+ return event.node.req.headers.host || "localhost";
257
+ }
258
+ function getRequestProtocol(event) {
259
+ if (event.node.req.headers["x-forwarded-proto"] === "https") {
260
+ return "https";
261
+ }
262
+ return event.node.req.connection.encrypted ? "https" : "http";
263
+ }
264
+ function getRequestURL(event) {
265
+ const host = getRequestHost(event);
266
+ const protocol = getRequestProtocol(event);
267
+ return new URL(event.path || "/", `${protocol}://${host}`);
268
+ }
250
269
 
251
270
  const RawBodySymbol = Symbol.for("h3RawBody");
252
271
  const ParsedBodySymbol = Symbol.for("h3ParsedBody");
@@ -379,6 +398,58 @@ function deleteCookie(event, name, serializeOptions) {
379
398
  maxAge: 0
380
399
  });
381
400
  }
401
+ function splitCookiesString(cookiesString) {
402
+ if (typeof cookiesString !== "string") {
403
+ return [];
404
+ }
405
+ const cookiesStrings = [];
406
+ let pos = 0;
407
+ let start;
408
+ let ch;
409
+ let lastComma;
410
+ let nextStart;
411
+ let cookiesSeparatorFound;
412
+ function skipWhitespace() {
413
+ while (pos < cookiesString.length && /\s/.test(cookiesString.charAt(pos))) {
414
+ pos += 1;
415
+ }
416
+ return pos < cookiesString.length;
417
+ }
418
+ function notSpecialChar() {
419
+ ch = cookiesString.charAt(pos);
420
+ return ch !== "=" && ch !== ";" && ch !== ",";
421
+ }
422
+ while (pos < cookiesString.length) {
423
+ start = pos;
424
+ cookiesSeparatorFound = false;
425
+ while (skipWhitespace()) {
426
+ ch = cookiesString.charAt(pos);
427
+ if (ch === ",") {
428
+ lastComma = pos;
429
+ pos += 1;
430
+ skipWhitespace();
431
+ nextStart = pos;
432
+ while (pos < cookiesString.length && notSpecialChar()) {
433
+ pos += 1;
434
+ }
435
+ if (pos < cookiesString.length && cookiesString.charAt(pos) === "=") {
436
+ cookiesSeparatorFound = true;
437
+ pos = nextStart;
438
+ cookiesStrings.push(cookiesString.slice(start, lastComma));
439
+ start = pos;
440
+ } else {
441
+ pos = lastComma + 1;
442
+ }
443
+ } else {
444
+ pos += 1;
445
+ }
446
+ }
447
+ if (!cookiesSeparatorFound || pos >= cookiesString.length) {
448
+ cookiesStrings.push(cookiesString.slice(start, cookiesString.length));
449
+ }
450
+ }
451
+ return cookiesStrings;
452
+ }
382
453
 
383
454
  const PayloadMethods = /* @__PURE__ */ new Set(["PATCH", "POST", "PUT", "DELETE"]);
384
455
  const ignoredHeaders = /* @__PURE__ */ new Set([
@@ -426,6 +497,27 @@ async function sendProxy(event, target, opts = {}) {
426
497
  if (key === "content-length") {
427
498
  continue;
428
499
  }
500
+ if (key === "set-cookie") {
501
+ const cookies = splitCookiesString(value).map((cookie) => {
502
+ if (opts.cookieDomainRewrite) {
503
+ cookie = rewriteCookieProperty(
504
+ cookie,
505
+ opts.cookieDomainRewrite,
506
+ "domain"
507
+ );
508
+ }
509
+ if (opts.cookiePathRewrite) {
510
+ cookie = rewriteCookieProperty(
511
+ cookie,
512
+ opts.cookiePathRewrite,
513
+ "path"
514
+ );
515
+ }
516
+ return cookie;
517
+ });
518
+ event.node.res.setHeader("set-cookie", cookies);
519
+ continue;
520
+ }
429
521
  event.node.res.setHeader(key, value);
430
522
  }
431
523
  if (response._data !== void 0) {
@@ -453,8 +545,7 @@ function getProxyRequestHeaders(event) {
453
545
  function fetchWithEvent(event, req, init, options) {
454
546
  return _getFetch(options?.fetch)(req, {
455
547
  ...init,
456
- // @ts-ignore (context is used for unenv and local fetch)
457
- context: init.context || event.context,
548
+ context: init?.context || event.context,
458
549
  headers: {
459
550
  ...getProxyRequestHeaders(event),
460
551
  ...init?.headers
@@ -472,6 +563,23 @@ function _getFetch(_fetch) {
472
563
  "fetch is not available. Try importing `node-fetch-native/polyfill` for Node.js."
473
564
  );
474
565
  }
566
+ function rewriteCookieProperty(header, map, property) {
567
+ const _map = typeof map === "string" ? { "*": map } : map;
568
+ return header.replace(
569
+ new RegExp(`(;\\s*${property}=)([^;]+)`, "gi"),
570
+ (match, prefix, previousValue) => {
571
+ let newValue;
572
+ if (previousValue in _map) {
573
+ newValue = _map[previousValue];
574
+ } else if ("*" in _map) {
575
+ newValue = _map["*"];
576
+ } else {
577
+ return match;
578
+ }
579
+ return newValue ? prefix + newValue : "";
580
+ }
581
+ );
582
+ }
475
583
 
476
584
  const defer = typeof setImmediate !== "undefined" ? setImmediate : (fn) => fn();
477
585
  function send(event, data, type) {
@@ -730,6 +838,120 @@ async function clearSession(event, config) {
730
838
  });
731
839
  }
732
840
 
841
+ function resolveCorsOptions(options = {}) {
842
+ const defaultOptions = {
843
+ origin: "*",
844
+ methods: "*",
845
+ allowHeaders: "*",
846
+ exposeHeaders: "*",
847
+ credentials: false,
848
+ maxAge: false,
849
+ preflight: {
850
+ statusCode: 204
851
+ }
852
+ };
853
+ return defu.defu(options, defaultOptions);
854
+ }
855
+ function isPreflightRequest(event) {
856
+ const method = getMethod(event);
857
+ const origin = getRequestHeader(event, "origin");
858
+ const accessControlRequestMethod = getRequestHeader(
859
+ event,
860
+ "access-control-request-method"
861
+ );
862
+ return method === "OPTIONS" && !!origin && !!accessControlRequestMethod;
863
+ }
864
+ function isCorsOriginAllowed(origin, options) {
865
+ const { origin: originOption } = options;
866
+ if (!origin || !originOption || originOption === "*" || originOption === "null") {
867
+ return true;
868
+ }
869
+ if (Array.isArray(originOption)) {
870
+ return originOption.some((_origin) => {
871
+ if (_origin instanceof RegExp) {
872
+ return _origin.test(origin);
873
+ }
874
+ return origin === _origin;
875
+ });
876
+ }
877
+ return originOption(origin);
878
+ }
879
+ function createOriginHeaders(event, options) {
880
+ const { origin: originOption } = options;
881
+ const origin = getRequestHeader(event, "origin");
882
+ if (!origin || !originOption || originOption === "*") {
883
+ return { "access-control-allow-origin": "*" };
884
+ }
885
+ if (typeof originOption === "string") {
886
+ return { "access-control-allow-origin": originOption, vary: "origin" };
887
+ }
888
+ return isCorsOriginAllowed(origin, options) ? { "access-control-allow-origin": origin, vary: "origin" } : {};
889
+ }
890
+ function createMethodsHeaders(options) {
891
+ const { methods } = options;
892
+ if (!methods) {
893
+ return {};
894
+ }
895
+ if (methods === "*") {
896
+ return { "access-control-allow-methods": "*" };
897
+ }
898
+ return methods.length > 0 ? { "access-control-allow-methods": methods.join(",") } : {};
899
+ }
900
+ function createCredentialsHeaders(options) {
901
+ const { credentials } = options;
902
+ if (credentials) {
903
+ return { "access-control-allow-credentials": "true" };
904
+ }
905
+ return {};
906
+ }
907
+ function createAllowHeaderHeaders(event, options) {
908
+ const { allowHeaders } = options;
909
+ if (!allowHeaders || allowHeaders === "*" || allowHeaders.length === 0) {
910
+ const header = getRequestHeader(event, "access-control-request-headers");
911
+ return header ? {
912
+ "access-control-allow-headers": header,
913
+ vary: "access-control-request-headers"
914
+ } : {};
915
+ }
916
+ return {
917
+ "access-control-allow-headers": allowHeaders.join(","),
918
+ vary: "access-control-request-headers"
919
+ };
920
+ }
921
+ function createExposeHeaders(options) {
922
+ const { exposeHeaders } = options;
923
+ if (!exposeHeaders) {
924
+ return {};
925
+ }
926
+ if (exposeHeaders === "*") {
927
+ return { "access-control-expose-headers": exposeHeaders };
928
+ }
929
+ return { "access-control-expose-headers": exposeHeaders.join(",") };
930
+ }
931
+ function appendCorsPreflightHeaders(event, options) {
932
+ appendHeaders(event, createOriginHeaders(event, options));
933
+ appendHeaders(event, createCredentialsHeaders(options));
934
+ appendHeaders(event, createExposeHeaders(options));
935
+ appendHeaders(event, createMethodsHeaders(options));
936
+ appendHeaders(event, createAllowHeaderHeaders(event, options));
937
+ }
938
+ function appendCorsHeaders(event, options) {
939
+ appendHeaders(event, createOriginHeaders(event, options));
940
+ appendHeaders(event, createCredentialsHeaders(options));
941
+ appendHeaders(event, createExposeHeaders(options));
942
+ }
943
+
944
+ function handleCors(event, options) {
945
+ const _options = resolveCorsOptions(options);
946
+ if (isPreflightRequest(event)) {
947
+ appendCorsPreflightHeaders(event, options);
948
+ sendNoContent(event, _options.preflight.statusCode);
949
+ return true;
950
+ }
951
+ appendCorsHeaders(event, options);
952
+ return false;
953
+ }
954
+
733
955
  class H3Headers {
734
956
  constructor(init) {
735
957
  if (!init) {
@@ -1184,6 +1406,8 @@ exports.H3Event = H3Event;
1184
1406
  exports.H3Headers = H3Headers;
1185
1407
  exports.H3Response = H3Response;
1186
1408
  exports.MIMES = MIMES;
1409
+ exports.appendCorsHeaders = appendCorsHeaders;
1410
+ exports.appendCorsPreflightHeaders = appendCorsPreflightHeaders;
1187
1411
  exports.appendHeader = appendHeader;
1188
1412
  exports.appendHeaders = appendHeaders;
1189
1413
  exports.appendResponseHeader = appendResponseHeader;
@@ -1214,6 +1438,9 @@ exports.getProxyRequestHeaders = getProxyRequestHeaders;
1214
1438
  exports.getQuery = getQuery;
1215
1439
  exports.getRequestHeader = getRequestHeader;
1216
1440
  exports.getRequestHeaders = getRequestHeaders;
1441
+ exports.getRequestHost = getRequestHost;
1442
+ exports.getRequestProtocol = getRequestProtocol;
1443
+ exports.getRequestURL = getRequestURL;
1217
1444
  exports.getResponseHeader = getResponseHeader;
1218
1445
  exports.getResponseHeaders = getResponseHeaders;
1219
1446
  exports.getResponseStatus = getResponseStatus;
@@ -1222,10 +1449,13 @@ exports.getRouterParam = getRouterParam;
1222
1449
  exports.getRouterParams = getRouterParams;
1223
1450
  exports.getSession = getSession;
1224
1451
  exports.handleCacheHeaders = handleCacheHeaders;
1452
+ exports.handleCors = handleCors;
1453
+ exports.isCorsOriginAllowed = isCorsOriginAllowed;
1225
1454
  exports.isError = isError;
1226
1455
  exports.isEvent = isEvent;
1227
1456
  exports.isEventHandler = isEventHandler;
1228
1457
  exports.isMethod = isMethod;
1458
+ exports.isPreflightRequest = isPreflightRequest;
1229
1459
  exports.isStream = isStream;
1230
1460
  exports.lazyEventHandler = lazyEventHandler;
1231
1461
  exports.parseCookies = parseCookies;
@@ -1247,6 +1477,7 @@ exports.setHeaders = setHeaders;
1247
1477
  exports.setResponseHeader = setResponseHeader;
1248
1478
  exports.setResponseHeaders = setResponseHeaders;
1249
1479
  exports.setResponseStatus = setResponseStatus;
1480
+ exports.splitCookiesString = splitCookiesString;
1250
1481
  exports.toEventHandler = toEventHandler;
1251
1482
  exports.toNodeListener = toNodeListener;
1252
1483
  exports.unsealSession = unsealSession;
package/dist/index.d.ts CHANGED
@@ -27,7 +27,7 @@ interface SessionConfig {
27
27
  }
28
28
  declare function useSession<T extends SessionDataT = SessionDataT>(event: H3Event, config: SessionConfig): Promise<{
29
29
  readonly id: string | undefined;
30
- readonly data: SessionDataT;
30
+ readonly data: T;
31
31
  update: (update: SessionUpdate<T>) => Promise<any>;
32
32
  clear: () => Promise<any>;
33
33
  }>;
@@ -36,7 +36,7 @@ type SessionUpdate<T extends SessionDataT = SessionDataT> = Partial<SessionData<
36
36
  declare function updateSession<T extends SessionDataT = SessionDataT>(event: H3Event, config: SessionConfig, update?: SessionUpdate<T>): Promise<Session<T>>;
37
37
  declare function sealSession<T extends SessionDataT = SessionDataT>(event: H3Event, config: SessionConfig): Promise<string>;
38
38
  declare function unsealSession(_event: H3Event, config: SessionConfig, sealed: string): Promise<Partial<Session<SessionDataT>>>;
39
- declare function clearSession(event: H3Event, config: SessionConfig): Promise<void>;
39
+ declare function clearSession(event: H3Event, config: Partial<SessionConfig>): Promise<void>;
40
40
 
41
41
  type HTTPMethod = "GET" | "HEAD" | "PATCH" | "POST" | "PUT" | "DELETE" | "CONNECT" | "OPTIONS" | "TRACE";
42
42
  type Encoding = false | "ascii" | "utf8" | "utf-8" | "utf16le" | "ucs2" | "ucs-2" | "base64" | "latin1" | "binary" | "hex";
@@ -231,7 +231,7 @@ declare function readRawBody<E extends Encoding = "utf8">(event: H3Event, encodi
231
231
  * @return {*} The `Object`, `Array`, `String`, `Number`, `Boolean`, or `null` value corresponding to the request JSON body
232
232
  *
233
233
  * ```ts
234
- * const body = await useBody(req)
234
+ * const body = await readBody(req)
235
235
  * ```
236
236
  */
237
237
  declare function readBody<T = any>(event: H3Event): Promise<T>;
@@ -270,7 +270,7 @@ declare function parseCookies(event: H3Event): Record<string, string>;
270
270
  * @param name Name of the cookie to get
271
271
  * @returns {*} Value of the cookie (String or undefined)
272
272
  * ```ts
273
- * const authorization = useCookie(request, 'Authorization')
273
+ * const authorization = getCookie(request, 'Authorization')
274
274
  * ```
275
275
  */
276
276
  declare function getCookie(event: H3Event, name: string): string | undefined;
@@ -295,17 +295,31 @@ declare function setCookie(event: H3Event, name: string, value: string, serializ
295
295
  * ```
296
296
  */
297
297
  declare function deleteCookie(event: H3Event, name: string, serializeOptions?: CookieSerializeOptions): void;
298
+ /**
299
+ * Set-Cookie header field-values are sometimes comma joined in one string. This splits them without choking on commas
300
+ * that are within a single set-cookie field-value, such as in the Expires portion.
301
+ * This is uncommon, but explicitly allowed - see https://tools.ietf.org/html/rfc2616#section-4.2
302
+ * Node.js does this for every header *except* set-cookie - see https://github.com/nodejs/node/blob/d5e363b77ebaf1caf67cd7528224b651c86815c1/lib/_http_incoming.js#L128
303
+ * Based on: https://github.com/google/j2objc/commit/16820fdbc8f76ca0c33472810ce0cb03d20efe25
304
+ * Credits to: https://github.com/tomball for original and https://github.com/chrusart for JavaScript implementation
305
+ * @source https://github.com/nfriedly/set-cookie-parser/blob/3eab8b7d5d12c8ed87832532861c1a35520cf5b3/lib/set-cookie.js#L144
306
+ */
307
+ declare function splitCookiesString(cookiesString: string): string[];
298
308
 
299
309
  interface ProxyOptions {
300
310
  headers?: RequestHeaders | HeadersInit;
301
311
  fetchOptions?: RequestInit;
302
312
  fetch?: typeof fetch;
303
313
  sendStream?: boolean;
314
+ cookieDomainRewrite?: string | Record<string, string>;
315
+ cookiePathRewrite?: string | Record<string, string>;
304
316
  }
305
317
  declare function proxyRequest(event: H3Event, target: string, opts?: ProxyOptions): Promise<any>;
306
318
  declare function sendProxy(event: H3Event, target: string, opts?: ProxyOptions): Promise<any>;
307
319
  declare function getProxyRequestHeaders(event: H3Event): any;
308
- declare function fetchWithEvent(event: H3Event, req: RequestInfo | URL, init?: RequestInit, options?: {
320
+ declare function fetchWithEvent(event: H3Event, req: RequestInfo | URL, init?: RequestInit & {
321
+ context?: H3EventContext;
322
+ }, options?: {
309
323
  fetch: typeof fetch;
310
324
  }): Promise<Response>;
311
325
 
@@ -319,6 +333,9 @@ declare function getRequestHeaders(event: H3Event): RequestHeaders;
319
333
  declare const getHeaders: typeof getRequestHeaders;
320
334
  declare function getRequestHeader(event: H3Event, name: string): RequestHeaders[string];
321
335
  declare const getHeader: typeof getRequestHeader;
336
+ declare function getRequestHost(event: H3Event): string;
337
+ declare function getRequestProtocol(event: H3Event): "https" | "http";
338
+ declare function getRequestURL(event: H3Event): URL;
322
339
 
323
340
  declare function send(event: H3Event, data?: any, type?: string): Promise<void>;
324
341
  /**
@@ -348,6 +365,25 @@ declare function isStream(data: any): any;
348
365
  declare function sendStream(event: H3Event, data: any): Promise<void>;
349
366
  declare function writeEarlyHints(event: H3Event, hints: string | string[] | Record<string, string | string[]>, cb?: () => void): void;
350
367
 
368
+ interface H3CorsOptions {
369
+ origin?: "*" | "null" | (string | RegExp)[] | ((origin: string) => boolean);
370
+ methods?: "*" | HTTPMethod[];
371
+ allowHeaders?: "*" | string[];
372
+ exposeHeaders?: "*" | string[];
373
+ credentials?: boolean;
374
+ maxAge?: string | false;
375
+ preflight?: {
376
+ statusCode?: number;
377
+ };
378
+ }
379
+
380
+ declare function handleCors(event: H3Event, options: H3CorsOptions): boolean;
381
+
382
+ declare function isPreflightRequest(event: H3Event): boolean;
383
+ declare function isCorsOriginAllowed(origin: ReturnType<typeof getRequestHeaders>["origin"], options: H3CorsOptions): boolean;
384
+ declare function appendCorsPreflightHeaders(event: H3Event, options: H3CorsOptions): void;
385
+ declare function appendCorsHeaders(event: H3Event, options: H3CorsOptions): void;
386
+
351
387
  type RouterMethod = Lowercase<HTTPMethod>;
352
388
  type RouterUse = (path: string, handler: EventHandler, method?: RouterMethod | RouterMethod[]) => Router;
353
389
  type AddRouteShortcuts = Record<RouterMethod, RouterUse>;
@@ -363,4 +399,4 @@ interface CreateRouterOptions {
363
399
  }
364
400
  declare function createRouter(opts?: CreateRouterOptions): Router;
365
401
 
366
- export { AddRouteShortcuts, App, AppOptions, AppUse, CacheConditions, CreateRouterOptions, DynamicEventHandler, Encoding, EventHandler, EventHandlerResponse, H3Error, H3Event, H3EventContext, H3Headers, H3Response, HTTPMethod, InputLayer, InputStack, Layer, LazyEventHandler, MIMES, Matcher, NodeEventContext, NodeListener, NodeMiddleware, NodePromisifiedHandler, ProxyOptions, RequestHeaders, Router, RouterMethod, RouterUse, Session, SessionConfig, SessionData, Stack, appendHeader, appendHeaders, appendResponseHeader, appendResponseHeaders, assertMethod, callNodeListener, clearSession, createApp, createAppEventHandler, createError, createEvent, createRouter, defaultContentType, defineEventHandler, defineLazyEventHandler, defineNodeListener, defineNodeMiddleware, deleteCookie, dynamicEventHandler, eventHandler, fetchWithEvent, fromNodeMiddleware, getCookie, getHeader, getHeaders, getMethod, getProxyRequestHeaders, getQuery, getRequestHeader, getRequestHeaders, getResponseHeader, getResponseHeaders, getResponseStatus, getResponseStatusText, getRouterParam, getRouterParams, getSession, handleCacheHeaders, isError, isEvent, isEventHandler, isMethod, isStream, lazyEventHandler, parseCookies, promisifyNodeListener, proxyRequest, readBody, readMultipartFormData, readRawBody, sealSession, send, sendError, sendNoContent, sendProxy, sendRedirect, sendStream, setCookie, setHeader, setHeaders, setResponseHeader, setResponseHeaders, setResponseStatus, toEventHandler, toNodeListener, unsealSession, updateSession, use, useBase, useSession, writeEarlyHints };
402
+ export { AddRouteShortcuts, App, AppOptions, AppUse, CacheConditions, CreateRouterOptions, DynamicEventHandler, Encoding, EventHandler, EventHandlerResponse, H3CorsOptions, H3Error, H3Event, H3EventContext, H3Headers, H3Response, HTTPMethod, InputLayer, InputStack, Layer, LazyEventHandler, MIMES, Matcher, MultiPartData, NodeEventContext, NodeListener, NodeMiddleware, NodePromisifiedHandler, ProxyOptions, RequestHeaders, Router, RouterMethod, RouterUse, Session, SessionConfig, SessionData, Stack, appendCorsHeaders, appendCorsPreflightHeaders, appendHeader, appendHeaders, appendResponseHeader, appendResponseHeaders, assertMethod, callNodeListener, clearSession, createApp, createAppEventHandler, createError, createEvent, createRouter, defaultContentType, defineEventHandler, defineLazyEventHandler, defineNodeListener, defineNodeMiddleware, deleteCookie, dynamicEventHandler, eventHandler, fetchWithEvent, fromNodeMiddleware, getCookie, getHeader, getHeaders, getMethod, getProxyRequestHeaders, getQuery, getRequestHeader, getRequestHeaders, getRequestHost, getRequestProtocol, getRequestURL, getResponseHeader, getResponseHeaders, getResponseStatus, getResponseStatusText, getRouterParam, getRouterParams, getSession, handleCacheHeaders, handleCors, isCorsOriginAllowed, isError, isEvent, isEventHandler, isMethod, isPreflightRequest, isStream, lazyEventHandler, parseCookies, promisifyNodeListener, proxyRequest, readBody, readMultipartFormData, readRawBody, sealSession, send, sendError, sendNoContent, sendProxy, sendRedirect, sendStream, setCookie, setHeader, setHeaders, setResponseHeader, setResponseHeaders, setResponseStatus, splitCookiesString, toEventHandler, toNodeListener, unsealSession, updateSession, use, useBase, useSession, writeEarlyHints };
package/dist/index.mjs CHANGED
@@ -4,6 +4,7 @@ import destr from 'destr';
4
4
  import { parse as parse$1, serialize } from 'cookie-es';
5
5
  import crypto from 'uncrypto';
6
6
  import { seal, defaults, unseal } from 'iron-webcrypto';
7
+ import { defu } from 'defu';
7
8
 
8
9
  function useBase(base, handler) {
9
10
  base = withoutTrailingSlash(base);
@@ -245,6 +246,24 @@ function getRequestHeader(event, name) {
245
246
  return value;
246
247
  }
247
248
  const getHeader = getRequestHeader;
249
+ function getRequestHost(event) {
250
+ const xForwardedHost = event.node.req.headers["x-forwarded-host"];
251
+ if (xForwardedHost) {
252
+ return xForwardedHost;
253
+ }
254
+ return event.node.req.headers.host || "localhost";
255
+ }
256
+ function getRequestProtocol(event) {
257
+ if (event.node.req.headers["x-forwarded-proto"] === "https") {
258
+ return "https";
259
+ }
260
+ return event.node.req.connection.encrypted ? "https" : "http";
261
+ }
262
+ function getRequestURL(event) {
263
+ const host = getRequestHost(event);
264
+ const protocol = getRequestProtocol(event);
265
+ return new URL(event.path || "/", `${protocol}://${host}`);
266
+ }
248
267
 
249
268
  const RawBodySymbol = Symbol.for("h3RawBody");
250
269
  const ParsedBodySymbol = Symbol.for("h3ParsedBody");
@@ -377,6 +396,58 @@ function deleteCookie(event, name, serializeOptions) {
377
396
  maxAge: 0
378
397
  });
379
398
  }
399
+ function splitCookiesString(cookiesString) {
400
+ if (typeof cookiesString !== "string") {
401
+ return [];
402
+ }
403
+ const cookiesStrings = [];
404
+ let pos = 0;
405
+ let start;
406
+ let ch;
407
+ let lastComma;
408
+ let nextStart;
409
+ let cookiesSeparatorFound;
410
+ function skipWhitespace() {
411
+ while (pos < cookiesString.length && /\s/.test(cookiesString.charAt(pos))) {
412
+ pos += 1;
413
+ }
414
+ return pos < cookiesString.length;
415
+ }
416
+ function notSpecialChar() {
417
+ ch = cookiesString.charAt(pos);
418
+ return ch !== "=" && ch !== ";" && ch !== ",";
419
+ }
420
+ while (pos < cookiesString.length) {
421
+ start = pos;
422
+ cookiesSeparatorFound = false;
423
+ while (skipWhitespace()) {
424
+ ch = cookiesString.charAt(pos);
425
+ if (ch === ",") {
426
+ lastComma = pos;
427
+ pos += 1;
428
+ skipWhitespace();
429
+ nextStart = pos;
430
+ while (pos < cookiesString.length && notSpecialChar()) {
431
+ pos += 1;
432
+ }
433
+ if (pos < cookiesString.length && cookiesString.charAt(pos) === "=") {
434
+ cookiesSeparatorFound = true;
435
+ pos = nextStart;
436
+ cookiesStrings.push(cookiesString.slice(start, lastComma));
437
+ start = pos;
438
+ } else {
439
+ pos = lastComma + 1;
440
+ }
441
+ } else {
442
+ pos += 1;
443
+ }
444
+ }
445
+ if (!cookiesSeparatorFound || pos >= cookiesString.length) {
446
+ cookiesStrings.push(cookiesString.slice(start, cookiesString.length));
447
+ }
448
+ }
449
+ return cookiesStrings;
450
+ }
380
451
 
381
452
  const PayloadMethods = /* @__PURE__ */ new Set(["PATCH", "POST", "PUT", "DELETE"]);
382
453
  const ignoredHeaders = /* @__PURE__ */ new Set([
@@ -424,6 +495,27 @@ async function sendProxy(event, target, opts = {}) {
424
495
  if (key === "content-length") {
425
496
  continue;
426
497
  }
498
+ if (key === "set-cookie") {
499
+ const cookies = splitCookiesString(value).map((cookie) => {
500
+ if (opts.cookieDomainRewrite) {
501
+ cookie = rewriteCookieProperty(
502
+ cookie,
503
+ opts.cookieDomainRewrite,
504
+ "domain"
505
+ );
506
+ }
507
+ if (opts.cookiePathRewrite) {
508
+ cookie = rewriteCookieProperty(
509
+ cookie,
510
+ opts.cookiePathRewrite,
511
+ "path"
512
+ );
513
+ }
514
+ return cookie;
515
+ });
516
+ event.node.res.setHeader("set-cookie", cookies);
517
+ continue;
518
+ }
427
519
  event.node.res.setHeader(key, value);
428
520
  }
429
521
  if (response._data !== void 0) {
@@ -451,8 +543,7 @@ function getProxyRequestHeaders(event) {
451
543
  function fetchWithEvent(event, req, init, options) {
452
544
  return _getFetch(options?.fetch)(req, {
453
545
  ...init,
454
- // @ts-ignore (context is used for unenv and local fetch)
455
- context: init.context || event.context,
546
+ context: init?.context || event.context,
456
547
  headers: {
457
548
  ...getProxyRequestHeaders(event),
458
549
  ...init?.headers
@@ -470,6 +561,23 @@ function _getFetch(_fetch) {
470
561
  "fetch is not available. Try importing `node-fetch-native/polyfill` for Node.js."
471
562
  );
472
563
  }
564
+ function rewriteCookieProperty(header, map, property) {
565
+ const _map = typeof map === "string" ? { "*": map } : map;
566
+ return header.replace(
567
+ new RegExp(`(;\\s*${property}=)([^;]+)`, "gi"),
568
+ (match, prefix, previousValue) => {
569
+ let newValue;
570
+ if (previousValue in _map) {
571
+ newValue = _map[previousValue];
572
+ } else if ("*" in _map) {
573
+ newValue = _map["*"];
574
+ } else {
575
+ return match;
576
+ }
577
+ return newValue ? prefix + newValue : "";
578
+ }
579
+ );
580
+ }
473
581
 
474
582
  const defer = typeof setImmediate !== "undefined" ? setImmediate : (fn) => fn();
475
583
  function send(event, data, type) {
@@ -728,6 +836,120 @@ async function clearSession(event, config) {
728
836
  });
729
837
  }
730
838
 
839
+ function resolveCorsOptions(options = {}) {
840
+ const defaultOptions = {
841
+ origin: "*",
842
+ methods: "*",
843
+ allowHeaders: "*",
844
+ exposeHeaders: "*",
845
+ credentials: false,
846
+ maxAge: false,
847
+ preflight: {
848
+ statusCode: 204
849
+ }
850
+ };
851
+ return defu(options, defaultOptions);
852
+ }
853
+ function isPreflightRequest(event) {
854
+ const method = getMethod(event);
855
+ const origin = getRequestHeader(event, "origin");
856
+ const accessControlRequestMethod = getRequestHeader(
857
+ event,
858
+ "access-control-request-method"
859
+ );
860
+ return method === "OPTIONS" && !!origin && !!accessControlRequestMethod;
861
+ }
862
+ function isCorsOriginAllowed(origin, options) {
863
+ const { origin: originOption } = options;
864
+ if (!origin || !originOption || originOption === "*" || originOption === "null") {
865
+ return true;
866
+ }
867
+ if (Array.isArray(originOption)) {
868
+ return originOption.some((_origin) => {
869
+ if (_origin instanceof RegExp) {
870
+ return _origin.test(origin);
871
+ }
872
+ return origin === _origin;
873
+ });
874
+ }
875
+ return originOption(origin);
876
+ }
877
+ function createOriginHeaders(event, options) {
878
+ const { origin: originOption } = options;
879
+ const origin = getRequestHeader(event, "origin");
880
+ if (!origin || !originOption || originOption === "*") {
881
+ return { "access-control-allow-origin": "*" };
882
+ }
883
+ if (typeof originOption === "string") {
884
+ return { "access-control-allow-origin": originOption, vary: "origin" };
885
+ }
886
+ return isCorsOriginAllowed(origin, options) ? { "access-control-allow-origin": origin, vary: "origin" } : {};
887
+ }
888
+ function createMethodsHeaders(options) {
889
+ const { methods } = options;
890
+ if (!methods) {
891
+ return {};
892
+ }
893
+ if (methods === "*") {
894
+ return { "access-control-allow-methods": "*" };
895
+ }
896
+ return methods.length > 0 ? { "access-control-allow-methods": methods.join(",") } : {};
897
+ }
898
+ function createCredentialsHeaders(options) {
899
+ const { credentials } = options;
900
+ if (credentials) {
901
+ return { "access-control-allow-credentials": "true" };
902
+ }
903
+ return {};
904
+ }
905
+ function createAllowHeaderHeaders(event, options) {
906
+ const { allowHeaders } = options;
907
+ if (!allowHeaders || allowHeaders === "*" || allowHeaders.length === 0) {
908
+ const header = getRequestHeader(event, "access-control-request-headers");
909
+ return header ? {
910
+ "access-control-allow-headers": header,
911
+ vary: "access-control-request-headers"
912
+ } : {};
913
+ }
914
+ return {
915
+ "access-control-allow-headers": allowHeaders.join(","),
916
+ vary: "access-control-request-headers"
917
+ };
918
+ }
919
+ function createExposeHeaders(options) {
920
+ const { exposeHeaders } = options;
921
+ if (!exposeHeaders) {
922
+ return {};
923
+ }
924
+ if (exposeHeaders === "*") {
925
+ return { "access-control-expose-headers": exposeHeaders };
926
+ }
927
+ return { "access-control-expose-headers": exposeHeaders.join(",") };
928
+ }
929
+ function appendCorsPreflightHeaders(event, options) {
930
+ appendHeaders(event, createOriginHeaders(event, options));
931
+ appendHeaders(event, createCredentialsHeaders(options));
932
+ appendHeaders(event, createExposeHeaders(options));
933
+ appendHeaders(event, createMethodsHeaders(options));
934
+ appendHeaders(event, createAllowHeaderHeaders(event, options));
935
+ }
936
+ function appendCorsHeaders(event, options) {
937
+ appendHeaders(event, createOriginHeaders(event, options));
938
+ appendHeaders(event, createCredentialsHeaders(options));
939
+ appendHeaders(event, createExposeHeaders(options));
940
+ }
941
+
942
+ function handleCors(event, options) {
943
+ const _options = resolveCorsOptions(options);
944
+ if (isPreflightRequest(event)) {
945
+ appendCorsPreflightHeaders(event, options);
946
+ sendNoContent(event, _options.preflight.statusCode);
947
+ return true;
948
+ }
949
+ appendCorsHeaders(event, options);
950
+ return false;
951
+ }
952
+
731
953
  class H3Headers {
732
954
  constructor(init) {
733
955
  if (!init) {
@@ -1177,4 +1399,4 @@ function createRouter(opts = {}) {
1177
1399
  return router;
1178
1400
  }
1179
1401
 
1180
- export { H3Error, H3Event, H3Headers, H3Response, MIMES, appendHeader, appendHeaders, appendResponseHeader, appendResponseHeaders, assertMethod, callNodeListener, clearSession, createApp, createAppEventHandler, createError, createEvent, createRouter, defaultContentType, defineEventHandler, defineLazyEventHandler, defineNodeListener, defineNodeMiddleware, deleteCookie, dynamicEventHandler, eventHandler, fetchWithEvent, fromNodeMiddleware, getCookie, getHeader, getHeaders, getMethod, getProxyRequestHeaders, getQuery, getRequestHeader, getRequestHeaders, getResponseHeader, getResponseHeaders, getResponseStatus, getResponseStatusText, getRouterParam, getRouterParams, getSession, handleCacheHeaders, isError, isEvent, isEventHandler, isMethod, isStream, lazyEventHandler, parseCookies, promisifyNodeListener, proxyRequest, readBody, readMultipartFormData, readRawBody, sealSession, send, sendError, sendNoContent, sendProxy, sendRedirect, sendStream, setCookie, setHeader, setHeaders, setResponseHeader, setResponseHeaders, setResponseStatus, toEventHandler, toNodeListener, unsealSession, updateSession, use, useBase, useSession, writeEarlyHints };
1402
+ export { H3Error, H3Event, H3Headers, H3Response, MIMES, appendCorsHeaders, appendCorsPreflightHeaders, appendHeader, appendHeaders, appendResponseHeader, appendResponseHeaders, assertMethod, callNodeListener, clearSession, createApp, createAppEventHandler, createError, createEvent, createRouter, defaultContentType, defineEventHandler, defineLazyEventHandler, defineNodeListener, defineNodeMiddleware, deleteCookie, dynamicEventHandler, eventHandler, fetchWithEvent, fromNodeMiddleware, getCookie, getHeader, getHeaders, getMethod, getProxyRequestHeaders, getQuery, getRequestHeader, getRequestHeaders, getRequestHost, getRequestProtocol, getRequestURL, getResponseHeader, getResponseHeaders, getResponseStatus, getResponseStatusText, getRouterParam, getRouterParams, getSession, handleCacheHeaders, handleCors, isCorsOriginAllowed, isError, isEvent, isEventHandler, isMethod, isPreflightRequest, isStream, lazyEventHandler, parseCookies, promisifyNodeListener, proxyRequest, readBody, readMultipartFormData, readRawBody, sealSession, send, sendError, sendNoContent, sendProxy, sendRedirect, sendStream, setCookie, setHeader, setHeaders, setResponseHeader, setResponseHeaders, setResponseStatus, splitCookiesString, toEventHandler, toNodeListener, unsealSession, updateSession, use, useBase, useSession, writeEarlyHints };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "h3",
3
- "version": "1.4.0",
3
+ "version": "1.6.0",
4
4
  "description": "Tiny JavaScript Server",
5
5
  "repository": "unjs/h3",
6
6
  "license": "MIT",
@@ -21,39 +21,41 @@
21
21
  ],
22
22
  "dependencies": {
23
23
  "cookie-es": "^0.5.0",
24
+ "defu": "^6.1.2",
24
25
  "destr": "^1.2.2",
25
- "iron-webcrypto": "^0.4.0",
26
+ "iron-webcrypto": "^0.5.0",
26
27
  "radix3": "^1.0.0",
27
- "ufo": "^1.0.1",
28
+ "ufo": "^1.1.1",
28
29
  "uncrypto": "^0.1.2"
29
30
  },
30
31
  "devDependencies": {
31
- "0x": "^5.4.1",
32
+ "0x": "^5.5.0",
32
33
  "@types/express": "^4.17.17",
33
- "@types/node": "^18.13.0",
34
+ "@types/node": "^18.15.0",
34
35
  "@types/supertest": "^2.0.12",
35
- "@vitest/coverage-c8": "^0.28.4",
36
+ "@vitest/coverage-c8": "^0.29.2",
36
37
  "autocannon": "^7.10.0",
37
- "changelogen": "^0.4.1",
38
+ "changelogen": "^0.5.1",
38
39
  "connect": "^3.7.0",
39
- "eslint": "^8.33.0",
40
+ "eslint": "^8.35.0",
40
41
  "eslint-config-unjs": "^0.1.0",
41
42
  "express": "^4.18.2",
42
43
  "get-port": "^6.1.2",
43
- "jiti": "^1.16.2",
44
- "listhen": "^1.0.2",
45
- "node-fetch-native": "^1.0.1",
44
+ "jiti": "^1.17.2",
45
+ "listhen": "^1.0.3",
46
+ "node-fetch-native": "^1.0.2",
46
47
  "prettier": "^2.8.4",
47
48
  "supertest": "^6.3.3",
48
49
  "typescript": "^4.9.5",
49
- "unbuild": "^1.1.1",
50
- "vitest": "^0.28.4"
50
+ "unbuild": "^1.1.2",
51
+ "vitest": "^0.29.2"
51
52
  },
52
- "packageManager": "pnpm@7.26.3",
53
+ "packageManager": "pnpm@7.29.0",
53
54
  "scripts": {
54
55
  "build": "unbuild",
55
56
  "dev": "vitest",
56
- "lint": "eslint --ext ts,mjs,cjs . && prettier -c src test playground",
57
+ "lint": "eslint --cache --ext .ts,.js,.mjs,.cjs . && prettier -c src test playground",
58
+ "lint:fix": "eslint --cache --ext .ts,.js,.mjs,.cjs . --fix && prettier -c src test playground -w",
57
59
  "play": "jiti ./playground/index.ts",
58
60
  "profile": "0x -o -D .profile -P 'autocannon -c 100 -p 10 -d 40 http://localhost:$PORT' ./playground/server.cjs",
59
61
  "release": "pnpm test && pnpm build && changelogen --release && pnpm publish && git push --follow-tags",