h3 1.3.0 → 1.5.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,7 +1,7 @@
1
1
  [![npm downloads](https://img.shields.io/npm/dm/h3.svg?style=flat-square)](https://npmjs.com/package/h3)
2
2
  [![version](https://img.shields.io/npm/v/h3/latest.svg?style=flat-square)](https://npmjs.com/package/h3)
3
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)
4
+ [![build status](https://img.shields.io/github/actions/workflow/status/unjs/h3/ci.yml?branch=main&style=flat-square)](https://github.com/unjs/h3/actions)
5
5
  [![coverage](https://img.shields.io/codecov/c/gh/unjs/h3/main?style=flat-square)](https://codecov.io/gh/unjs/h3)
6
6
  [![jsDocs.io](https://img.shields.io/badge/jsDocs.io-reference-blue?style=flat-square)](https://www.jsdocs.io/package/h3)
7
7
 
@@ -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)`
@@ -155,10 +168,17 @@ H3 has a concept of composable utilities that accept `event` (from `eventHandler
155
168
  - `getResponseStatus(event)`
156
169
  - `getResponseStatusText(event)`
157
170
  - `readMultipartFormData(event)`
158
- - `useSession(event, { password, name?, cookie?, seal?, crypto? })`
159
- - `getSession(event, { password, name?, cookie?, seal?, crypto? })`
160
- - `updateSession(event, { password, name?, cookie?, seal?, crypto? }), update)`
161
- - `clearSession(event, { password, name?, cookie?, seal?, crypto? }))`
171
+ - `useSession(event, config = { password, maxAge?, name?, cookie?, seal?, crypto? })`
172
+ - `getSession(event, config)`
173
+ - `updateSession(event, config, update)`
174
+ - `clearSession(event, config)`
175
+ - `sealSession(event, config)`
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)
162
182
 
163
183
  👉 You can learn more about usage in [JSDocs Documentation](https://www.jsdocs.io/package/h3#package-functions).
164
184
 
@@ -170,9 +190,6 @@ Please check their READMEs for more details.
170
190
 
171
191
  PRs are welcome to add your packages.
172
192
 
173
- - [h3-cors](https://github.com/NozomuIkuta/h3-cors)
174
- - `defineCorsEventHandler(options)`
175
- - `isPreflight(event)`
176
193
  - [h3-typebox](https://github.com/kevinmarrec/h3-typebox)
177
194
  - `validateBody(event, schema)`
178
195
  - `validateQuery(event, schema)`
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);
@@ -380,6 +381,59 @@ function deleteCookie(event, name, serializeOptions) {
380
381
  });
381
382
  }
382
383
 
384
+ function splitCookiesString(cookiesString) {
385
+ if (typeof cookiesString !== "string") {
386
+ return [];
387
+ }
388
+ const cookiesStrings = [];
389
+ let pos = 0;
390
+ let start;
391
+ let ch;
392
+ let lastComma;
393
+ let nextStart;
394
+ let cookiesSeparatorFound;
395
+ function skipWhitespace() {
396
+ while (pos < cookiesString.length && /\s/.test(cookiesString.charAt(pos))) {
397
+ pos += 1;
398
+ }
399
+ return pos < cookiesString.length;
400
+ }
401
+ function notSpecialChar() {
402
+ ch = cookiesString.charAt(pos);
403
+ return ch !== "=" && ch !== ";" && ch !== ",";
404
+ }
405
+ while (pos < cookiesString.length) {
406
+ start = pos;
407
+ cookiesSeparatorFound = false;
408
+ while (skipWhitespace()) {
409
+ ch = cookiesString.charAt(pos);
410
+ if (ch === ",") {
411
+ lastComma = pos;
412
+ pos += 1;
413
+ skipWhitespace();
414
+ nextStart = pos;
415
+ while (pos < cookiesString.length && notSpecialChar()) {
416
+ pos += 1;
417
+ }
418
+ if (pos < cookiesString.length && cookiesString.charAt(pos) === "=") {
419
+ cookiesSeparatorFound = true;
420
+ pos = nextStart;
421
+ cookiesStrings.push(cookiesString.slice(start, lastComma));
422
+ start = pos;
423
+ } else {
424
+ pos = lastComma + 1;
425
+ }
426
+ } else {
427
+ pos += 1;
428
+ }
429
+ }
430
+ if (!cookiesSeparatorFound || pos >= cookiesString.length) {
431
+ cookiesStrings.push(cookiesString.slice(start, cookiesString.length));
432
+ }
433
+ }
434
+ return cookiesStrings;
435
+ }
436
+
383
437
  const PayloadMethods = /* @__PURE__ */ new Set(["PATCH", "POST", "PUT", "DELETE"]);
384
438
  const ignoredHeaders = /* @__PURE__ */ new Set([
385
439
  "transfer-encoding",
@@ -426,6 +480,27 @@ async function sendProxy(event, target, opts = {}) {
426
480
  if (key === "content-length") {
427
481
  continue;
428
482
  }
483
+ if (key === "set-cookie") {
484
+ const cookies = splitCookiesString(value).map((cookie) => {
485
+ if (opts.cookieDomainRewrite) {
486
+ cookie = rewriteCookieProperty(
487
+ cookie,
488
+ opts.cookieDomainRewrite,
489
+ "domain"
490
+ );
491
+ }
492
+ if (opts.cookiePathRewrite) {
493
+ cookie = rewriteCookieProperty(
494
+ cookie,
495
+ opts.cookiePathRewrite,
496
+ "path"
497
+ );
498
+ }
499
+ return cookie;
500
+ });
501
+ event.node.res.setHeader("set-cookie", cookies);
502
+ continue;
503
+ }
429
504
  event.node.res.setHeader(key, value);
430
505
  }
431
506
  if (response._data !== void 0) {
@@ -472,6 +547,23 @@ function _getFetch(_fetch) {
472
547
  "fetch is not available. Try importing `node-fetch-native/polyfill` for Node.js."
473
548
  );
474
549
  }
550
+ function rewriteCookieProperty(header, map, property) {
551
+ const _map = typeof map === "string" ? { "*": map } : map;
552
+ return header.replace(
553
+ new RegExp(`(;\\s*${property}=)([^;]+)`, "gi"),
554
+ (match, prefix, previousValue) => {
555
+ let newValue;
556
+ if (previousValue in _map) {
557
+ newValue = _map[previousValue];
558
+ } else if ("*" in _map) {
559
+ newValue = _map["*"];
560
+ } else {
561
+ return match;
562
+ }
563
+ return newValue ? prefix + newValue : "";
564
+ }
565
+ );
566
+ }
475
567
 
476
568
  const defer = typeof setImmediate !== "undefined" ? setImmediate : (fn) => fn();
477
569
  function send(event, data, type) {
@@ -640,21 +732,35 @@ async function getSession(event, config) {
640
732
  if (event.context.sessions[sessionName]) {
641
733
  return event.context.sessions[sessionName];
642
734
  }
643
- const session = { id: "", data: /* @__PURE__ */ Object.create(null) };
735
+ const session = {
736
+ id: "",
737
+ createdAt: 0,
738
+ data: /* @__PURE__ */ Object.create(null)
739
+ };
644
740
  event.context.sessions[sessionName] = session;
645
- const reqCookie = getCookie(event, sessionName);
646
- if (!reqCookie) {
647
- session.id = (config.crypto || crypto).randomUUID();
648
- await updateSession(event, config);
649
- } else {
650
- const unsealed = await ironWebcrypto.unseal(
651
- config.crypto || crypto,
652
- reqCookie,
653
- config.password,
654
- config.seal || ironWebcrypto.defaults
741
+ let sealedSession;
742
+ if (config.sessionHeader !== false) {
743
+ const headerName = typeof config.sessionHeader === "string" ? config.sessionHeader.toLowerCase() : `x-${sessionName.toLowerCase()}-session`;
744
+ const headerValue = event.node.req.headers[headerName];
745
+ if (typeof headerValue === "string") {
746
+ sealedSession = headerValue;
747
+ }
748
+ }
749
+ if (!sealedSession) {
750
+ sealedSession = getCookie(event, sessionName);
751
+ }
752
+ if (sealedSession) {
753
+ const unsealed = await unsealSession(event, config, sealedSession).catch(
754
+ () => {
755
+ }
655
756
  );
656
757
  Object.assign(session, unsealed);
657
758
  }
759
+ if (!session.id) {
760
+ session.id = (config.crypto || crypto).randomUUID();
761
+ session.createdAt = Date.now();
762
+ await updateSession(event, config);
763
+ }
658
764
  return session;
659
765
  }
660
766
  async function updateSession(event, config, update) {
@@ -666,21 +772,168 @@ async function updateSession(event, config, update) {
666
772
  if (update) {
667
773
  Object.assign(session.data, update);
668
774
  }
669
- const sealed = await ironWebcrypto.seal(
775
+ if (config.cookie !== false) {
776
+ const sealed = await sealSession(event, config);
777
+ setCookie(event, sessionName, sealed, {
778
+ ...DEFAULT_COOKIE,
779
+ expires: config.maxAge ? new Date(session.createdAt + config.maxAge * 1e3) : void 0,
780
+ ...config.cookie
781
+ });
782
+ }
783
+ return session;
784
+ }
785
+ async function sealSession(event, config) {
786
+ const sessionName = config.name || DEFAULT_NAME;
787
+ const session = event.context.sessions?.[sessionName] || await getSession(event, config);
788
+ const sealed = await ironWebcrypto.seal(config.crypto || crypto, session, config.password, {
789
+ ...ironWebcrypto.defaults,
790
+ ttl: config.maxAge ? config.maxAge * 1e3 : 0,
791
+ ...config.seal
792
+ });
793
+ return sealed;
794
+ }
795
+ async function unsealSession(_event, config, sealed) {
796
+ const unsealed = await ironWebcrypto.unseal(
670
797
  config.crypto || crypto,
671
- session,
798
+ sealed,
672
799
  config.password,
673
- config.seal || ironWebcrypto.defaults
800
+ {
801
+ ...ironWebcrypto.defaults,
802
+ ttl: config.maxAge ? config.maxAge * 1e3 : 0,
803
+ ...config.seal
804
+ }
674
805
  );
675
- setCookie(event, sessionName, sealed, config.cookie || DEFAULT_COOKIE);
676
- return session;
806
+ if (config.maxAge) {
807
+ const age = Date.now() - (unsealed.createdAt || Number.NEGATIVE_INFINITY);
808
+ if (age > config.maxAge * 1e3) {
809
+ throw new Error("Session expired!");
810
+ }
811
+ }
812
+ return unsealed;
677
813
  }
678
814
  async function clearSession(event, config) {
679
815
  const sessionName = config.name || DEFAULT_NAME;
680
816
  if (event.context.sessions?.[sessionName]) {
681
817
  delete event.context.sessions[sessionName];
682
818
  }
683
- await setCookie(event, sessionName, "", config.cookie || DEFAULT_COOKIE);
819
+ await setCookie(event, sessionName, "", {
820
+ ...DEFAULT_COOKIE,
821
+ ...config.cookie
822
+ });
823
+ }
824
+
825
+ function resolveCorsOptions(options = {}) {
826
+ const defaultOptions = {
827
+ origin: "*",
828
+ methods: "*",
829
+ allowHeaders: "*",
830
+ exposeHeaders: "*",
831
+ credentials: false,
832
+ maxAge: false,
833
+ preflight: {
834
+ statusCode: 204
835
+ }
836
+ };
837
+ return defu.defu(options, defaultOptions);
838
+ }
839
+ function isPreflightRequest(event) {
840
+ const method = getMethod(event);
841
+ const origin = getRequestHeader(event, "origin");
842
+ const accessControlRequestMethod = getRequestHeader(
843
+ event,
844
+ "access-control-request-method"
845
+ );
846
+ return method === "OPTIONS" && !!origin && !!accessControlRequestMethod;
847
+ }
848
+ function isCorsOriginAllowed(origin, options) {
849
+ const { origin: originOption } = options;
850
+ if (!origin || !originOption || originOption === "*" || originOption === "null") {
851
+ return true;
852
+ }
853
+ if (Array.isArray(originOption)) {
854
+ return originOption.some((_origin) => {
855
+ if (_origin instanceof RegExp) {
856
+ return _origin.test(origin);
857
+ }
858
+ return origin === _origin;
859
+ });
860
+ }
861
+ return originOption(origin);
862
+ }
863
+ function createOriginHeaders(event, options) {
864
+ const { origin: originOption } = options;
865
+ const origin = getRequestHeader(event, "origin");
866
+ if (!origin || !originOption || originOption === "*") {
867
+ return { "access-control-allow-origin": "*" };
868
+ }
869
+ if (typeof originOption === "string") {
870
+ return { "access-control-allow-origin": originOption, vary: "origin" };
871
+ }
872
+ return isCorsOriginAllowed(origin, options) ? { "access-control-allow-origin": origin, vary: "origin" } : {};
873
+ }
874
+ function createMethodsHeaders(options) {
875
+ const { methods } = options;
876
+ if (!methods) {
877
+ return {};
878
+ }
879
+ if (methods === "*") {
880
+ return { "access-control-allow-methods": "*" };
881
+ }
882
+ return methods.length > 0 ? { "access-control-allow-methods": methods.join(",") } : {};
883
+ }
884
+ function createCredentialsHeaders(options) {
885
+ const { credentials } = options;
886
+ if (credentials) {
887
+ return { "access-control-allow-credentials": "true" };
888
+ }
889
+ return {};
890
+ }
891
+ function createAllowHeaderHeaders(event, options) {
892
+ const { allowHeaders } = options;
893
+ if (!allowHeaders || allowHeaders === "*" || allowHeaders.length === 0) {
894
+ const header = getRequestHeader(event, "access-control-request-headers");
895
+ return header ? {
896
+ "access-control-allow-headers": header,
897
+ vary: "access-control-request-headers"
898
+ } : {};
899
+ }
900
+ return {
901
+ "access-control-allow-headers": allowHeaders.join(","),
902
+ vary: "access-control-request-headers"
903
+ };
904
+ }
905
+ function createExposeHeaders(options) {
906
+ const { exposeHeaders } = options;
907
+ if (!exposeHeaders) {
908
+ return {};
909
+ }
910
+ if (exposeHeaders === "*") {
911
+ return { "access-control-expose-headers": exposeHeaders };
912
+ }
913
+ return { "access-control-expose-headers": exposeHeaders.join(",") };
914
+ }
915
+ function appendCorsPreflightHeaders(event, options) {
916
+ appendHeaders(event, createOriginHeaders(event, options));
917
+ appendHeaders(event, createCredentialsHeaders(options));
918
+ appendHeaders(event, createExposeHeaders(options));
919
+ appendHeaders(event, createMethodsHeaders(options));
920
+ appendHeaders(event, createAllowHeaderHeaders(event, options));
921
+ }
922
+ function appendCorsHeaders(event, options) {
923
+ appendHeaders(event, createOriginHeaders(event, options));
924
+ appendHeaders(event, createCredentialsHeaders(options));
925
+ appendHeaders(event, createExposeHeaders(options));
926
+ }
927
+
928
+ function handleCors(event, options) {
929
+ const _options = resolveCorsOptions(options);
930
+ if (isPreflightRequest(event)) {
931
+ appendCorsPreflightHeaders(event, options);
932
+ sendNoContent(event, _options.preflight.statusCode);
933
+ return true;
934
+ }
935
+ appendCorsHeaders(event, options);
936
+ return false;
684
937
  }
685
938
 
686
939
  class H3Headers {
@@ -1137,6 +1390,8 @@ exports.H3Event = H3Event;
1137
1390
  exports.H3Headers = H3Headers;
1138
1391
  exports.H3Response = H3Response;
1139
1392
  exports.MIMES = MIMES;
1393
+ exports.appendCorsHeaders = appendCorsHeaders;
1394
+ exports.appendCorsPreflightHeaders = appendCorsPreflightHeaders;
1140
1395
  exports.appendHeader = appendHeader;
1141
1396
  exports.appendHeaders = appendHeaders;
1142
1397
  exports.appendResponseHeader = appendResponseHeader;
@@ -1175,10 +1430,13 @@ exports.getRouterParam = getRouterParam;
1175
1430
  exports.getRouterParams = getRouterParams;
1176
1431
  exports.getSession = getSession;
1177
1432
  exports.handleCacheHeaders = handleCacheHeaders;
1433
+ exports.handleCors = handleCors;
1434
+ exports.isCorsOriginAllowed = isCorsOriginAllowed;
1178
1435
  exports.isError = isError;
1179
1436
  exports.isEvent = isEvent;
1180
1437
  exports.isEventHandler = isEventHandler;
1181
1438
  exports.isMethod = isMethod;
1439
+ exports.isPreflightRequest = isPreflightRequest;
1182
1440
  exports.isStream = isStream;
1183
1441
  exports.lazyEventHandler = lazyEventHandler;
1184
1442
  exports.parseCookies = parseCookies;
@@ -1187,6 +1445,7 @@ exports.proxyRequest = proxyRequest;
1187
1445
  exports.readBody = readBody;
1188
1446
  exports.readMultipartFormData = readMultipartFormData;
1189
1447
  exports.readRawBody = readRawBody;
1448
+ exports.sealSession = sealSession;
1190
1449
  exports.send = send;
1191
1450
  exports.sendError = sendError;
1192
1451
  exports.sendNoContent = sendNoContent;
@@ -1201,6 +1460,7 @@ exports.setResponseHeaders = setResponseHeaders;
1201
1460
  exports.setResponseStatus = setResponseStatus;
1202
1461
  exports.toEventHandler = toEventHandler;
1203
1462
  exports.toNodeListener = toNodeListener;
1463
+ exports.unsealSession = unsealSession;
1204
1464
  exports.updateSession = updateSession;
1205
1465
  exports.use = use;
1206
1466
  exports.useBase = useBase;
package/dist/index.d.ts CHANGED
@@ -8,12 +8,20 @@ type SessionDataT = Record<string, any>;
8
8
  type SessionData<T extends SessionDataT = SessionDataT> = T;
9
9
  interface Session<T extends SessionDataT = SessionDataT> {
10
10
  id: string;
11
+ createdAt: number;
11
12
  data: SessionData<T>;
12
13
  }
13
14
  interface SessionConfig {
15
+ /** Private key used to encrypt session tokens */
14
16
  password: string;
17
+ /** Session expiration time in seconds */
18
+ maxAge?: number;
19
+ /** default is h3 */
15
20
  name?: string;
16
- cookie?: CookieSerializeOptions;
21
+ /** Default is secure, httpOnly, / */
22
+ cookie?: false | CookieSerializeOptions;
23
+ /** Default is x-h3-session / x-{name}-session */
24
+ sessionHeader?: false | string;
17
25
  seal?: SealOptions;
18
26
  crypto?: Crypto;
19
27
  }
@@ -26,6 +34,8 @@ declare function useSession<T extends SessionDataT = SessionDataT>(event: H3Even
26
34
  declare function getSession<T extends SessionDataT = SessionDataT>(event: H3Event, config: SessionConfig): Promise<Session<T>>;
27
35
  type SessionUpdate<T extends SessionDataT = SessionDataT> = Partial<SessionData<T>> | ((oldData: SessionData<T>) => Partial<SessionData<T>> | undefined);
28
36
  declare function updateSession<T extends SessionDataT = SessionDataT>(event: H3Event, config: SessionConfig, update?: SessionUpdate<T>): Promise<Session<T>>;
37
+ declare function sealSession<T extends SessionDataT = SessionDataT>(event: H3Event, config: SessionConfig): Promise<string>;
38
+ declare function unsealSession(_event: H3Event, config: SessionConfig, sealed: string): Promise<Partial<Session<SessionDataT>>>;
29
39
  declare function clearSession(event: H3Event, config: SessionConfig): Promise<void>;
30
40
 
31
41
  type HTTPMethod = "GET" | "HEAD" | "PATCH" | "POST" | "PUT" | "DELETE" | "CONNECT" | "OPTIONS" | "TRACE";
@@ -291,6 +301,8 @@ interface ProxyOptions {
291
301
  fetchOptions?: RequestInit;
292
302
  fetch?: typeof fetch;
293
303
  sendStream?: boolean;
304
+ cookieDomainRewrite?: string | Record<string, string>;
305
+ cookiePathRewrite?: string | Record<string, string>;
294
306
  }
295
307
  declare function proxyRequest(event: H3Event, target: string, opts?: ProxyOptions): Promise<any>;
296
308
  declare function sendProxy(event: H3Event, target: string, opts?: ProxyOptions): Promise<any>;
@@ -338,6 +350,25 @@ declare function isStream(data: any): any;
338
350
  declare function sendStream(event: H3Event, data: any): Promise<void>;
339
351
  declare function writeEarlyHints(event: H3Event, hints: string | string[] | Record<string, string | string[]>, cb?: () => void): void;
340
352
 
353
+ interface H3CorsOptions {
354
+ origin?: "*" | "null" | (string | RegExp)[] | ((origin: string) => boolean);
355
+ methods?: "*" | HTTPMethod[];
356
+ allowHeaders?: "*" | string[];
357
+ exposeHeaders?: "*" | string[];
358
+ credentials?: boolean;
359
+ maxAge?: string | false;
360
+ preflight?: {
361
+ statusCode?: number;
362
+ };
363
+ }
364
+
365
+ declare function handleCors(event: H3Event, options: H3CorsOptions): boolean;
366
+
367
+ declare function isPreflightRequest(event: H3Event): boolean;
368
+ declare function isCorsOriginAllowed(origin: ReturnType<typeof getRequestHeaders>["origin"], options: H3CorsOptions): boolean;
369
+ declare function appendCorsPreflightHeaders(event: H3Event, options: H3CorsOptions): void;
370
+ declare function appendCorsHeaders(event: H3Event, options: H3CorsOptions): void;
371
+
341
372
  type RouterMethod = Lowercase<HTTPMethod>;
342
373
  type RouterUse = (path: string, handler: EventHandler, method?: RouterMethod | RouterMethod[]) => Router;
343
374
  type AddRouteShortcuts = Record<RouterMethod, RouterUse>;
@@ -353,4 +384,4 @@ interface CreateRouterOptions {
353
384
  }
354
385
  declare function createRouter(opts?: CreateRouterOptions): Router;
355
386
 
356
- 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, send, sendError, sendNoContent, sendProxy, sendRedirect, sendStream, setCookie, setHeader, setHeaders, setResponseHeader, setResponseHeaders, setResponseStatus, toEventHandler, toNodeListener, updateSession, use, useBase, useSession, writeEarlyHints };
387
+ 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, 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, toEventHandler, toNodeListener, unsealSession, updateSession, use, useBase, useSession, writeEarlyHints };
package/dist/index.mjs CHANGED
@@ -3,7 +3,8 @@ import { createRouter as createRouter$1 } from 'radix3';
3
3
  import destr from 'destr';
4
4
  import { parse as parse$1, serialize } from 'cookie-es';
5
5
  import crypto from 'uncrypto';
6
- import { unseal, defaults, seal } from 'iron-webcrypto';
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);
@@ -378,6 +379,59 @@ function deleteCookie(event, name, serializeOptions) {
378
379
  });
379
380
  }
380
381
 
382
+ function splitCookiesString(cookiesString) {
383
+ if (typeof cookiesString !== "string") {
384
+ return [];
385
+ }
386
+ const cookiesStrings = [];
387
+ let pos = 0;
388
+ let start;
389
+ let ch;
390
+ let lastComma;
391
+ let nextStart;
392
+ let cookiesSeparatorFound;
393
+ function skipWhitespace() {
394
+ while (pos < cookiesString.length && /\s/.test(cookiesString.charAt(pos))) {
395
+ pos += 1;
396
+ }
397
+ return pos < cookiesString.length;
398
+ }
399
+ function notSpecialChar() {
400
+ ch = cookiesString.charAt(pos);
401
+ return ch !== "=" && ch !== ";" && ch !== ",";
402
+ }
403
+ while (pos < cookiesString.length) {
404
+ start = pos;
405
+ cookiesSeparatorFound = false;
406
+ while (skipWhitespace()) {
407
+ ch = cookiesString.charAt(pos);
408
+ if (ch === ",") {
409
+ lastComma = pos;
410
+ pos += 1;
411
+ skipWhitespace();
412
+ nextStart = pos;
413
+ while (pos < cookiesString.length && notSpecialChar()) {
414
+ pos += 1;
415
+ }
416
+ if (pos < cookiesString.length && cookiesString.charAt(pos) === "=") {
417
+ cookiesSeparatorFound = true;
418
+ pos = nextStart;
419
+ cookiesStrings.push(cookiesString.slice(start, lastComma));
420
+ start = pos;
421
+ } else {
422
+ pos = lastComma + 1;
423
+ }
424
+ } else {
425
+ pos += 1;
426
+ }
427
+ }
428
+ if (!cookiesSeparatorFound || pos >= cookiesString.length) {
429
+ cookiesStrings.push(cookiesString.slice(start, cookiesString.length));
430
+ }
431
+ }
432
+ return cookiesStrings;
433
+ }
434
+
381
435
  const PayloadMethods = /* @__PURE__ */ new Set(["PATCH", "POST", "PUT", "DELETE"]);
382
436
  const ignoredHeaders = /* @__PURE__ */ new Set([
383
437
  "transfer-encoding",
@@ -424,6 +478,27 @@ async function sendProxy(event, target, opts = {}) {
424
478
  if (key === "content-length") {
425
479
  continue;
426
480
  }
481
+ if (key === "set-cookie") {
482
+ const cookies = splitCookiesString(value).map((cookie) => {
483
+ if (opts.cookieDomainRewrite) {
484
+ cookie = rewriteCookieProperty(
485
+ cookie,
486
+ opts.cookieDomainRewrite,
487
+ "domain"
488
+ );
489
+ }
490
+ if (opts.cookiePathRewrite) {
491
+ cookie = rewriteCookieProperty(
492
+ cookie,
493
+ opts.cookiePathRewrite,
494
+ "path"
495
+ );
496
+ }
497
+ return cookie;
498
+ });
499
+ event.node.res.setHeader("set-cookie", cookies);
500
+ continue;
501
+ }
427
502
  event.node.res.setHeader(key, value);
428
503
  }
429
504
  if (response._data !== void 0) {
@@ -470,6 +545,23 @@ function _getFetch(_fetch) {
470
545
  "fetch is not available. Try importing `node-fetch-native/polyfill` for Node.js."
471
546
  );
472
547
  }
548
+ function rewriteCookieProperty(header, map, property) {
549
+ const _map = typeof map === "string" ? { "*": map } : map;
550
+ return header.replace(
551
+ new RegExp(`(;\\s*${property}=)([^;]+)`, "gi"),
552
+ (match, prefix, previousValue) => {
553
+ let newValue;
554
+ if (previousValue in _map) {
555
+ newValue = _map[previousValue];
556
+ } else if ("*" in _map) {
557
+ newValue = _map["*"];
558
+ } else {
559
+ return match;
560
+ }
561
+ return newValue ? prefix + newValue : "";
562
+ }
563
+ );
564
+ }
473
565
 
474
566
  const defer = typeof setImmediate !== "undefined" ? setImmediate : (fn) => fn();
475
567
  function send(event, data, type) {
@@ -638,21 +730,35 @@ async function getSession(event, config) {
638
730
  if (event.context.sessions[sessionName]) {
639
731
  return event.context.sessions[sessionName];
640
732
  }
641
- const session = { id: "", data: /* @__PURE__ */ Object.create(null) };
733
+ const session = {
734
+ id: "",
735
+ createdAt: 0,
736
+ data: /* @__PURE__ */ Object.create(null)
737
+ };
642
738
  event.context.sessions[sessionName] = session;
643
- const reqCookie = getCookie(event, sessionName);
644
- if (!reqCookie) {
645
- session.id = (config.crypto || crypto).randomUUID();
646
- await updateSession(event, config);
647
- } else {
648
- const unsealed = await unseal(
649
- config.crypto || crypto,
650
- reqCookie,
651
- config.password,
652
- config.seal || defaults
739
+ let sealedSession;
740
+ if (config.sessionHeader !== false) {
741
+ const headerName = typeof config.sessionHeader === "string" ? config.sessionHeader.toLowerCase() : `x-${sessionName.toLowerCase()}-session`;
742
+ const headerValue = event.node.req.headers[headerName];
743
+ if (typeof headerValue === "string") {
744
+ sealedSession = headerValue;
745
+ }
746
+ }
747
+ if (!sealedSession) {
748
+ sealedSession = getCookie(event, sessionName);
749
+ }
750
+ if (sealedSession) {
751
+ const unsealed = await unsealSession(event, config, sealedSession).catch(
752
+ () => {
753
+ }
653
754
  );
654
755
  Object.assign(session, unsealed);
655
756
  }
757
+ if (!session.id) {
758
+ session.id = (config.crypto || crypto).randomUUID();
759
+ session.createdAt = Date.now();
760
+ await updateSession(event, config);
761
+ }
656
762
  return session;
657
763
  }
658
764
  async function updateSession(event, config, update) {
@@ -664,21 +770,168 @@ async function updateSession(event, config, update) {
664
770
  if (update) {
665
771
  Object.assign(session.data, update);
666
772
  }
667
- const sealed = await seal(
773
+ if (config.cookie !== false) {
774
+ const sealed = await sealSession(event, config);
775
+ setCookie(event, sessionName, sealed, {
776
+ ...DEFAULT_COOKIE,
777
+ expires: config.maxAge ? new Date(session.createdAt + config.maxAge * 1e3) : void 0,
778
+ ...config.cookie
779
+ });
780
+ }
781
+ return session;
782
+ }
783
+ async function sealSession(event, config) {
784
+ const sessionName = config.name || DEFAULT_NAME;
785
+ const session = event.context.sessions?.[sessionName] || await getSession(event, config);
786
+ const sealed = await seal(config.crypto || crypto, session, config.password, {
787
+ ...defaults,
788
+ ttl: config.maxAge ? config.maxAge * 1e3 : 0,
789
+ ...config.seal
790
+ });
791
+ return sealed;
792
+ }
793
+ async function unsealSession(_event, config, sealed) {
794
+ const unsealed = await unseal(
668
795
  config.crypto || crypto,
669
- session,
796
+ sealed,
670
797
  config.password,
671
- config.seal || defaults
798
+ {
799
+ ...defaults,
800
+ ttl: config.maxAge ? config.maxAge * 1e3 : 0,
801
+ ...config.seal
802
+ }
672
803
  );
673
- setCookie(event, sessionName, sealed, config.cookie || DEFAULT_COOKIE);
674
- return session;
804
+ if (config.maxAge) {
805
+ const age = Date.now() - (unsealed.createdAt || Number.NEGATIVE_INFINITY);
806
+ if (age > config.maxAge * 1e3) {
807
+ throw new Error("Session expired!");
808
+ }
809
+ }
810
+ return unsealed;
675
811
  }
676
812
  async function clearSession(event, config) {
677
813
  const sessionName = config.name || DEFAULT_NAME;
678
814
  if (event.context.sessions?.[sessionName]) {
679
815
  delete event.context.sessions[sessionName];
680
816
  }
681
- await setCookie(event, sessionName, "", config.cookie || DEFAULT_COOKIE);
817
+ await setCookie(event, sessionName, "", {
818
+ ...DEFAULT_COOKIE,
819
+ ...config.cookie
820
+ });
821
+ }
822
+
823
+ function resolveCorsOptions(options = {}) {
824
+ const defaultOptions = {
825
+ origin: "*",
826
+ methods: "*",
827
+ allowHeaders: "*",
828
+ exposeHeaders: "*",
829
+ credentials: false,
830
+ maxAge: false,
831
+ preflight: {
832
+ statusCode: 204
833
+ }
834
+ };
835
+ return defu(options, defaultOptions);
836
+ }
837
+ function isPreflightRequest(event) {
838
+ const method = getMethod(event);
839
+ const origin = getRequestHeader(event, "origin");
840
+ const accessControlRequestMethod = getRequestHeader(
841
+ event,
842
+ "access-control-request-method"
843
+ );
844
+ return method === "OPTIONS" && !!origin && !!accessControlRequestMethod;
845
+ }
846
+ function isCorsOriginAllowed(origin, options) {
847
+ const { origin: originOption } = options;
848
+ if (!origin || !originOption || originOption === "*" || originOption === "null") {
849
+ return true;
850
+ }
851
+ if (Array.isArray(originOption)) {
852
+ return originOption.some((_origin) => {
853
+ if (_origin instanceof RegExp) {
854
+ return _origin.test(origin);
855
+ }
856
+ return origin === _origin;
857
+ });
858
+ }
859
+ return originOption(origin);
860
+ }
861
+ function createOriginHeaders(event, options) {
862
+ const { origin: originOption } = options;
863
+ const origin = getRequestHeader(event, "origin");
864
+ if (!origin || !originOption || originOption === "*") {
865
+ return { "access-control-allow-origin": "*" };
866
+ }
867
+ if (typeof originOption === "string") {
868
+ return { "access-control-allow-origin": originOption, vary: "origin" };
869
+ }
870
+ return isCorsOriginAllowed(origin, options) ? { "access-control-allow-origin": origin, vary: "origin" } : {};
871
+ }
872
+ function createMethodsHeaders(options) {
873
+ const { methods } = options;
874
+ if (!methods) {
875
+ return {};
876
+ }
877
+ if (methods === "*") {
878
+ return { "access-control-allow-methods": "*" };
879
+ }
880
+ return methods.length > 0 ? { "access-control-allow-methods": methods.join(",") } : {};
881
+ }
882
+ function createCredentialsHeaders(options) {
883
+ const { credentials } = options;
884
+ if (credentials) {
885
+ return { "access-control-allow-credentials": "true" };
886
+ }
887
+ return {};
888
+ }
889
+ function createAllowHeaderHeaders(event, options) {
890
+ const { allowHeaders } = options;
891
+ if (!allowHeaders || allowHeaders === "*" || allowHeaders.length === 0) {
892
+ const header = getRequestHeader(event, "access-control-request-headers");
893
+ return header ? {
894
+ "access-control-allow-headers": header,
895
+ vary: "access-control-request-headers"
896
+ } : {};
897
+ }
898
+ return {
899
+ "access-control-allow-headers": allowHeaders.join(","),
900
+ vary: "access-control-request-headers"
901
+ };
902
+ }
903
+ function createExposeHeaders(options) {
904
+ const { exposeHeaders } = options;
905
+ if (!exposeHeaders) {
906
+ return {};
907
+ }
908
+ if (exposeHeaders === "*") {
909
+ return { "access-control-expose-headers": exposeHeaders };
910
+ }
911
+ return { "access-control-expose-headers": exposeHeaders.join(",") };
912
+ }
913
+ function appendCorsPreflightHeaders(event, options) {
914
+ appendHeaders(event, createOriginHeaders(event, options));
915
+ appendHeaders(event, createCredentialsHeaders(options));
916
+ appendHeaders(event, createExposeHeaders(options));
917
+ appendHeaders(event, createMethodsHeaders(options));
918
+ appendHeaders(event, createAllowHeaderHeaders(event, options));
919
+ }
920
+ function appendCorsHeaders(event, options) {
921
+ appendHeaders(event, createOriginHeaders(event, options));
922
+ appendHeaders(event, createCredentialsHeaders(options));
923
+ appendHeaders(event, createExposeHeaders(options));
924
+ }
925
+
926
+ function handleCors(event, options) {
927
+ const _options = resolveCorsOptions(options);
928
+ if (isPreflightRequest(event)) {
929
+ appendCorsPreflightHeaders(event, options);
930
+ sendNoContent(event, _options.preflight.statusCode);
931
+ return true;
932
+ }
933
+ appendCorsHeaders(event, options);
934
+ return false;
682
935
  }
683
936
 
684
937
  class H3Headers {
@@ -1130,4 +1383,4 @@ function createRouter(opts = {}) {
1130
1383
  return router;
1131
1384
  }
1132
1385
 
1133
- 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, send, sendError, sendNoContent, sendProxy, sendRedirect, sendStream, setCookie, setHeader, setHeaders, setResponseHeader, setResponseHeaders, setResponseStatus, toEventHandler, toNodeListener, updateSession, use, useBase, useSession, writeEarlyHints };
1386
+ 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, 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, 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.3.0",
3
+ "version": "1.5.0",
4
4
  "description": "Tiny JavaScript Server",
5
5
  "repository": "unjs/h3",
6
6
  "license": "MIT",
@@ -21,10 +21,11 @@
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.0",
28
29
  "uncrypto": "^0.1.2"
29
30
  },
30
31
  "devDependencies": {
@@ -32,28 +33,29 @@
32
33
  "@types/express": "^4.17.17",
33
34
  "@types/node": "^18.13.0",
34
35
  "@types/supertest": "^2.0.12",
35
- "@vitest/coverage-c8": "^0.28.4",
36
+ "@vitest/coverage-c8": "^0.28.5",
36
37
  "autocannon": "^7.10.0",
37
38
  "changelogen": "^0.4.1",
38
39
  "connect": "^3.7.0",
39
- "eslint": "^8.33.0",
40
+ "eslint": "^8.34.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
+ "jiti": "^1.17.0",
44
45
  "listhen": "^1.0.2",
45
- "node-fetch-native": "^1.0.1",
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
50
  "unbuild": "^1.1.1",
50
- "vitest": "^0.28.4"
51
+ "vitest": "^0.28.5"
51
52
  },
52
- "packageManager": "pnpm@7.26.3",
53
+ "packageManager": "pnpm@7.27.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",