h3 1.1.0 → 1.2.1

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
@@ -90,7 +90,7 @@ const router = createRouter()
90
90
  app.use(router);
91
91
  ```
92
92
 
93
- **Tip:** We can register same route more than once with different methods.
93
+ **Tip:** We can register the same route more than once with different methods.
94
94
 
95
95
  Routes are internally stored in a [Radix Tree](https://en.wikipedia.org/wiki/Radix_tree) and matched using [unjs/radix3](https://github.com/unjs/radix3).
96
96
 
@@ -119,7 +119,7 @@ app.use('/big', () => import('./big-handler'), { lazy: true })
119
119
 
120
120
  ## Utilities
121
121
 
122
- H3 has concept of compasable utilities that accept `event` (from `eventHandler((event) => {})`) as their first argument. This has several performance benefits over injecting them to `event` or `app` instances and global middleware commonly used in Node.js frameworks such as Express, which Only required code is evaluated and bundled and rest of utils can be tree-shaken when not used.
122
+ H3 has a concept of composable utilities that accept `event` (from `eventHandler((event) => {})`) as their first argument. This has several performance benefits over injecting them to `event` or `app` instances in global middleware commonly used in Node.js frameworks, such as Express. This concept means only required code is evaluated and bundled, and the rest of the utilities can be tree-shaken when not used.
123
123
 
124
124
  ### Built-in
125
125
 
@@ -153,12 +153,16 @@ H3 has concept of compasable utilities that accept `event` (from `eventHandler((
153
153
  - `getResponseStatus(event)`
154
154
  - `getResponseStatusText(event)`
155
155
  - `readMultipartFormData(event)`
156
+ - `useSession(event, { password, name?, cookie?, seal?, crypto? })`
157
+ - `getSession(event, { password, name?, cookie?, seal?, crypto? })`
158
+ - `updateSession(event, { password, name?, cookie?, seal?, crypto? }), update)`
159
+ - `clearSession(event, { password, name?, cookie?, seal?, crypto? }))`
156
160
 
157
161
  👉 You can learn more about usage in [JSDocs Documentation](https://www.jsdocs.io/package/h3#package-functions).
158
162
 
159
163
  ## Community Packages
160
164
 
161
- You can use more h3 event utilities made by the community.
165
+ You can use more H3 event utilities made by the community.
162
166
 
163
167
  Please check their READMEs for more details.
164
168
 
package/dist/index.cjs CHANGED
@@ -4,6 +4,7 @@ const ufo = require('ufo');
4
4
  const radix3 = require('radix3');
5
5
  const destr = require('destr');
6
6
  const cookieEs = require('cookie-es');
7
+ const crypto = require('uncrypto');
7
8
 
8
9
  function useBase(base, handler) {
9
10
  base = ufo.withoutTrailingSlash(base);
@@ -351,6 +352,111 @@ const MIMES = {
351
352
  json: "application/json"
352
353
  };
353
354
 
355
+ function parseCookies(event) {
356
+ return cookieEs.parse(event.node.req.headers.cookie || "");
357
+ }
358
+ function getCookie(event, name) {
359
+ return parseCookies(event)[name];
360
+ }
361
+ function setCookie(event, name, value, serializeOptions) {
362
+ const cookieStr = cookieEs.serialize(name, value, {
363
+ path: "/",
364
+ ...serializeOptions
365
+ });
366
+ let setCookies = event.node.res.getHeader("set-cookie");
367
+ if (!Array.isArray(setCookies)) {
368
+ setCookies = [setCookies];
369
+ }
370
+ setCookies = setCookies.filter((cookieValue) => {
371
+ return cookieValue && !cookieValue.startsWith(name + "=");
372
+ });
373
+ event.node.res.setHeader("set-cookie", [...setCookies, cookieStr]);
374
+ }
375
+ function deleteCookie(event, name, serializeOptions) {
376
+ setCookie(event, name, "", {
377
+ ...serializeOptions,
378
+ maxAge: 0
379
+ });
380
+ }
381
+
382
+ const PayloadMethods = /* @__PURE__ */ new Set(["PATCH", "POST", "PUT", "DELETE"]);
383
+ const ignoredHeaders = /* @__PURE__ */ new Set([
384
+ "transfer-encoding",
385
+ "connection",
386
+ "keep-alive",
387
+ "upgrade",
388
+ "expect",
389
+ "host"
390
+ ]);
391
+ async function proxyRequest(event, target, opts = {}) {
392
+ const method = getMethod(event);
393
+ let body;
394
+ if (PayloadMethods.has(method)) {
395
+ body = await readRawBody(event).catch(() => void 0);
396
+ }
397
+ const headers = /* @__PURE__ */ Object.create(null);
398
+ const reqHeaders = getRequestHeaders(event);
399
+ for (const name in reqHeaders) {
400
+ if (!ignoredHeaders.has(name)) {
401
+ headers[name] = reqHeaders[name];
402
+ }
403
+ }
404
+ if (opts.fetchOptions?.headers) {
405
+ Object.assign(headers, opts.fetchOptions.headers);
406
+ }
407
+ if (opts.headers) {
408
+ Object.assign(headers, opts.headers);
409
+ }
410
+ return sendProxy(event, target, {
411
+ ...opts,
412
+ fetchOptions: {
413
+ headers,
414
+ method,
415
+ body,
416
+ ...opts.fetchOptions
417
+ }
418
+ });
419
+ }
420
+ async function sendProxy(event, target, opts = {}) {
421
+ const _fetch = opts.fetch || globalThis.fetch;
422
+ if (!_fetch) {
423
+ throw new Error(
424
+ "fetch is not available. Try importing `node-fetch-native/polyfill` for Node.js."
425
+ );
426
+ }
427
+ const response = await _fetch(target, {
428
+ headers: opts.headers,
429
+ ...opts.fetchOptions
430
+ });
431
+ event.node.res.statusCode = response.status;
432
+ event.node.res.statusMessage = response.statusText;
433
+ for (const [key, value] of response.headers.entries()) {
434
+ if (key === "content-encoding") {
435
+ continue;
436
+ }
437
+ if (key === "content-length") {
438
+ continue;
439
+ }
440
+ event.node.res.setHeader(key, value);
441
+ }
442
+ try {
443
+ if (response.body) {
444
+ if (opts.sendStream === false) {
445
+ const data = new Uint8Array(await response.arrayBuffer());
446
+ event.node.res.end(data);
447
+ } else {
448
+ for await (const chunk of response.body) {
449
+ event.node.res.write(chunk);
450
+ }
451
+ event.node.res.end();
452
+ }
453
+ }
454
+ } catch (error) {
455
+ event.node.res.end();
456
+ throw error;
457
+ }
458
+ }
459
+
354
460
  const defer = typeof setImmediate !== "undefined" ? setImmediate : (fn) => fn();
355
461
  function send(event, data, type) {
356
462
  if (type) {
@@ -483,101 +589,372 @@ ${header}: ${value}`;
483
589
  }
484
590
  }
485
591
 
486
- function parseCookies(event) {
487
- return cookieEs.parse(event.node.req.headers.cookie || "");
488
- }
489
- function getCookie(event, name) {
490
- return parseCookies(event)[name];
491
- }
492
- function setCookie(event, name, value, serializeOptions) {
493
- const cookieStr = cookieEs.serialize(name, value, {
494
- path: "/",
495
- ...serializeOptions
496
- });
497
- appendHeader(event, "Set-Cookie", cookieStr);
498
- }
499
- function deleteCookie(event, name, serializeOptions) {
500
- setCookie(event, name, "", {
501
- ...serializeOptions,
502
- maxAge: 0
503
- });
504
- }
505
-
506
- const PayloadMethods = /* @__PURE__ */ new Set(["PATCH", "POST", "PUT", "DELETE"]);
507
- const ignoredHeaders = /* @__PURE__ */ new Set([
508
- "transfer-encoding",
509
- "connection",
510
- "keep-alive",
511
- "upgrade",
512
- "expect"
513
- ]);
514
- async function proxyRequest(event, target, opts = {}) {
515
- const method = getMethod(event);
516
- let body;
517
- if (PayloadMethods.has(method)) {
518
- body = await readRawBody(event).catch(() => void 0);
592
+ const base64urlEncode = (value) => (Buffer.isBuffer(value) ? value : Buffer.from(value)).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
593
+ const defaults = {
594
+ encryption: {
595
+ saltBits: 256,
596
+ algorithm: "aes-256-cbc",
597
+ iterations: 1,
598
+ minPasswordlength: 32
599
+ },
600
+ integrity: {
601
+ saltBits: 256,
602
+ algorithm: "sha256",
603
+ iterations: 1,
604
+ minPasswordlength: 32
605
+ },
606
+ ttl: 0,
607
+ timestampSkewSec: 60,
608
+ localtimeOffsetMsec: 0
609
+ };
610
+ const clone = (options) => ({
611
+ ...options,
612
+ encryption: { ...options.encryption },
613
+ integrity: { ...options.integrity }
614
+ });
615
+ const algorithms = {
616
+ "aes-128-ctr": { keyBits: 128, ivBits: 128, name: "AES-CTR" },
617
+ "aes-256-cbc": { keyBits: 256, ivBits: 128, name: "AES-CBC" },
618
+ sha256: { keyBits: 256, name: "SHA-256" }
619
+ };
620
+ const macFormatVersion = "2";
621
+ const macPrefix = `Fe26.${macFormatVersion}`;
622
+ const randomBytes = (_crypto, size) => {
623
+ const bytes = Buffer.allocUnsafe(size);
624
+ _crypto.getRandomValues(bytes);
625
+ return bytes;
626
+ };
627
+ const randomBits = (_crypto, bits) => {
628
+ if (bits < 1) {
629
+ throw new Error("Invalid random bits count");
519
630
  }
520
- const headers = /* @__PURE__ */ Object.create(null);
521
- const reqHeaders = getRequestHeaders(event);
522
- for (const name in reqHeaders) {
523
- if (!ignoredHeaders.has(name)) {
524
- headers[name] = reqHeaders[name];
631
+ const bytes = Math.ceil(bits / 8);
632
+ return randomBytes(_crypto, bytes);
633
+ };
634
+ const pbkdf2 = async (_crypto, password, salt, iterations, keyLength, hash) => {
635
+ const textEncoder = new TextEncoder();
636
+ const passwordBuffer = textEncoder.encode(password);
637
+ const importedKey = await _crypto.subtle.importKey(
638
+ "raw",
639
+ passwordBuffer,
640
+ "PBKDF2",
641
+ false,
642
+ ["deriveBits"]
643
+ );
644
+ const saltBuffer = textEncoder.encode(salt);
645
+ const params = { name: "PBKDF2", hash, salt: saltBuffer, iterations };
646
+ const derivation = await _crypto.subtle.deriveBits(
647
+ params,
648
+ importedKey,
649
+ keyLength * 8
650
+ );
651
+ return Buffer.from(derivation);
652
+ };
653
+ const generateKey = async (_crypto, password, options) => {
654
+ if (password == null || password.length === 0) {
655
+ throw new Error("Empty password");
656
+ }
657
+ if (options == null || typeof options !== "object") {
658
+ throw new Error("Bad options");
659
+ }
660
+ if (!(options.algorithm in algorithms)) {
661
+ throw new Error(`Unknown algorithm: ${options.algorithm}`);
662
+ }
663
+ const algorithm = algorithms[options.algorithm];
664
+ const result = {};
665
+ const hmac = options.hmac ?? false;
666
+ const id = hmac ? { name: "HMAC", hash: algorithm.name } : { name: algorithm.name };
667
+ const usage = hmac ? ["sign", "verify"] : ["encrypt", "decrypt"];
668
+ if (typeof password === "string") {
669
+ if (password.length < options.minPasswordlength) {
670
+ throw new Error(
671
+ `Password string too short (min ${options.minPasswordlength} characters required)`
672
+ );
525
673
  }
674
+ let { salt = "" } = options;
675
+ if (!salt) {
676
+ const { saltBits = 0 } = options;
677
+ if (!saltBits) {
678
+ throw new Error("Missing salt and saltBits options");
679
+ }
680
+ const randomSalt = randomBits(_crypto, saltBits);
681
+ salt = randomSalt.toString("hex");
682
+ }
683
+ const derivedKey = await pbkdf2(
684
+ _crypto,
685
+ password,
686
+ salt,
687
+ options.iterations,
688
+ algorithm.keyBits / 8,
689
+ "SHA-1"
690
+ );
691
+ const importedEncryptionKey = await _crypto.subtle.importKey(
692
+ "raw",
693
+ derivedKey,
694
+ id,
695
+ false,
696
+ usage
697
+ );
698
+ result.key = importedEncryptionKey;
699
+ result.salt = salt;
700
+ } else {
701
+ if (password.length < algorithm.keyBits / 8) {
702
+ throw new Error("Key buffer (password) too small");
703
+ }
704
+ result.key = await _crypto.subtle.importKey(
705
+ "raw",
706
+ password,
707
+ id,
708
+ false,
709
+ usage
710
+ );
711
+ result.salt = "";
526
712
  }
527
- if (opts.fetchOptions?.headers) {
528
- Object.assign(headers, opts.fetchOptions.headers);
529
- }
530
- if (opts.headers) {
531
- Object.assign(headers, opts.headers);
713
+ if (options.iv) {
714
+ result.iv = options.iv;
715
+ } else if ("ivBits" in algorithm) {
716
+ result.iv = randomBits(_crypto, algorithm.ivBits);
532
717
  }
533
- return sendProxy(event, target, {
534
- ...opts,
535
- fetchOptions: {
536
- headers,
537
- method,
538
- body,
539
- ...opts.fetchOptions
718
+ return result;
719
+ };
720
+ const encrypt = async (_crypto, password, options, data) => {
721
+ const key = await generateKey(_crypto, password, options);
722
+ const textEncoder = new TextEncoder();
723
+ const textBuffer = textEncoder.encode(data);
724
+ const encrypted = await _crypto.subtle.encrypt(
725
+ { name: algorithms[options.algorithm].name, iv: key.iv },
726
+ key.key,
727
+ textBuffer
728
+ );
729
+ return { encrypted: Buffer.from(encrypted), key };
730
+ };
731
+ const decrypt = async (_crypto, password, options, data) => {
732
+ const key = await generateKey(_crypto, password, options);
733
+ const decrypted = await _crypto.subtle.decrypt(
734
+ { name: algorithms[options.algorithm].name, iv: key.iv },
735
+ key.key,
736
+ Buffer.isBuffer(data) ? data : Buffer.from(data)
737
+ );
738
+ const textDecoder = new TextDecoder();
739
+ return textDecoder.decode(decrypted);
740
+ };
741
+ const hmacWithPassword = async (_crypto, password, options, data) => {
742
+ const key = await generateKey(_crypto, password, { ...options, hmac: true });
743
+ const textEncoder = new TextEncoder();
744
+ const textBuffer = textEncoder.encode(data);
745
+ const signed = await _crypto.subtle.sign(
746
+ { name: "HMAC" },
747
+ key.key,
748
+ textBuffer
749
+ );
750
+ const digest = base64urlEncode(Buffer.from(signed));
751
+ return { digest, salt: key.salt };
752
+ };
753
+ const normalizePassword = (password) => {
754
+ if (typeof password === "object" && !Buffer.isBuffer(password)) {
755
+ if ("secret" in password) {
756
+ return {
757
+ id: password.id,
758
+ encryption: password.secret,
759
+ integrity: password.secret
760
+ };
540
761
  }
541
- });
542
- }
543
- async function sendProxy(event, target, opts = {}) {
544
- const _fetch = opts.fetch || globalThis.fetch;
545
- if (!_fetch) {
546
- throw new Error(
547
- "fetch is not available. Try importing `node-fetch-native/polyfill` for Node.js."
548
- );
762
+ return {
763
+ id: password.id,
764
+ encryption: password.encryption,
765
+ integrity: password.integrity
766
+ };
549
767
  }
550
- const response = await _fetch(target, {
551
- headers: opts.headers,
552
- ...opts.fetchOptions
553
- });
554
- event.node.res.statusCode = response.status;
555
- event.node.res.statusMessage = response.statusText;
556
- for (const [key, value] of response.headers.entries()) {
557
- if (key === "content-encoding") {
558
- continue;
768
+ return { encryption: password, integrity: password };
769
+ };
770
+ const seal = async (_crypto, object, password, options) => {
771
+ if (!password) {
772
+ throw new Error("Empty password");
773
+ }
774
+ const opts = clone(options);
775
+ const now = Date.now() + (opts.localtimeOffsetMsec || 0);
776
+ const objectString = JSON.stringify(object);
777
+ const pass = normalizePassword(password);
778
+ const { id = "" } = pass;
779
+ if (id && !/^\w+$/.test(id)) {
780
+ throw new Error("Invalid password id");
781
+ }
782
+ const { encrypted, key } = await encrypt(
783
+ _crypto,
784
+ pass.encryption,
785
+ opts.encryption,
786
+ objectString
787
+ );
788
+ const encryptedB64 = base64urlEncode(encrypted);
789
+ const iv = base64urlEncode(key.iv);
790
+ const expiration = opts.ttl ? now + opts.ttl : "";
791
+ const macBaseString = `${macPrefix}*${id}*${key.salt}*${iv}*${encryptedB64}*${expiration}`;
792
+ const mac = await hmacWithPassword(
793
+ _crypto,
794
+ pass.integrity,
795
+ opts.integrity,
796
+ macBaseString
797
+ );
798
+ const sealed = `${macBaseString}*${mac.salt}*${mac.digest}`;
799
+ return sealed;
800
+ };
801
+ const fixedTimeComparison = (a, b) => {
802
+ let mismatch = a.length === b.length ? 0 : 1;
803
+ if (mismatch) {
804
+ b = a;
805
+ }
806
+ for (let i = 0; i < a.length; i += 1) {
807
+ mismatch |= a.charCodeAt(i) ^ b.charCodeAt(i);
808
+ }
809
+ return mismatch === 0;
810
+ };
811
+ const unseal = async (_crypto, sealed, password, options) => {
812
+ if (!password) {
813
+ throw new Error("Empty password");
814
+ }
815
+ const opts = clone(options);
816
+ const now = Date.now() + (opts.localtimeOffsetMsec || 0);
817
+ const parts = sealed.split("*");
818
+ if (parts.length !== 8) {
819
+ throw new Error("Incorrect number of sealed components");
820
+ }
821
+ const prefix = parts[0];
822
+ const passwordId = parts[1];
823
+ const encryptionSalt = parts[2];
824
+ const encryptionIv = parts[3];
825
+ const encryptedB64 = parts[4];
826
+ const expiration = parts[5];
827
+ const hmacSalt = parts[6];
828
+ const hmac = parts[7];
829
+ const macBaseString = `${prefix}*${passwordId}*${encryptionSalt}*${encryptionIv}*${encryptedB64}*${expiration}`;
830
+ if (macPrefix !== prefix) {
831
+ throw new Error("Wrong mac prefix");
832
+ }
833
+ if (expiration) {
834
+ if (!/^\d+$/.test(expiration)) {
835
+ throw new Error("Invalid expiration");
559
836
  }
560
- if (key === "content-length") {
561
- continue;
837
+ const exp = Number.parseInt(expiration, 10);
838
+ if (exp <= now - opts.timestampSkewSec * 1e3) {
839
+ throw new Error("Expired seal");
562
840
  }
563
- event.node.res.setHeader(key, value);
564
841
  }
565
- try {
566
- if (response.body) {
567
- if (opts.sendStream === false) {
568
- const data = new Uint8Array(await response.arrayBuffer());
569
- event.node.res.end(data);
570
- } else {
571
- for await (const chunk of response.body) {
572
- event.node.res.write(chunk);
573
- }
574
- event.node.res.end();
575
- }
842
+ if (typeof password === "undefined" || typeof password === "string" && password.length === 0) {
843
+ throw new Error("Empty password");
844
+ }
845
+ let pass;
846
+ if (typeof password === "object" && !Buffer.isBuffer(password)) {
847
+ if (!((passwordId || "default") in password)) {
848
+ throw new Error(`Cannot find password: ${passwordId}`);
576
849
  }
577
- } catch (error) {
578
- event.node.res.end();
579
- throw error;
850
+ pass = password[passwordId || "default"];
851
+ } else {
852
+ pass = password;
853
+ }
854
+ pass = normalizePassword(pass);
855
+ const macOptions = opts.integrity;
856
+ macOptions.salt = hmacSalt;
857
+ const mac = await hmacWithPassword(
858
+ _crypto,
859
+ pass.integrity,
860
+ macOptions,
861
+ macBaseString
862
+ );
863
+ if (!fixedTimeComparison(mac.digest, hmac)) {
864
+ throw new Error("Bad hmac value");
865
+ }
866
+ const encrypted = Buffer.from(encryptedB64, "base64");
867
+ const decryptOptions = opts.encryption;
868
+ decryptOptions.salt = encryptionSalt;
869
+ decryptOptions.iv = Buffer.from(encryptionIv, "base64");
870
+ const decrypted = await decrypt(
871
+ _crypto,
872
+ pass.encryption,
873
+ decryptOptions,
874
+ encrypted
875
+ );
876
+ if (decrypted) {
877
+ return JSON.parse(decrypted);
878
+ }
879
+ return null;
880
+ };
881
+
882
+ const DEFAULT_NAME = "h3";
883
+ const DEFAULT_COOKIE = {
884
+ path: "/",
885
+ secure: true,
886
+ httpOnly: true
887
+ };
888
+ async function useSession(event, config) {
889
+ const sessionName = config.name || DEFAULT_NAME;
890
+ await getSession(event, config);
891
+ const sessionManager = {
892
+ get id() {
893
+ return event.context.sessions?.[sessionName]?.id;
894
+ },
895
+ get data() {
896
+ return event.context.sessions?.[sessionName]?.data || {};
897
+ },
898
+ update: async (update) => {
899
+ await updateSession(event, config, update);
900
+ return sessionManager;
901
+ },
902
+ clear: async () => {
903
+ await clearSession(event, config);
904
+ return sessionManager;
905
+ }
906
+ };
907
+ return sessionManager;
908
+ }
909
+ async function getSession(event, config) {
910
+ const sessionName = config.name || DEFAULT_NAME;
911
+ if (!event.context.sessions) {
912
+ event.context.sessions = /* @__PURE__ */ Object.create(null);
913
+ }
914
+ if (event.context.sessions[sessionName]) {
915
+ return event.context.sessions[sessionName];
916
+ }
917
+ const session = { id: "", data: /* @__PURE__ */ Object.create(null) };
918
+ event.context.sessions[sessionName] = session;
919
+ const reqCookie = getCookie(event, sessionName);
920
+ if (!reqCookie) {
921
+ session.id = (config.crypto || crypto).randomUUID();
922
+ await updateSession(event, config);
923
+ } else {
924
+ const unsealed = await unseal(
925
+ config.crypto || crypto,
926
+ reqCookie,
927
+ config.password,
928
+ config.seal || defaults
929
+ );
930
+ Object.assign(session, unsealed);
931
+ }
932
+ return session;
933
+ }
934
+ async function updateSession(event, config, update) {
935
+ const sessionName = config.name || DEFAULT_NAME;
936
+ const session = event.context.sessions?.[sessionName] || await getSession(event, config);
937
+ if (typeof update === "function") {
938
+ update = update(session.data);
939
+ }
940
+ if (update) {
941
+ Object.assign(session.data, update);
942
+ }
943
+ const sealed = await seal(
944
+ config.crypto || crypto,
945
+ session,
946
+ config.password,
947
+ config.seal || defaults
948
+ );
949
+ setCookie(event, sessionName, sealed, config.cookie || DEFAULT_COOKIE);
950
+ return session;
951
+ }
952
+ async function clearSession(event, config) {
953
+ const sessionName = config.name || DEFAULT_NAME;
954
+ if (event.context.sessions?.[sessionName]) {
955
+ delete event.context.sessions[sessionName];
580
956
  }
957
+ await setCookie(event, sessionName, "", config.cookie || DEFAULT_COOKIE);
581
958
  }
582
959
 
583
960
  class H3Headers {
@@ -1040,6 +1417,7 @@ exports.appendResponseHeader = appendResponseHeader;
1040
1417
  exports.appendResponseHeaders = appendResponseHeaders;
1041
1418
  exports.assertMethod = assertMethod;
1042
1419
  exports.callNodeListener = callNodeListener;
1420
+ exports.clearSession = clearSession;
1043
1421
  exports.createApp = createApp;
1044
1422
  exports.createAppEventHandler = createAppEventHandler;
1045
1423
  exports.createError = createError;
@@ -1067,6 +1445,7 @@ exports.getResponseStatus = getResponseStatus;
1067
1445
  exports.getResponseStatusText = getResponseStatusText;
1068
1446
  exports.getRouterParam = getRouterParam;
1069
1447
  exports.getRouterParams = getRouterParams;
1448
+ exports.getSession = getSession;
1070
1449
  exports.handleCacheHeaders = handleCacheHeaders;
1071
1450
  exports.isError = isError;
1072
1451
  exports.isEvent = isEvent;
@@ -1094,6 +1473,8 @@ exports.setResponseHeaders = setResponseHeaders;
1094
1473
  exports.setResponseStatus = setResponseStatus;
1095
1474
  exports.toEventHandler = toEventHandler;
1096
1475
  exports.toNodeListener = toNodeListener;
1476
+ exports.updateSession = updateSession;
1097
1477
  exports.use = use;
1098
1478
  exports.useBase = useBase;
1479
+ exports.useSession = useSession;
1099
1480
  exports.writeEarlyHints = writeEarlyHints;
package/dist/index.d.ts CHANGED
@@ -1,11 +1,84 @@
1
+ import { CookieSerializeOptions } from 'cookie-es';
1
2
  import { IncomingMessage, ServerResponse, OutgoingMessage } from 'node:http';
2
3
  export { IncomingMessage as NodeIncomingMessage, ServerResponse as NodeServerResponse } from 'node:http';
3
- import { CookieSerializeOptions } from 'cookie-es';
4
4
  import * as ufo from 'ufo';
5
5
 
6
+ /**
7
+ * seal() method options.
8
+ */
9
+ interface SealOptionsSub {
10
+ /**
11
+ * The length of the salt (random buffer used to ensure that two identical objects will generate a different encrypted result). Defaults to 256.
12
+ */
13
+ saltBits: number;
14
+ /**
15
+ * The algorithm used. Defaults to 'aes-256-cbc' for encryption and 'sha256' for integrity.
16
+ */
17
+ algorithm: "aes-128-ctr" | "aes-256-cbc" | "sha256";
18
+ /**
19
+ * The number of iterations used to derive a key from the password. Defaults to 1.
20
+ */
21
+ iterations: number;
22
+ /**
23
+ * Minimum password size. Defaults to 32.
24
+ */
25
+ minPasswordlength: number;
26
+ }
27
+ /**
28
+ * Options for customizing the key derivation algorithm used to generate encryption and integrity verification keys as well as the algorithms and salt sizes used.
29
+ */
30
+ interface SealOptions {
31
+ /**
32
+ * Encryption step options.
33
+ */
34
+ encryption: SealOptionsSub;
35
+ /**
36
+ * Integrity step options.
37
+ */
38
+ integrity: SealOptionsSub;
39
+ /**
40
+ * Sealed object lifetime in milliseconds where 0 means forever. Defaults to 0.
41
+ */
42
+ ttl: number;
43
+ /**
44
+ * Number of seconds of permitted clock skew for incoming expirations. Defaults to 60 seconds.
45
+ */
46
+ timestampSkewSec: number;
47
+ /**
48
+ * Local clock time offset, expressed in number of milliseconds (positive or negative). Defaults to 0.
49
+ */
50
+ localtimeOffsetMsec: number;
51
+ }
52
+
53
+ type SessionDataT = Record<string, string | number | boolean>;
54
+ type SessionData<T extends SessionDataT = SessionDataT> = T;
55
+ interface Session<T extends SessionDataT = SessionDataT> {
56
+ id: string;
57
+ data: SessionData<T>;
58
+ }
59
+ interface SessionConfig {
60
+ password: string;
61
+ name?: string;
62
+ cookie?: CookieSerializeOptions;
63
+ seal?: SealOptions;
64
+ crypto?: Crypto;
65
+ }
66
+ declare function useSession<T extends SessionDataT = SessionDataT>(event: H3Event, config: SessionConfig): Promise<{
67
+ readonly id: string | undefined;
68
+ readonly data: SessionDataT;
69
+ update: (update: SessionUpdate<T>) => Promise<any>;
70
+ clear: () => Promise<any>;
71
+ }>;
72
+ declare function getSession<T extends SessionDataT = SessionDataT>(event: H3Event, config: SessionConfig): Promise<Session<T>>;
73
+ type SessionUpdate<T extends SessionDataT = SessionDataT> = Partial<SessionData<T>> | ((oldData: SessionData<T>) => Partial<SessionData<T>> | undefined);
74
+ declare function updateSession<T extends SessionDataT = SessionDataT>(event: H3Event, config: SessionConfig, update?: SessionUpdate<T>): Promise<Session<T>>;
75
+ declare function clearSession(event: H3Event, config: SessionConfig): Promise<void>;
76
+
6
77
  type HTTPMethod = "GET" | "HEAD" | "PATCH" | "POST" | "PUT" | "DELETE" | "CONNECT" | "OPTIONS" | "TRACE";
7
78
  type Encoding = false | "ascii" | "utf8" | "utf-8" | "utf16le" | "ucs2" | "ucs-2" | "base64" | "latin1" | "binary" | "hex";
8
79
  interface H3EventContext extends Record<string, any> {
80
+ params?: Record<string, string>;
81
+ sessions?: Record<string, Session>;
9
82
  }
10
83
  type EventHandlerResponse<T = any> = T | Promise<T>;
11
84
  interface EventHandler<T = any> {
@@ -322,4 +395,4 @@ interface CreateRouterOptions {
322
395
  }
323
396
  declare function createRouter(opts?: CreateRouterOptions): Router;
324
397
 
325
- 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, Stack, appendHeader, appendHeaders, appendResponseHeader, appendResponseHeaders, assertMethod, callNodeListener, createApp, createAppEventHandler, createError, createEvent, createRouter, defaultContentType, defineEventHandler, defineLazyEventHandler, defineNodeListener, defineNodeMiddleware, deleteCookie, dynamicEventHandler, eventHandler, fromNodeMiddleware, getCookie, getHeader, getHeaders, getMethod, getQuery, getRequestHeader, getRequestHeaders, getResponseHeader, getResponseHeaders, getResponseStatus, getResponseStatusText, getRouterParam, getRouterParams, 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, use, useBase, writeEarlyHints };
398
+ 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, fromNodeMiddleware, getCookie, getHeader, getHeaders, getMethod, 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 };
package/dist/index.mjs CHANGED
@@ -2,6 +2,7 @@ import { withoutTrailingSlash, withoutBase, getQuery as getQuery$1 } from 'ufo';
2
2
  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
+ import crypto from 'uncrypto';
5
6
 
6
7
  function useBase(base, handler) {
7
8
  base = withoutTrailingSlash(base);
@@ -349,6 +350,111 @@ const MIMES = {
349
350
  json: "application/json"
350
351
  };
351
352
 
353
+ function parseCookies(event) {
354
+ return parse$1(event.node.req.headers.cookie || "");
355
+ }
356
+ function getCookie(event, name) {
357
+ return parseCookies(event)[name];
358
+ }
359
+ function setCookie(event, name, value, serializeOptions) {
360
+ const cookieStr = serialize(name, value, {
361
+ path: "/",
362
+ ...serializeOptions
363
+ });
364
+ let setCookies = event.node.res.getHeader("set-cookie");
365
+ if (!Array.isArray(setCookies)) {
366
+ setCookies = [setCookies];
367
+ }
368
+ setCookies = setCookies.filter((cookieValue) => {
369
+ return cookieValue && !cookieValue.startsWith(name + "=");
370
+ });
371
+ event.node.res.setHeader("set-cookie", [...setCookies, cookieStr]);
372
+ }
373
+ function deleteCookie(event, name, serializeOptions) {
374
+ setCookie(event, name, "", {
375
+ ...serializeOptions,
376
+ maxAge: 0
377
+ });
378
+ }
379
+
380
+ const PayloadMethods = /* @__PURE__ */ new Set(["PATCH", "POST", "PUT", "DELETE"]);
381
+ const ignoredHeaders = /* @__PURE__ */ new Set([
382
+ "transfer-encoding",
383
+ "connection",
384
+ "keep-alive",
385
+ "upgrade",
386
+ "expect",
387
+ "host"
388
+ ]);
389
+ async function proxyRequest(event, target, opts = {}) {
390
+ const method = getMethod(event);
391
+ let body;
392
+ if (PayloadMethods.has(method)) {
393
+ body = await readRawBody(event).catch(() => void 0);
394
+ }
395
+ const headers = /* @__PURE__ */ Object.create(null);
396
+ const reqHeaders = getRequestHeaders(event);
397
+ for (const name in reqHeaders) {
398
+ if (!ignoredHeaders.has(name)) {
399
+ headers[name] = reqHeaders[name];
400
+ }
401
+ }
402
+ if (opts.fetchOptions?.headers) {
403
+ Object.assign(headers, opts.fetchOptions.headers);
404
+ }
405
+ if (opts.headers) {
406
+ Object.assign(headers, opts.headers);
407
+ }
408
+ return sendProxy(event, target, {
409
+ ...opts,
410
+ fetchOptions: {
411
+ headers,
412
+ method,
413
+ body,
414
+ ...opts.fetchOptions
415
+ }
416
+ });
417
+ }
418
+ async function sendProxy(event, target, opts = {}) {
419
+ const _fetch = opts.fetch || globalThis.fetch;
420
+ if (!_fetch) {
421
+ throw new Error(
422
+ "fetch is not available. Try importing `node-fetch-native/polyfill` for Node.js."
423
+ );
424
+ }
425
+ const response = await _fetch(target, {
426
+ headers: opts.headers,
427
+ ...opts.fetchOptions
428
+ });
429
+ event.node.res.statusCode = response.status;
430
+ event.node.res.statusMessage = response.statusText;
431
+ for (const [key, value] of response.headers.entries()) {
432
+ if (key === "content-encoding") {
433
+ continue;
434
+ }
435
+ if (key === "content-length") {
436
+ continue;
437
+ }
438
+ event.node.res.setHeader(key, value);
439
+ }
440
+ try {
441
+ if (response.body) {
442
+ if (opts.sendStream === false) {
443
+ const data = new Uint8Array(await response.arrayBuffer());
444
+ event.node.res.end(data);
445
+ } else {
446
+ for await (const chunk of response.body) {
447
+ event.node.res.write(chunk);
448
+ }
449
+ event.node.res.end();
450
+ }
451
+ }
452
+ } catch (error) {
453
+ event.node.res.end();
454
+ throw error;
455
+ }
456
+ }
457
+
352
458
  const defer = typeof setImmediate !== "undefined" ? setImmediate : (fn) => fn();
353
459
  function send(event, data, type) {
354
460
  if (type) {
@@ -481,101 +587,372 @@ ${header}: ${value}`;
481
587
  }
482
588
  }
483
589
 
484
- function parseCookies(event) {
485
- return parse$1(event.node.req.headers.cookie || "");
486
- }
487
- function getCookie(event, name) {
488
- return parseCookies(event)[name];
489
- }
490
- function setCookie(event, name, value, serializeOptions) {
491
- const cookieStr = serialize(name, value, {
492
- path: "/",
493
- ...serializeOptions
494
- });
495
- appendHeader(event, "Set-Cookie", cookieStr);
496
- }
497
- function deleteCookie(event, name, serializeOptions) {
498
- setCookie(event, name, "", {
499
- ...serializeOptions,
500
- maxAge: 0
501
- });
502
- }
503
-
504
- const PayloadMethods = /* @__PURE__ */ new Set(["PATCH", "POST", "PUT", "DELETE"]);
505
- const ignoredHeaders = /* @__PURE__ */ new Set([
506
- "transfer-encoding",
507
- "connection",
508
- "keep-alive",
509
- "upgrade",
510
- "expect"
511
- ]);
512
- async function proxyRequest(event, target, opts = {}) {
513
- const method = getMethod(event);
514
- let body;
515
- if (PayloadMethods.has(method)) {
516
- body = await readRawBody(event).catch(() => void 0);
590
+ const base64urlEncode = (value) => (Buffer.isBuffer(value) ? value : Buffer.from(value)).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
591
+ const defaults = {
592
+ encryption: {
593
+ saltBits: 256,
594
+ algorithm: "aes-256-cbc",
595
+ iterations: 1,
596
+ minPasswordlength: 32
597
+ },
598
+ integrity: {
599
+ saltBits: 256,
600
+ algorithm: "sha256",
601
+ iterations: 1,
602
+ minPasswordlength: 32
603
+ },
604
+ ttl: 0,
605
+ timestampSkewSec: 60,
606
+ localtimeOffsetMsec: 0
607
+ };
608
+ const clone = (options) => ({
609
+ ...options,
610
+ encryption: { ...options.encryption },
611
+ integrity: { ...options.integrity }
612
+ });
613
+ const algorithms = {
614
+ "aes-128-ctr": { keyBits: 128, ivBits: 128, name: "AES-CTR" },
615
+ "aes-256-cbc": { keyBits: 256, ivBits: 128, name: "AES-CBC" },
616
+ sha256: { keyBits: 256, name: "SHA-256" }
617
+ };
618
+ const macFormatVersion = "2";
619
+ const macPrefix = `Fe26.${macFormatVersion}`;
620
+ const randomBytes = (_crypto, size) => {
621
+ const bytes = Buffer.allocUnsafe(size);
622
+ _crypto.getRandomValues(bytes);
623
+ return bytes;
624
+ };
625
+ const randomBits = (_crypto, bits) => {
626
+ if (bits < 1) {
627
+ throw new Error("Invalid random bits count");
517
628
  }
518
- const headers = /* @__PURE__ */ Object.create(null);
519
- const reqHeaders = getRequestHeaders(event);
520
- for (const name in reqHeaders) {
521
- if (!ignoredHeaders.has(name)) {
522
- headers[name] = reqHeaders[name];
629
+ const bytes = Math.ceil(bits / 8);
630
+ return randomBytes(_crypto, bytes);
631
+ };
632
+ const pbkdf2 = async (_crypto, password, salt, iterations, keyLength, hash) => {
633
+ const textEncoder = new TextEncoder();
634
+ const passwordBuffer = textEncoder.encode(password);
635
+ const importedKey = await _crypto.subtle.importKey(
636
+ "raw",
637
+ passwordBuffer,
638
+ "PBKDF2",
639
+ false,
640
+ ["deriveBits"]
641
+ );
642
+ const saltBuffer = textEncoder.encode(salt);
643
+ const params = { name: "PBKDF2", hash, salt: saltBuffer, iterations };
644
+ const derivation = await _crypto.subtle.deriveBits(
645
+ params,
646
+ importedKey,
647
+ keyLength * 8
648
+ );
649
+ return Buffer.from(derivation);
650
+ };
651
+ const generateKey = async (_crypto, password, options) => {
652
+ if (password == null || password.length === 0) {
653
+ throw new Error("Empty password");
654
+ }
655
+ if (options == null || typeof options !== "object") {
656
+ throw new Error("Bad options");
657
+ }
658
+ if (!(options.algorithm in algorithms)) {
659
+ throw new Error(`Unknown algorithm: ${options.algorithm}`);
660
+ }
661
+ const algorithm = algorithms[options.algorithm];
662
+ const result = {};
663
+ const hmac = options.hmac ?? false;
664
+ const id = hmac ? { name: "HMAC", hash: algorithm.name } : { name: algorithm.name };
665
+ const usage = hmac ? ["sign", "verify"] : ["encrypt", "decrypt"];
666
+ if (typeof password === "string") {
667
+ if (password.length < options.minPasswordlength) {
668
+ throw new Error(
669
+ `Password string too short (min ${options.minPasswordlength} characters required)`
670
+ );
523
671
  }
672
+ let { salt = "" } = options;
673
+ if (!salt) {
674
+ const { saltBits = 0 } = options;
675
+ if (!saltBits) {
676
+ throw new Error("Missing salt and saltBits options");
677
+ }
678
+ const randomSalt = randomBits(_crypto, saltBits);
679
+ salt = randomSalt.toString("hex");
680
+ }
681
+ const derivedKey = await pbkdf2(
682
+ _crypto,
683
+ password,
684
+ salt,
685
+ options.iterations,
686
+ algorithm.keyBits / 8,
687
+ "SHA-1"
688
+ );
689
+ const importedEncryptionKey = await _crypto.subtle.importKey(
690
+ "raw",
691
+ derivedKey,
692
+ id,
693
+ false,
694
+ usage
695
+ );
696
+ result.key = importedEncryptionKey;
697
+ result.salt = salt;
698
+ } else {
699
+ if (password.length < algorithm.keyBits / 8) {
700
+ throw new Error("Key buffer (password) too small");
701
+ }
702
+ result.key = await _crypto.subtle.importKey(
703
+ "raw",
704
+ password,
705
+ id,
706
+ false,
707
+ usage
708
+ );
709
+ result.salt = "";
524
710
  }
525
- if (opts.fetchOptions?.headers) {
526
- Object.assign(headers, opts.fetchOptions.headers);
527
- }
528
- if (opts.headers) {
529
- Object.assign(headers, opts.headers);
711
+ if (options.iv) {
712
+ result.iv = options.iv;
713
+ } else if ("ivBits" in algorithm) {
714
+ result.iv = randomBits(_crypto, algorithm.ivBits);
530
715
  }
531
- return sendProxy(event, target, {
532
- ...opts,
533
- fetchOptions: {
534
- headers,
535
- method,
536
- body,
537
- ...opts.fetchOptions
716
+ return result;
717
+ };
718
+ const encrypt = async (_crypto, password, options, data) => {
719
+ const key = await generateKey(_crypto, password, options);
720
+ const textEncoder = new TextEncoder();
721
+ const textBuffer = textEncoder.encode(data);
722
+ const encrypted = await _crypto.subtle.encrypt(
723
+ { name: algorithms[options.algorithm].name, iv: key.iv },
724
+ key.key,
725
+ textBuffer
726
+ );
727
+ return { encrypted: Buffer.from(encrypted), key };
728
+ };
729
+ const decrypt = async (_crypto, password, options, data) => {
730
+ const key = await generateKey(_crypto, password, options);
731
+ const decrypted = await _crypto.subtle.decrypt(
732
+ { name: algorithms[options.algorithm].name, iv: key.iv },
733
+ key.key,
734
+ Buffer.isBuffer(data) ? data : Buffer.from(data)
735
+ );
736
+ const textDecoder = new TextDecoder();
737
+ return textDecoder.decode(decrypted);
738
+ };
739
+ const hmacWithPassword = async (_crypto, password, options, data) => {
740
+ const key = await generateKey(_crypto, password, { ...options, hmac: true });
741
+ const textEncoder = new TextEncoder();
742
+ const textBuffer = textEncoder.encode(data);
743
+ const signed = await _crypto.subtle.sign(
744
+ { name: "HMAC" },
745
+ key.key,
746
+ textBuffer
747
+ );
748
+ const digest = base64urlEncode(Buffer.from(signed));
749
+ return { digest, salt: key.salt };
750
+ };
751
+ const normalizePassword = (password) => {
752
+ if (typeof password === "object" && !Buffer.isBuffer(password)) {
753
+ if ("secret" in password) {
754
+ return {
755
+ id: password.id,
756
+ encryption: password.secret,
757
+ integrity: password.secret
758
+ };
538
759
  }
539
- });
540
- }
541
- async function sendProxy(event, target, opts = {}) {
542
- const _fetch = opts.fetch || globalThis.fetch;
543
- if (!_fetch) {
544
- throw new Error(
545
- "fetch is not available. Try importing `node-fetch-native/polyfill` for Node.js."
546
- );
760
+ return {
761
+ id: password.id,
762
+ encryption: password.encryption,
763
+ integrity: password.integrity
764
+ };
547
765
  }
548
- const response = await _fetch(target, {
549
- headers: opts.headers,
550
- ...opts.fetchOptions
551
- });
552
- event.node.res.statusCode = response.status;
553
- event.node.res.statusMessage = response.statusText;
554
- for (const [key, value] of response.headers.entries()) {
555
- if (key === "content-encoding") {
556
- continue;
766
+ return { encryption: password, integrity: password };
767
+ };
768
+ const seal = async (_crypto, object, password, options) => {
769
+ if (!password) {
770
+ throw new Error("Empty password");
771
+ }
772
+ const opts = clone(options);
773
+ const now = Date.now() + (opts.localtimeOffsetMsec || 0);
774
+ const objectString = JSON.stringify(object);
775
+ const pass = normalizePassword(password);
776
+ const { id = "" } = pass;
777
+ if (id && !/^\w+$/.test(id)) {
778
+ throw new Error("Invalid password id");
779
+ }
780
+ const { encrypted, key } = await encrypt(
781
+ _crypto,
782
+ pass.encryption,
783
+ opts.encryption,
784
+ objectString
785
+ );
786
+ const encryptedB64 = base64urlEncode(encrypted);
787
+ const iv = base64urlEncode(key.iv);
788
+ const expiration = opts.ttl ? now + opts.ttl : "";
789
+ const macBaseString = `${macPrefix}*${id}*${key.salt}*${iv}*${encryptedB64}*${expiration}`;
790
+ const mac = await hmacWithPassword(
791
+ _crypto,
792
+ pass.integrity,
793
+ opts.integrity,
794
+ macBaseString
795
+ );
796
+ const sealed = `${macBaseString}*${mac.salt}*${mac.digest}`;
797
+ return sealed;
798
+ };
799
+ const fixedTimeComparison = (a, b) => {
800
+ let mismatch = a.length === b.length ? 0 : 1;
801
+ if (mismatch) {
802
+ b = a;
803
+ }
804
+ for (let i = 0; i < a.length; i += 1) {
805
+ mismatch |= a.charCodeAt(i) ^ b.charCodeAt(i);
806
+ }
807
+ return mismatch === 0;
808
+ };
809
+ const unseal = async (_crypto, sealed, password, options) => {
810
+ if (!password) {
811
+ throw new Error("Empty password");
812
+ }
813
+ const opts = clone(options);
814
+ const now = Date.now() + (opts.localtimeOffsetMsec || 0);
815
+ const parts = sealed.split("*");
816
+ if (parts.length !== 8) {
817
+ throw new Error("Incorrect number of sealed components");
818
+ }
819
+ const prefix = parts[0];
820
+ const passwordId = parts[1];
821
+ const encryptionSalt = parts[2];
822
+ const encryptionIv = parts[3];
823
+ const encryptedB64 = parts[4];
824
+ const expiration = parts[5];
825
+ const hmacSalt = parts[6];
826
+ const hmac = parts[7];
827
+ const macBaseString = `${prefix}*${passwordId}*${encryptionSalt}*${encryptionIv}*${encryptedB64}*${expiration}`;
828
+ if (macPrefix !== prefix) {
829
+ throw new Error("Wrong mac prefix");
830
+ }
831
+ if (expiration) {
832
+ if (!/^\d+$/.test(expiration)) {
833
+ throw new Error("Invalid expiration");
557
834
  }
558
- if (key === "content-length") {
559
- continue;
835
+ const exp = Number.parseInt(expiration, 10);
836
+ if (exp <= now - opts.timestampSkewSec * 1e3) {
837
+ throw new Error("Expired seal");
560
838
  }
561
- event.node.res.setHeader(key, value);
562
839
  }
563
- try {
564
- if (response.body) {
565
- if (opts.sendStream === false) {
566
- const data = new Uint8Array(await response.arrayBuffer());
567
- event.node.res.end(data);
568
- } else {
569
- for await (const chunk of response.body) {
570
- event.node.res.write(chunk);
571
- }
572
- event.node.res.end();
573
- }
840
+ if (typeof password === "undefined" || typeof password === "string" && password.length === 0) {
841
+ throw new Error("Empty password");
842
+ }
843
+ let pass;
844
+ if (typeof password === "object" && !Buffer.isBuffer(password)) {
845
+ if (!((passwordId || "default") in password)) {
846
+ throw new Error(`Cannot find password: ${passwordId}`);
574
847
  }
575
- } catch (error) {
576
- event.node.res.end();
577
- throw error;
848
+ pass = password[passwordId || "default"];
849
+ } else {
850
+ pass = password;
851
+ }
852
+ pass = normalizePassword(pass);
853
+ const macOptions = opts.integrity;
854
+ macOptions.salt = hmacSalt;
855
+ const mac = await hmacWithPassword(
856
+ _crypto,
857
+ pass.integrity,
858
+ macOptions,
859
+ macBaseString
860
+ );
861
+ if (!fixedTimeComparison(mac.digest, hmac)) {
862
+ throw new Error("Bad hmac value");
863
+ }
864
+ const encrypted = Buffer.from(encryptedB64, "base64");
865
+ const decryptOptions = opts.encryption;
866
+ decryptOptions.salt = encryptionSalt;
867
+ decryptOptions.iv = Buffer.from(encryptionIv, "base64");
868
+ const decrypted = await decrypt(
869
+ _crypto,
870
+ pass.encryption,
871
+ decryptOptions,
872
+ encrypted
873
+ );
874
+ if (decrypted) {
875
+ return JSON.parse(decrypted);
876
+ }
877
+ return null;
878
+ };
879
+
880
+ const DEFAULT_NAME = "h3";
881
+ const DEFAULT_COOKIE = {
882
+ path: "/",
883
+ secure: true,
884
+ httpOnly: true
885
+ };
886
+ async function useSession(event, config) {
887
+ const sessionName = config.name || DEFAULT_NAME;
888
+ await getSession(event, config);
889
+ const sessionManager = {
890
+ get id() {
891
+ return event.context.sessions?.[sessionName]?.id;
892
+ },
893
+ get data() {
894
+ return event.context.sessions?.[sessionName]?.data || {};
895
+ },
896
+ update: async (update) => {
897
+ await updateSession(event, config, update);
898
+ return sessionManager;
899
+ },
900
+ clear: async () => {
901
+ await clearSession(event, config);
902
+ return sessionManager;
903
+ }
904
+ };
905
+ return sessionManager;
906
+ }
907
+ async function getSession(event, config) {
908
+ const sessionName = config.name || DEFAULT_NAME;
909
+ if (!event.context.sessions) {
910
+ event.context.sessions = /* @__PURE__ */ Object.create(null);
911
+ }
912
+ if (event.context.sessions[sessionName]) {
913
+ return event.context.sessions[sessionName];
914
+ }
915
+ const session = { id: "", data: /* @__PURE__ */ Object.create(null) };
916
+ event.context.sessions[sessionName] = session;
917
+ const reqCookie = getCookie(event, sessionName);
918
+ if (!reqCookie) {
919
+ session.id = (config.crypto || crypto).randomUUID();
920
+ await updateSession(event, config);
921
+ } else {
922
+ const unsealed = await unseal(
923
+ config.crypto || crypto,
924
+ reqCookie,
925
+ config.password,
926
+ config.seal || defaults
927
+ );
928
+ Object.assign(session, unsealed);
929
+ }
930
+ return session;
931
+ }
932
+ async function updateSession(event, config, update) {
933
+ const sessionName = config.name || DEFAULT_NAME;
934
+ const session = event.context.sessions?.[sessionName] || await getSession(event, config);
935
+ if (typeof update === "function") {
936
+ update = update(session.data);
937
+ }
938
+ if (update) {
939
+ Object.assign(session.data, update);
940
+ }
941
+ const sealed = await seal(
942
+ config.crypto || crypto,
943
+ session,
944
+ config.password,
945
+ config.seal || defaults
946
+ );
947
+ setCookie(event, sessionName, sealed, config.cookie || DEFAULT_COOKIE);
948
+ return session;
949
+ }
950
+ async function clearSession(event, config) {
951
+ const sessionName = config.name || DEFAULT_NAME;
952
+ if (event.context.sessions?.[sessionName]) {
953
+ delete event.context.sessions[sessionName];
578
954
  }
955
+ await setCookie(event, sessionName, "", config.cookie || DEFAULT_COOKIE);
579
956
  }
580
957
 
581
958
  class H3Headers {
@@ -1027,4 +1404,4 @@ function createRouter(opts = {}) {
1027
1404
  return router;
1028
1405
  }
1029
1406
 
1030
- export { H3Error, H3Event, H3Headers, H3Response, MIMES, appendHeader, appendHeaders, appendResponseHeader, appendResponseHeaders, assertMethod, callNodeListener, createApp, createAppEventHandler, createError, createEvent, createRouter, defaultContentType, defineEventHandler, defineLazyEventHandler, defineNodeListener, defineNodeMiddleware, deleteCookie, dynamicEventHandler, eventHandler, fromNodeMiddleware, getCookie, getHeader, getHeaders, getMethod, getQuery, getRequestHeader, getRequestHeaders, getResponseHeader, getResponseHeaders, getResponseStatus, getResponseStatusText, getRouterParam, getRouterParams, 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, use, useBase, writeEarlyHints };
1407
+ 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, fromNodeMiddleware, getCookie, getHeader, getHeaders, getMethod, 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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "h3",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
4
4
  "description": "Tiny JavaScript Server",
5
5
  "repository": "unjs/h3",
6
6
  "license": "MIT",
@@ -23,18 +23,19 @@
23
23
  "cookie-es": "^0.5.0",
24
24
  "destr": "^1.2.2",
25
25
  "radix3": "^1.0.0",
26
- "ufo": "^1.0.1"
26
+ "ufo": "^1.0.1",
27
+ "uncrypto": "^0.1.2"
27
28
  },
28
29
  "devDependencies": {
29
30
  "0x": "^5.4.1",
30
31
  "@types/express": "^4.17.16",
31
32
  "@types/node": "^18.11.18",
32
33
  "@types/supertest": "^2.0.12",
33
- "@vitest/coverage-c8": "^0.28.2",
34
+ "@vitest/coverage-c8": "^0.28.3",
34
35
  "autocannon": "^7.10.0",
35
36
  "changelogen": "^0.4.1",
36
37
  "connect": "^3.7.0",
37
- "eslint": "^8.32.0",
38
+ "eslint": "^8.33.0",
38
39
  "eslint-config-unjs": "^0.1.0",
39
40
  "express": "^4.18.2",
40
41
  "get-port": "^6.1.2",
@@ -43,11 +44,11 @@
43
44
  "node-fetch-native": "^1.0.1",
44
45
  "prettier": "^2.8.3",
45
46
  "supertest": "^6.3.3",
46
- "typescript": "^4.9.4",
47
+ "typescript": "^4.9.5",
47
48
  "unbuild": "^1.1.1",
48
- "vitest": "^0.28.2"
49
+ "vitest": "^0.28.3"
49
50
  },
50
- "packageManager": "pnpm@7.26.0",
51
+ "packageManager": "pnpm@7.26.3",
51
52
  "scripts": {
52
53
  "build": "unbuild",
53
54
  "dev": "vitest",