h3 1.2.1 → 1.4.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 +8 -4
- package/dist/index.cjs +112 -335
- package/dist/index.d.ts +20 -52
- package/dist/index.mjs +109 -336
- package/package.json +7 -6
package/README.md
CHANGED
|
@@ -148,15 +148,19 @@ H3 has a concept of composable utilities that accept `event` (from `eventHandler
|
|
|
148
148
|
- `createError({ statusCode, statusMessage, data? })`
|
|
149
149
|
- `sendProxy(event, { target, headers?, fetchOptions?, fetch?, sendStream? })`
|
|
150
150
|
- `proxyRequest(event, { target, headers?, fetchOptions?, fetch?, sendStream? })`
|
|
151
|
+
- `fetchWithEvent(event, req, init, { fetch? }?)`
|
|
152
|
+
- `getProxyRequestHeaders(event)`
|
|
151
153
|
- `sendNoContent(event, code = 204)`
|
|
152
154
|
- `setResponseStatus(event, status)`
|
|
153
155
|
- `getResponseStatus(event)`
|
|
154
156
|
- `getResponseStatusText(event)`
|
|
155
157
|
- `readMultipartFormData(event)`
|
|
156
|
-
- `useSession(event, { password, name?, cookie?, seal?, crypto? })`
|
|
157
|
-
- `getSession(event,
|
|
158
|
-
- `updateSession(event,
|
|
159
|
-
- `clearSession(event,
|
|
158
|
+
- `useSession(event, config = { password, maxAge?, name?, cookie?, seal?, crypto? })`
|
|
159
|
+
- `getSession(event, config)`
|
|
160
|
+
- `updateSession(event, config, update)`
|
|
161
|
+
- `clearSession(event, config)`
|
|
162
|
+
- `sealSession(event, config)`
|
|
163
|
+
- `unsealSession(event, config, sealed)`
|
|
160
164
|
|
|
161
165
|
👉 You can learn more about usage in [JSDocs Documentation](https://www.jsdocs.io/package/h3#package-functions).
|
|
162
166
|
|
package/dist/index.cjs
CHANGED
|
@@ -5,6 +5,7 @@ const radix3 = require('radix3');
|
|
|
5
5
|
const destr = require('destr');
|
|
6
6
|
const cookieEs = require('cookie-es');
|
|
7
7
|
const crypto = require('uncrypto');
|
|
8
|
+
const ironWebcrypto = require('iron-webcrypto');
|
|
8
9
|
|
|
9
10
|
function useBase(base, handler) {
|
|
10
11
|
base = ufo.withoutTrailingSlash(base);
|
|
@@ -394,13 +395,7 @@ async function proxyRequest(event, target, opts = {}) {
|
|
|
394
395
|
if (PayloadMethods.has(method)) {
|
|
395
396
|
body = await readRawBody(event).catch(() => void 0);
|
|
396
397
|
}
|
|
397
|
-
const headers =
|
|
398
|
-
const reqHeaders = getRequestHeaders(event);
|
|
399
|
-
for (const name in reqHeaders) {
|
|
400
|
-
if (!ignoredHeaders.has(name)) {
|
|
401
|
-
headers[name] = reqHeaders[name];
|
|
402
|
-
}
|
|
403
|
-
}
|
|
398
|
+
const headers = getProxyRequestHeaders(event);
|
|
404
399
|
if (opts.fetchOptions?.headers) {
|
|
405
400
|
Object.assign(headers, opts.fetchOptions.headers);
|
|
406
401
|
}
|
|
@@ -418,13 +413,7 @@ async function proxyRequest(event, target, opts = {}) {
|
|
|
418
413
|
});
|
|
419
414
|
}
|
|
420
415
|
async function sendProxy(event, target, opts = {}) {
|
|
421
|
-
const
|
|
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, {
|
|
416
|
+
const response = await _getFetch(opts.fetch)(target, {
|
|
428
417
|
headers: opts.headers,
|
|
429
418
|
...opts.fetchOptions
|
|
430
419
|
});
|
|
@@ -439,22 +428,49 @@ async function sendProxy(event, target, opts = {}) {
|
|
|
439
428
|
}
|
|
440
429
|
event.node.res.setHeader(key, value);
|
|
441
430
|
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
431
|
+
if (response._data !== void 0) {
|
|
432
|
+
return response._data;
|
|
433
|
+
}
|
|
434
|
+
if (opts.sendStream === false) {
|
|
435
|
+
const data = new Uint8Array(await response.arrayBuffer());
|
|
436
|
+
return event.node.res.end(data);
|
|
437
|
+
}
|
|
438
|
+
for await (const chunk of response.body) {
|
|
439
|
+
event.node.res.write(chunk);
|
|
440
|
+
}
|
|
441
|
+
return event.node.res.end();
|
|
442
|
+
}
|
|
443
|
+
function getProxyRequestHeaders(event) {
|
|
444
|
+
const headers = /* @__PURE__ */ Object.create(null);
|
|
445
|
+
const reqHeaders = getRequestHeaders(event);
|
|
446
|
+
for (const name in reqHeaders) {
|
|
447
|
+
if (!ignoredHeaders.has(name)) {
|
|
448
|
+
headers[name] = reqHeaders[name];
|
|
453
449
|
}
|
|
454
|
-
} catch (error) {
|
|
455
|
-
event.node.res.end();
|
|
456
|
-
throw error;
|
|
457
450
|
}
|
|
451
|
+
return headers;
|
|
452
|
+
}
|
|
453
|
+
function fetchWithEvent(event, req, init, options) {
|
|
454
|
+
return _getFetch(options?.fetch)(req, {
|
|
455
|
+
...init,
|
|
456
|
+
// @ts-ignore (context is used for unenv and local fetch)
|
|
457
|
+
context: init.context || event.context,
|
|
458
|
+
headers: {
|
|
459
|
+
...getProxyRequestHeaders(event),
|
|
460
|
+
...init?.headers
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
function _getFetch(_fetch) {
|
|
465
|
+
if (_fetch) {
|
|
466
|
+
return _fetch;
|
|
467
|
+
}
|
|
468
|
+
if (globalThis.fetch) {
|
|
469
|
+
return globalThis.fetch;
|
|
470
|
+
}
|
|
471
|
+
throw new Error(
|
|
472
|
+
"fetch is not available. Try importing `node-fetch-native/polyfill` for Node.js."
|
|
473
|
+
);
|
|
458
474
|
}
|
|
459
475
|
|
|
460
476
|
const defer = typeof setImmediate !== "undefined" ? setImmediate : (fn) => fn();
|
|
@@ -589,296 +605,6 @@ ${header}: ${value}`;
|
|
|
589
605
|
}
|
|
590
606
|
}
|
|
591
607
|
|
|
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");
|
|
630
|
-
}
|
|
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
|
-
);
|
|
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 = "";
|
|
712
|
-
}
|
|
713
|
-
if (options.iv) {
|
|
714
|
-
result.iv = options.iv;
|
|
715
|
-
} else if ("ivBits" in algorithm) {
|
|
716
|
-
result.iv = randomBits(_crypto, algorithm.ivBits);
|
|
717
|
-
}
|
|
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
|
-
};
|
|
761
|
-
}
|
|
762
|
-
return {
|
|
763
|
-
id: password.id,
|
|
764
|
-
encryption: password.encryption,
|
|
765
|
-
integrity: password.integrity
|
|
766
|
-
};
|
|
767
|
-
}
|
|
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");
|
|
836
|
-
}
|
|
837
|
-
const exp = Number.parseInt(expiration, 10);
|
|
838
|
-
if (exp <= now - opts.timestampSkewSec * 1e3) {
|
|
839
|
-
throw new Error("Expired seal");
|
|
840
|
-
}
|
|
841
|
-
}
|
|
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}`);
|
|
849
|
-
}
|
|
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
608
|
const DEFAULT_NAME = "h3";
|
|
883
609
|
const DEFAULT_COOKIE = {
|
|
884
610
|
path: "/",
|
|
@@ -914,21 +640,35 @@ async function getSession(event, config) {
|
|
|
914
640
|
if (event.context.sessions[sessionName]) {
|
|
915
641
|
return event.context.sessions[sessionName];
|
|
916
642
|
}
|
|
917
|
-
const session = {
|
|
643
|
+
const session = {
|
|
644
|
+
id: "",
|
|
645
|
+
createdAt: 0,
|
|
646
|
+
data: /* @__PURE__ */ Object.create(null)
|
|
647
|
+
};
|
|
918
648
|
event.context.sessions[sessionName] = session;
|
|
919
|
-
|
|
920
|
-
if (
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
649
|
+
let sealedSession;
|
|
650
|
+
if (config.sessionHeader !== false) {
|
|
651
|
+
const headerName = typeof config.sessionHeader === "string" ? config.sessionHeader.toLowerCase() : `x-${sessionName.toLowerCase()}-session`;
|
|
652
|
+
const headerValue = event.node.req.headers[headerName];
|
|
653
|
+
if (typeof headerValue === "string") {
|
|
654
|
+
sealedSession = headerValue;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
if (!sealedSession) {
|
|
658
|
+
sealedSession = getCookie(event, sessionName);
|
|
659
|
+
}
|
|
660
|
+
if (sealedSession) {
|
|
661
|
+
const unsealed = await unsealSession(event, config, sealedSession).catch(
|
|
662
|
+
() => {
|
|
663
|
+
}
|
|
929
664
|
);
|
|
930
665
|
Object.assign(session, unsealed);
|
|
931
666
|
}
|
|
667
|
+
if (!session.id) {
|
|
668
|
+
session.id = (config.crypto || crypto).randomUUID();
|
|
669
|
+
session.createdAt = Date.now();
|
|
670
|
+
await updateSession(event, config);
|
|
671
|
+
}
|
|
932
672
|
return session;
|
|
933
673
|
}
|
|
934
674
|
async function updateSession(event, config, update) {
|
|
@@ -940,21 +680,54 @@ async function updateSession(event, config, update) {
|
|
|
940
680
|
if (update) {
|
|
941
681
|
Object.assign(session.data, update);
|
|
942
682
|
}
|
|
943
|
-
|
|
683
|
+
if (config.cookie !== false) {
|
|
684
|
+
const sealed = await sealSession(event, config);
|
|
685
|
+
setCookie(event, sessionName, sealed, {
|
|
686
|
+
...DEFAULT_COOKIE,
|
|
687
|
+
expires: config.maxAge ? new Date(session.createdAt + config.maxAge * 1e3) : void 0,
|
|
688
|
+
...config.cookie
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
return session;
|
|
692
|
+
}
|
|
693
|
+
async function sealSession(event, config) {
|
|
694
|
+
const sessionName = config.name || DEFAULT_NAME;
|
|
695
|
+
const session = event.context.sessions?.[sessionName] || await getSession(event, config);
|
|
696
|
+
const sealed = await ironWebcrypto.seal(config.crypto || crypto, session, config.password, {
|
|
697
|
+
...ironWebcrypto.defaults,
|
|
698
|
+
ttl: config.maxAge ? config.maxAge * 1e3 : 0,
|
|
699
|
+
...config.seal
|
|
700
|
+
});
|
|
701
|
+
return sealed;
|
|
702
|
+
}
|
|
703
|
+
async function unsealSession(_event, config, sealed) {
|
|
704
|
+
const unsealed = await ironWebcrypto.unseal(
|
|
944
705
|
config.crypto || crypto,
|
|
945
|
-
|
|
706
|
+
sealed,
|
|
946
707
|
config.password,
|
|
947
|
-
|
|
708
|
+
{
|
|
709
|
+
...ironWebcrypto.defaults,
|
|
710
|
+
ttl: config.maxAge ? config.maxAge * 1e3 : 0,
|
|
711
|
+
...config.seal
|
|
712
|
+
}
|
|
948
713
|
);
|
|
949
|
-
|
|
950
|
-
|
|
714
|
+
if (config.maxAge) {
|
|
715
|
+
const age = Date.now() - (unsealed.createdAt || Number.NEGATIVE_INFINITY);
|
|
716
|
+
if (age > config.maxAge * 1e3) {
|
|
717
|
+
throw new Error("Session expired!");
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
return unsealed;
|
|
951
721
|
}
|
|
952
722
|
async function clearSession(event, config) {
|
|
953
723
|
const sessionName = config.name || DEFAULT_NAME;
|
|
954
724
|
if (event.context.sessions?.[sessionName]) {
|
|
955
725
|
delete event.context.sessions[sessionName];
|
|
956
726
|
}
|
|
957
|
-
await setCookie(event, sessionName, "",
|
|
727
|
+
await setCookie(event, sessionName, "", {
|
|
728
|
+
...DEFAULT_COOKIE,
|
|
729
|
+
...config.cookie
|
|
730
|
+
});
|
|
958
731
|
}
|
|
959
732
|
|
|
960
733
|
class H3Headers {
|
|
@@ -1431,11 +1204,13 @@ exports.defineNodeMiddleware = defineNodeMiddleware;
|
|
|
1431
1204
|
exports.deleteCookie = deleteCookie;
|
|
1432
1205
|
exports.dynamicEventHandler = dynamicEventHandler;
|
|
1433
1206
|
exports.eventHandler = eventHandler;
|
|
1207
|
+
exports.fetchWithEvent = fetchWithEvent;
|
|
1434
1208
|
exports.fromNodeMiddleware = fromNodeMiddleware;
|
|
1435
1209
|
exports.getCookie = getCookie;
|
|
1436
1210
|
exports.getHeader = getHeader;
|
|
1437
1211
|
exports.getHeaders = getHeaders;
|
|
1438
1212
|
exports.getMethod = getMethod;
|
|
1213
|
+
exports.getProxyRequestHeaders = getProxyRequestHeaders;
|
|
1439
1214
|
exports.getQuery = getQuery;
|
|
1440
1215
|
exports.getRequestHeader = getRequestHeader;
|
|
1441
1216
|
exports.getRequestHeaders = getRequestHeaders;
|
|
@@ -1459,6 +1234,7 @@ exports.proxyRequest = proxyRequest;
|
|
|
1459
1234
|
exports.readBody = readBody;
|
|
1460
1235
|
exports.readMultipartFormData = readMultipartFormData;
|
|
1461
1236
|
exports.readRawBody = readRawBody;
|
|
1237
|
+
exports.sealSession = sealSession;
|
|
1462
1238
|
exports.send = send;
|
|
1463
1239
|
exports.sendError = sendError;
|
|
1464
1240
|
exports.sendNoContent = sendNoContent;
|
|
@@ -1473,6 +1249,7 @@ exports.setResponseHeaders = setResponseHeaders;
|
|
|
1473
1249
|
exports.setResponseStatus = setResponseStatus;
|
|
1474
1250
|
exports.toEventHandler = toEventHandler;
|
|
1475
1251
|
exports.toNodeListener = toNodeListener;
|
|
1252
|
+
exports.unsealSession = unsealSession;
|
|
1476
1253
|
exports.updateSession = updateSession;
|
|
1477
1254
|
exports.use = use;
|
|
1478
1255
|
exports.useBase = useBase;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,65 +1,27 @@
|
|
|
1
1
|
import { CookieSerializeOptions } from 'cookie-es';
|
|
2
|
+
import { SealOptions } from 'iron-webcrypto';
|
|
2
3
|
import { IncomingMessage, ServerResponse, OutgoingMessage } from 'node:http';
|
|
3
4
|
export { IncomingMessage as NodeIncomingMessage, ServerResponse as NodeServerResponse } from 'node:http';
|
|
4
5
|
import * as ufo from 'ufo';
|
|
5
6
|
|
|
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>;
|
|
7
|
+
type SessionDataT = Record<string, any>;
|
|
54
8
|
type SessionData<T extends SessionDataT = SessionDataT> = T;
|
|
55
9
|
interface Session<T extends SessionDataT = SessionDataT> {
|
|
56
10
|
id: string;
|
|
11
|
+
createdAt: number;
|
|
57
12
|
data: SessionData<T>;
|
|
58
13
|
}
|
|
59
14
|
interface SessionConfig {
|
|
15
|
+
/** Private key used to encrypt session tokens */
|
|
60
16
|
password: string;
|
|
17
|
+
/** Session expiration time in seconds */
|
|
18
|
+
maxAge?: number;
|
|
19
|
+
/** default is h3 */
|
|
61
20
|
name?: string;
|
|
62
|
-
|
|
21
|
+
/** Default is secure, httpOnly, / */
|
|
22
|
+
cookie?: false | CookieSerializeOptions;
|
|
23
|
+
/** Default is x-h3-session / x-{name}-session */
|
|
24
|
+
sessionHeader?: false | string;
|
|
63
25
|
seal?: SealOptions;
|
|
64
26
|
crypto?: Crypto;
|
|
65
27
|
}
|
|
@@ -72,6 +34,8 @@ declare function useSession<T extends SessionDataT = SessionDataT>(event: H3Even
|
|
|
72
34
|
declare function getSession<T extends SessionDataT = SessionDataT>(event: H3Event, config: SessionConfig): Promise<Session<T>>;
|
|
73
35
|
type SessionUpdate<T extends SessionDataT = SessionDataT> = Partial<SessionData<T>> | ((oldData: SessionData<T>) => Partial<SessionData<T>> | undefined);
|
|
74
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>>>;
|
|
75
39
|
declare function clearSession(event: H3Event, config: SessionConfig): Promise<void>;
|
|
76
40
|
|
|
77
41
|
type HTTPMethod = "GET" | "HEAD" | "PATCH" | "POST" | "PUT" | "DELETE" | "CONNECT" | "OPTIONS" | "TRACE";
|
|
@@ -338,8 +302,12 @@ interface ProxyOptions {
|
|
|
338
302
|
fetch?: typeof fetch;
|
|
339
303
|
sendStream?: boolean;
|
|
340
304
|
}
|
|
341
|
-
declare function proxyRequest(event: H3Event, target: string, opts?: ProxyOptions): Promise<
|
|
342
|
-
declare function sendProxy(event: H3Event, target: string, opts?: ProxyOptions): Promise<
|
|
305
|
+
declare function proxyRequest(event: H3Event, target: string, opts?: ProxyOptions): Promise<any>;
|
|
306
|
+
declare function sendProxy(event: H3Event, target: string, opts?: ProxyOptions): Promise<any>;
|
|
307
|
+
declare function getProxyRequestHeaders(event: H3Event): any;
|
|
308
|
+
declare function fetchWithEvent(event: H3Event, req: RequestInfo | URL, init?: RequestInit, options?: {
|
|
309
|
+
fetch: typeof fetch;
|
|
310
|
+
}): Promise<Response>;
|
|
343
311
|
|
|
344
312
|
declare function getQuery(event: H3Event): ufo.QueryObject;
|
|
345
313
|
declare function getRouterParams(event: H3Event): H3Event["context"];
|
|
@@ -395,4 +363,4 @@ interface CreateRouterOptions {
|
|
|
395
363
|
}
|
|
396
364
|
declare function createRouter(opts?: CreateRouterOptions): Router;
|
|
397
365
|
|
|
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 };
|
|
366
|
+
export { AddRouteShortcuts, App, AppOptions, AppUse, CacheConditions, CreateRouterOptions, DynamicEventHandler, Encoding, EventHandler, EventHandlerResponse, H3Error, H3Event, H3EventContext, H3Headers, H3Response, HTTPMethod, InputLayer, InputStack, Layer, LazyEventHandler, MIMES, Matcher, NodeEventContext, NodeListener, NodeMiddleware, NodePromisifiedHandler, ProxyOptions, RequestHeaders, Router, RouterMethod, RouterUse, Session, SessionConfig, SessionData, Stack, appendHeader, appendHeaders, appendResponseHeader, appendResponseHeaders, assertMethod, callNodeListener, clearSession, createApp, createAppEventHandler, createError, createEvent, createRouter, defaultContentType, defineEventHandler, defineLazyEventHandler, defineNodeListener, defineNodeMiddleware, deleteCookie, dynamicEventHandler, eventHandler, fetchWithEvent, fromNodeMiddleware, getCookie, getHeader, getHeaders, getMethod, getProxyRequestHeaders, getQuery, getRequestHeader, getRequestHeaders, getResponseHeader, getResponseHeaders, getResponseStatus, getResponseStatusText, getRouterParam, getRouterParams, getSession, handleCacheHeaders, isError, isEvent, isEventHandler, isMethod, isStream, lazyEventHandler, parseCookies, promisifyNodeListener, proxyRequest, readBody, readMultipartFormData, readRawBody, sealSession, send, sendError, sendNoContent, sendProxy, sendRedirect, sendStream, setCookie, setHeader, setHeaders, setResponseHeader, setResponseHeaders, setResponseStatus, toEventHandler, toNodeListener, unsealSession, updateSession, use, useBase, useSession, writeEarlyHints };
|
package/dist/index.mjs
CHANGED
|
@@ -3,6 +3,7 @@ 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 { seal, defaults, unseal } from 'iron-webcrypto';
|
|
6
7
|
|
|
7
8
|
function useBase(base, handler) {
|
|
8
9
|
base = withoutTrailingSlash(base);
|
|
@@ -392,13 +393,7 @@ async function proxyRequest(event, target, opts = {}) {
|
|
|
392
393
|
if (PayloadMethods.has(method)) {
|
|
393
394
|
body = await readRawBody(event).catch(() => void 0);
|
|
394
395
|
}
|
|
395
|
-
const headers =
|
|
396
|
-
const reqHeaders = getRequestHeaders(event);
|
|
397
|
-
for (const name in reqHeaders) {
|
|
398
|
-
if (!ignoredHeaders.has(name)) {
|
|
399
|
-
headers[name] = reqHeaders[name];
|
|
400
|
-
}
|
|
401
|
-
}
|
|
396
|
+
const headers = getProxyRequestHeaders(event);
|
|
402
397
|
if (opts.fetchOptions?.headers) {
|
|
403
398
|
Object.assign(headers, opts.fetchOptions.headers);
|
|
404
399
|
}
|
|
@@ -416,13 +411,7 @@ async function proxyRequest(event, target, opts = {}) {
|
|
|
416
411
|
});
|
|
417
412
|
}
|
|
418
413
|
async function sendProxy(event, target, opts = {}) {
|
|
419
|
-
const
|
|
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, {
|
|
414
|
+
const response = await _getFetch(opts.fetch)(target, {
|
|
426
415
|
headers: opts.headers,
|
|
427
416
|
...opts.fetchOptions
|
|
428
417
|
});
|
|
@@ -437,22 +426,49 @@ async function sendProxy(event, target, opts = {}) {
|
|
|
437
426
|
}
|
|
438
427
|
event.node.res.setHeader(key, value);
|
|
439
428
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
429
|
+
if (response._data !== void 0) {
|
|
430
|
+
return response._data;
|
|
431
|
+
}
|
|
432
|
+
if (opts.sendStream === false) {
|
|
433
|
+
const data = new Uint8Array(await response.arrayBuffer());
|
|
434
|
+
return event.node.res.end(data);
|
|
435
|
+
}
|
|
436
|
+
for await (const chunk of response.body) {
|
|
437
|
+
event.node.res.write(chunk);
|
|
438
|
+
}
|
|
439
|
+
return event.node.res.end();
|
|
440
|
+
}
|
|
441
|
+
function getProxyRequestHeaders(event) {
|
|
442
|
+
const headers = /* @__PURE__ */ Object.create(null);
|
|
443
|
+
const reqHeaders = getRequestHeaders(event);
|
|
444
|
+
for (const name in reqHeaders) {
|
|
445
|
+
if (!ignoredHeaders.has(name)) {
|
|
446
|
+
headers[name] = reqHeaders[name];
|
|
451
447
|
}
|
|
452
|
-
} catch (error) {
|
|
453
|
-
event.node.res.end();
|
|
454
|
-
throw error;
|
|
455
448
|
}
|
|
449
|
+
return headers;
|
|
450
|
+
}
|
|
451
|
+
function fetchWithEvent(event, req, init, options) {
|
|
452
|
+
return _getFetch(options?.fetch)(req, {
|
|
453
|
+
...init,
|
|
454
|
+
// @ts-ignore (context is used for unenv and local fetch)
|
|
455
|
+
context: init.context || event.context,
|
|
456
|
+
headers: {
|
|
457
|
+
...getProxyRequestHeaders(event),
|
|
458
|
+
...init?.headers
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
function _getFetch(_fetch) {
|
|
463
|
+
if (_fetch) {
|
|
464
|
+
return _fetch;
|
|
465
|
+
}
|
|
466
|
+
if (globalThis.fetch) {
|
|
467
|
+
return globalThis.fetch;
|
|
468
|
+
}
|
|
469
|
+
throw new Error(
|
|
470
|
+
"fetch is not available. Try importing `node-fetch-native/polyfill` for Node.js."
|
|
471
|
+
);
|
|
456
472
|
}
|
|
457
473
|
|
|
458
474
|
const defer = typeof setImmediate !== "undefined" ? setImmediate : (fn) => fn();
|
|
@@ -587,296 +603,6 @@ ${header}: ${value}`;
|
|
|
587
603
|
}
|
|
588
604
|
}
|
|
589
605
|
|
|
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");
|
|
628
|
-
}
|
|
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
|
-
);
|
|
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 = "";
|
|
710
|
-
}
|
|
711
|
-
if (options.iv) {
|
|
712
|
-
result.iv = options.iv;
|
|
713
|
-
} else if ("ivBits" in algorithm) {
|
|
714
|
-
result.iv = randomBits(_crypto, algorithm.ivBits);
|
|
715
|
-
}
|
|
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
|
-
};
|
|
759
|
-
}
|
|
760
|
-
return {
|
|
761
|
-
id: password.id,
|
|
762
|
-
encryption: password.encryption,
|
|
763
|
-
integrity: password.integrity
|
|
764
|
-
};
|
|
765
|
-
}
|
|
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");
|
|
834
|
-
}
|
|
835
|
-
const exp = Number.parseInt(expiration, 10);
|
|
836
|
-
if (exp <= now - opts.timestampSkewSec * 1e3) {
|
|
837
|
-
throw new Error("Expired seal");
|
|
838
|
-
}
|
|
839
|
-
}
|
|
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}`);
|
|
847
|
-
}
|
|
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
606
|
const DEFAULT_NAME = "h3";
|
|
881
607
|
const DEFAULT_COOKIE = {
|
|
882
608
|
path: "/",
|
|
@@ -912,21 +638,35 @@ async function getSession(event, config) {
|
|
|
912
638
|
if (event.context.sessions[sessionName]) {
|
|
913
639
|
return event.context.sessions[sessionName];
|
|
914
640
|
}
|
|
915
|
-
const session = {
|
|
641
|
+
const session = {
|
|
642
|
+
id: "",
|
|
643
|
+
createdAt: 0,
|
|
644
|
+
data: /* @__PURE__ */ Object.create(null)
|
|
645
|
+
};
|
|
916
646
|
event.context.sessions[sessionName] = session;
|
|
917
|
-
|
|
918
|
-
if (
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
647
|
+
let sealedSession;
|
|
648
|
+
if (config.sessionHeader !== false) {
|
|
649
|
+
const headerName = typeof config.sessionHeader === "string" ? config.sessionHeader.toLowerCase() : `x-${sessionName.toLowerCase()}-session`;
|
|
650
|
+
const headerValue = event.node.req.headers[headerName];
|
|
651
|
+
if (typeof headerValue === "string") {
|
|
652
|
+
sealedSession = headerValue;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
if (!sealedSession) {
|
|
656
|
+
sealedSession = getCookie(event, sessionName);
|
|
657
|
+
}
|
|
658
|
+
if (sealedSession) {
|
|
659
|
+
const unsealed = await unsealSession(event, config, sealedSession).catch(
|
|
660
|
+
() => {
|
|
661
|
+
}
|
|
927
662
|
);
|
|
928
663
|
Object.assign(session, unsealed);
|
|
929
664
|
}
|
|
665
|
+
if (!session.id) {
|
|
666
|
+
session.id = (config.crypto || crypto).randomUUID();
|
|
667
|
+
session.createdAt = Date.now();
|
|
668
|
+
await updateSession(event, config);
|
|
669
|
+
}
|
|
930
670
|
return session;
|
|
931
671
|
}
|
|
932
672
|
async function updateSession(event, config, update) {
|
|
@@ -938,21 +678,54 @@ async function updateSession(event, config, update) {
|
|
|
938
678
|
if (update) {
|
|
939
679
|
Object.assign(session.data, update);
|
|
940
680
|
}
|
|
941
|
-
|
|
681
|
+
if (config.cookie !== false) {
|
|
682
|
+
const sealed = await sealSession(event, config);
|
|
683
|
+
setCookie(event, sessionName, sealed, {
|
|
684
|
+
...DEFAULT_COOKIE,
|
|
685
|
+
expires: config.maxAge ? new Date(session.createdAt + config.maxAge * 1e3) : void 0,
|
|
686
|
+
...config.cookie
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
return session;
|
|
690
|
+
}
|
|
691
|
+
async function sealSession(event, config) {
|
|
692
|
+
const sessionName = config.name || DEFAULT_NAME;
|
|
693
|
+
const session = event.context.sessions?.[sessionName] || await getSession(event, config);
|
|
694
|
+
const sealed = await seal(config.crypto || crypto, session, config.password, {
|
|
695
|
+
...defaults,
|
|
696
|
+
ttl: config.maxAge ? config.maxAge * 1e3 : 0,
|
|
697
|
+
...config.seal
|
|
698
|
+
});
|
|
699
|
+
return sealed;
|
|
700
|
+
}
|
|
701
|
+
async function unsealSession(_event, config, sealed) {
|
|
702
|
+
const unsealed = await unseal(
|
|
942
703
|
config.crypto || crypto,
|
|
943
|
-
|
|
704
|
+
sealed,
|
|
944
705
|
config.password,
|
|
945
|
-
|
|
706
|
+
{
|
|
707
|
+
...defaults,
|
|
708
|
+
ttl: config.maxAge ? config.maxAge * 1e3 : 0,
|
|
709
|
+
...config.seal
|
|
710
|
+
}
|
|
946
711
|
);
|
|
947
|
-
|
|
948
|
-
|
|
712
|
+
if (config.maxAge) {
|
|
713
|
+
const age = Date.now() - (unsealed.createdAt || Number.NEGATIVE_INFINITY);
|
|
714
|
+
if (age > config.maxAge * 1e3) {
|
|
715
|
+
throw new Error("Session expired!");
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
return unsealed;
|
|
949
719
|
}
|
|
950
720
|
async function clearSession(event, config) {
|
|
951
721
|
const sessionName = config.name || DEFAULT_NAME;
|
|
952
722
|
if (event.context.sessions?.[sessionName]) {
|
|
953
723
|
delete event.context.sessions[sessionName];
|
|
954
724
|
}
|
|
955
|
-
await setCookie(event, sessionName, "",
|
|
725
|
+
await setCookie(event, sessionName, "", {
|
|
726
|
+
...DEFAULT_COOKIE,
|
|
727
|
+
...config.cookie
|
|
728
|
+
});
|
|
956
729
|
}
|
|
957
730
|
|
|
958
731
|
class H3Headers {
|
|
@@ -1404,4 +1177,4 @@ function createRouter(opts = {}) {
|
|
|
1404
1177
|
return router;
|
|
1405
1178
|
}
|
|
1406
1179
|
|
|
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 };
|
|
1180
|
+
export { H3Error, H3Event, H3Headers, H3Response, MIMES, appendHeader, appendHeaders, appendResponseHeader, appendResponseHeaders, assertMethod, callNodeListener, clearSession, createApp, createAppEventHandler, createError, createEvent, createRouter, defaultContentType, defineEventHandler, defineLazyEventHandler, defineNodeListener, defineNodeMiddleware, deleteCookie, dynamicEventHandler, eventHandler, fetchWithEvent, fromNodeMiddleware, getCookie, getHeader, getHeaders, getMethod, getProxyRequestHeaders, getQuery, getRequestHeader, getRequestHeaders, getResponseHeader, getResponseHeaders, getResponseStatus, getResponseStatusText, getRouterParam, getRouterParams, getSession, handleCacheHeaders, isError, isEvent, isEventHandler, isMethod, isStream, lazyEventHandler, parseCookies, promisifyNodeListener, proxyRequest, readBody, readMultipartFormData, readRawBody, sealSession, send, sendError, sendNoContent, sendProxy, sendRedirect, sendStream, setCookie, setHeader, setHeaders, setResponseHeader, setResponseHeaders, setResponseStatus, toEventHandler, toNodeListener, unsealSession, updateSession, use, useBase, useSession, writeEarlyHints };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "h3",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Tiny JavaScript Server",
|
|
5
5
|
"repository": "unjs/h3",
|
|
6
6
|
"license": "MIT",
|
|
@@ -22,16 +22,17 @@
|
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"cookie-es": "^0.5.0",
|
|
24
24
|
"destr": "^1.2.2",
|
|
25
|
+
"iron-webcrypto": "^0.4.0",
|
|
25
26
|
"radix3": "^1.0.0",
|
|
26
27
|
"ufo": "^1.0.1",
|
|
27
28
|
"uncrypto": "^0.1.2"
|
|
28
29
|
},
|
|
29
30
|
"devDependencies": {
|
|
30
31
|
"0x": "^5.4.1",
|
|
31
|
-
"@types/express": "^4.17.
|
|
32
|
-
"@types/node": "^18.
|
|
32
|
+
"@types/express": "^4.17.17",
|
|
33
|
+
"@types/node": "^18.13.0",
|
|
33
34
|
"@types/supertest": "^2.0.12",
|
|
34
|
-
"@vitest/coverage-c8": "^0.28.
|
|
35
|
+
"@vitest/coverage-c8": "^0.28.4",
|
|
35
36
|
"autocannon": "^7.10.0",
|
|
36
37
|
"changelogen": "^0.4.1",
|
|
37
38
|
"connect": "^3.7.0",
|
|
@@ -42,11 +43,11 @@
|
|
|
42
43
|
"jiti": "^1.16.2",
|
|
43
44
|
"listhen": "^1.0.2",
|
|
44
45
|
"node-fetch-native": "^1.0.1",
|
|
45
|
-
"prettier": "^2.8.
|
|
46
|
+
"prettier": "^2.8.4",
|
|
46
47
|
"supertest": "^6.3.3",
|
|
47
48
|
"typescript": "^4.9.5",
|
|
48
49
|
"unbuild": "^1.1.1",
|
|
49
|
-
"vitest": "^0.28.
|
|
50
|
+
"vitest": "^0.28.4"
|
|
50
51
|
},
|
|
51
52
|
"packageManager": "pnpm@7.26.3",
|
|
52
53
|
"scripts": {
|