unjwt 0.5.23 → 0.5.25
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 +119 -4
- package/dist/{h3.d.mts → adapters/h3.d.mts} +6 -6
- package/dist/{h3.mjs → adapters/h3.mjs} +8 -8
- package/dist/adapters/h3v1.d.mts +7 -0
- package/dist/adapters/h3v1.mjs +7 -0
- package/dist/adapters/h3v2.d.mts +129 -0
- package/dist/adapters/h3v2.mjs +522 -0
- package/package.json +19 -6
package/README.md
CHANGED
|
@@ -603,18 +603,18 @@ console.log(rsaPublicSpki);
|
|
|
603
603
|
|
|
604
604
|
### Adapters
|
|
605
605
|
|
|
606
|
-
#### H3 (Nuxt, Nitro)
|
|
606
|
+
#### H3 v1 (Nuxt v4, Nitro v2)
|
|
607
607
|
|
|
608
|
-
The `
|
|
608
|
+
The `h3v1` adapter bundles session helpers that store data inside signed or encrypted JWTs.
|
|
609
609
|
|
|
610
610
|
- `useJWESession(event, config)` encrypts the session payload with the provided `secret` (password string or private/symmetric JWK). Use this when session data must remain confidential. (cookie's `httpOnly: true` by default)
|
|
611
611
|
- `useJWSSession(event, config)` signs, but does not encrypt, the session payload with `config.key`. Use this when clients may read the session content but you still need tamper protection. (cookie's `httpOnly: false` by default)
|
|
612
612
|
|
|
613
|
-
Both helpers expose the same API: read `session.id` / `session.data`, call `session.update()` to patch values, and `session.clear()` to invalidate the cookie. When setting `maxAge`, `exp` claim is automatically managed and validated.
|
|
613
|
+
Both helpers expose the same API: read `session.id` / `session.data`, call `session.update()` to patch values, and `session.clear()` to invalidate the cookie. When setting `maxAge`, `exp` claim is automatically managed and validated. They also support a number of `hooks` in the config for custom logic (e.g. logging, refreshing, etc).
|
|
614
614
|
|
|
615
615
|
```ts
|
|
616
616
|
import { defineEventHandler } from "h3";
|
|
617
|
-
import { useJWESession, useJWSSession, generateJWK } from "unjwt/adapters/
|
|
617
|
+
import { useJWESession, useJWSSession, generateJWK } from "unjwt/adapters/h3v1";
|
|
618
618
|
|
|
619
619
|
const keys = await generateJWK("RS256");
|
|
620
620
|
|
|
@@ -641,6 +641,121 @@ export default defineEventHandler(async (event) => {
|
|
|
641
641
|
});
|
|
642
642
|
```
|
|
643
643
|
|
|
644
|
+
#### H3 v2 (Nuxt v5, Nitro v3)
|
|
645
|
+
|
|
646
|
+
The `h3v2` adapter bundles session helpers that store data inside signed or encrypted JWTs.
|
|
647
|
+
|
|
648
|
+
- `useJWESession(event, config)` encrypts the session payload with the provided `secret` (password string or private/symmetric JWK). Use this when session data must remain confidential. (cookie's `httpOnly: true` by default)
|
|
649
|
+
- `useJWSSession(event, config)` signs, but does not encrypt, the session payload with `config.key`. Use this when clients may read the session content but you still need tamper protection. (cookie's `httpOnly: false` by default)
|
|
650
|
+
|
|
651
|
+
Both helpers expose the same API: read `session.id` / `session.data`, call `session.update()` to patch values, and `session.clear()` to invalidate the cookie. When setting `maxAge`, `exp` claim is automatically managed and validated. They also support a number of `hooks` in the config for custom logic (e.g. logging, refreshing, etc).
|
|
652
|
+
|
|
653
|
+
```ts
|
|
654
|
+
import { H3, HTTPError, serve } from "h3v2";
|
|
655
|
+
|
|
656
|
+
import {
|
|
657
|
+
type SessionConfigJWE,
|
|
658
|
+
type SessionConfigJWS,
|
|
659
|
+
useJWESession,
|
|
660
|
+
useJWSSession,
|
|
661
|
+
getJWESession,
|
|
662
|
+
updateJWSSession,
|
|
663
|
+
generateJWK,
|
|
664
|
+
} from "unjwt/adapters/h3v2";
|
|
665
|
+
|
|
666
|
+
const atJwk = await generateJWK("RS256");
|
|
667
|
+
|
|
668
|
+
const jweOptions = {
|
|
669
|
+
key: "refresh_token_secret",
|
|
670
|
+
name: "refresh_token",
|
|
671
|
+
} satisfies SessionConfigJWE;
|
|
672
|
+
|
|
673
|
+
const jwsOptions = {
|
|
674
|
+
key: atJwk,
|
|
675
|
+
name: "access_token",
|
|
676
|
+
maxAge: 15 * 60, // 15 minutes
|
|
677
|
+
hooks: {
|
|
678
|
+
async onExpire(event, _error, config) {
|
|
679
|
+
const refreshSession = await getJWESession(event, jweOptions);
|
|
680
|
+
if (!refreshSession.data.sub) {
|
|
681
|
+
// no valid refresh session, nothing to do
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
console.log("Access token expired, refreshing...");
|
|
686
|
+
|
|
687
|
+
// refresh the access token
|
|
688
|
+
await updateJWSSession(event, config, {
|
|
689
|
+
sub: refreshSession.data.sub,
|
|
690
|
+
scope: refreshSession.data.scope,
|
|
691
|
+
});
|
|
692
|
+
},
|
|
693
|
+
},
|
|
694
|
+
} satisfies SessionConfigJWS;
|
|
695
|
+
|
|
696
|
+
const app = new H3();
|
|
697
|
+
|
|
698
|
+
app.post("/login", async (event) => {
|
|
699
|
+
const refreshSession = await useJWESession(event, jweOptions);
|
|
700
|
+
const accessSession = await useJWSSession(event, jwsOptions);
|
|
701
|
+
|
|
702
|
+
if (accessSession.data.sub) {
|
|
703
|
+
// user already logged in, return existing info
|
|
704
|
+
return {
|
|
705
|
+
accessToken: {
|
|
706
|
+
id: accessSession.id,
|
|
707
|
+
createdAt: accessSession.createdAt,
|
|
708
|
+
expiresAt: accessSession.expiresAt,
|
|
709
|
+
data: accessSession.data,
|
|
710
|
+
},
|
|
711
|
+
refreshSession: {
|
|
712
|
+
id: refreshSession.id,
|
|
713
|
+
createdAt: refreshSession.createdAt,
|
|
714
|
+
expiresAt: refreshSession.expiresAt,
|
|
715
|
+
data: refreshSession.data,
|
|
716
|
+
},
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
const data = (await event.req.json()) as {
|
|
721
|
+
username?: string;
|
|
722
|
+
password?: string;
|
|
723
|
+
};
|
|
724
|
+
|
|
725
|
+
if (!data.username || !data.password) {
|
|
726
|
+
throw new HTTPError("Username and password are required", { status: 400 });
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// validate user credentials here
|
|
730
|
+
|
|
731
|
+
const claims = {
|
|
732
|
+
sub: data.username,
|
|
733
|
+
scope: ["read:profile"],
|
|
734
|
+
};
|
|
735
|
+
|
|
736
|
+
await accessSession.update(claims);
|
|
737
|
+
|
|
738
|
+
await refreshSession.update(claims);
|
|
739
|
+
|
|
740
|
+
return {
|
|
741
|
+
accessToken: {
|
|
742
|
+
id: accessSession.id,
|
|
743
|
+
createdAt: accessSession.createdAt,
|
|
744
|
+
expiresAt: accessSession.expiresAt,
|
|
745
|
+
data: accessSession.data,
|
|
746
|
+
},
|
|
747
|
+
refreshSession: {
|
|
748
|
+
id: refreshSession.id,
|
|
749
|
+
createdAt: refreshSession.createdAt,
|
|
750
|
+
expiresAt: refreshSession.expiresAt,
|
|
751
|
+
data: refreshSession.data,
|
|
752
|
+
},
|
|
753
|
+
};
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
serve(app);
|
|
757
|
+
```
|
|
758
|
+
|
|
644
759
|
---
|
|
645
760
|
|
|
646
761
|
### Utility Functions
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { b as JWEEncryptOptions } from '
|
|
2
|
-
export { d as JWEDecryptOptions, e as JWEDecryptResult, J as JWEHeaderParameters, c as JWEKeyLookupFunction, a as JWEKeyManagementHeaderParameters } from '
|
|
3
|
-
import { X as JWTClaims, r as JWK_Symmetric, u as JWK_Private, t as JWK_Public, Z as JWTClaimValidationOptions, w as JWKSet } from '
|
|
4
|
-
export { C as CompositeKey, f as ContentEncryptionAlgorithm, D as DeriveKeyOptions, e as DeriveKeyReturn, c as GenerateJWKOptions, d as GenerateJWKReturn, G as GenerateKeyAlgorithm, a as GenerateKeyOptions, b as GenerateKeyReturn, v as JWK, S as JWKAlgorithm, T as JWKPEMAlgorithm, J as JWKParameters, I as JWK_AES_CBC_HMAC, L as JWK_AES_GCM, H as JWK_AES_GCM_KW, F as JWK_AES_KW, s as JWK_Asymmetric, R as JWK_Asymmetric_Algorithm, j as JWK_EC, O as JWK_ECDH_ES, A as JWK_ECDSA, i as JWK_EC_Private, h as JWK_EC_Public, x as JWK_HMAC, p as JWK_OKP, o as JWK_OKP_Private, n as JWK_OKP_Public, P as JWK_OKP_SIGN, N as JWK_PBES2, m as JWK_RSA, B as JWK_RSA_ENC, z as JWK_RSA_PSS, l as JWK_RSA_Private, k as JWK_RSA_Public, y as JWK_RSA_SIGN, Q as JWK_Symmetric_Algorithm, q as JWK_oct, Y as JWT, V as JoseHeaderParameters, K as KeyManagementAlgorithm, U as UnwrapKeyOptions, W as WrapKeyOptions, g as WrapKeyResult } from '
|
|
1
|
+
import { b as JWEEncryptOptions } from '../shared/unjwt.9mL1vSFP.mjs';
|
|
2
|
+
export { d as JWEDecryptOptions, e as JWEDecryptResult, J as JWEHeaderParameters, c as JWEKeyLookupFunction, a as JWEKeyManagementHeaderParameters } from '../shared/unjwt.9mL1vSFP.mjs';
|
|
3
|
+
import { X as JWTClaims, r as JWK_Symmetric, u as JWK_Private, t as JWK_Public, Z as JWTClaimValidationOptions, w as JWKSet } from '../shared/unjwt.CtHrClHc.mjs';
|
|
4
|
+
export { C as CompositeKey, f as ContentEncryptionAlgorithm, D as DeriveKeyOptions, e as DeriveKeyReturn, c as GenerateJWKOptions, d as GenerateJWKReturn, G as GenerateKeyAlgorithm, a as GenerateKeyOptions, b as GenerateKeyReturn, v as JWK, S as JWKAlgorithm, T as JWKPEMAlgorithm, J as JWKParameters, I as JWK_AES_CBC_HMAC, L as JWK_AES_GCM, H as JWK_AES_GCM_KW, F as JWK_AES_KW, s as JWK_Asymmetric, R as JWK_Asymmetric_Algorithm, j as JWK_EC, O as JWK_ECDH_ES, A as JWK_ECDSA, i as JWK_EC_Private, h as JWK_EC_Public, x as JWK_HMAC, p as JWK_OKP, o as JWK_OKP_Private, n as JWK_OKP_Public, P as JWK_OKP_SIGN, N as JWK_PBES2, m as JWK_RSA, B as JWK_RSA_ENC, z as JWK_RSA_PSS, l as JWK_RSA_Private, k as JWK_RSA_Public, y as JWK_RSA_SIGN, Q as JWK_Symmetric_Algorithm, q as JWK_oct, Y as JWT, V as JoseHeaderParameters, K as KeyManagementAlgorithm, U as UnwrapKeyOptions, W as WrapKeyOptions, g as WrapKeyResult } from '../shared/unjwt.CtHrClHc.mjs';
|
|
5
5
|
import { CookieSerializeOptions } from 'cookie-es';
|
|
6
6
|
import { H3Event } from 'h3';
|
|
7
|
-
import { c as JWSSignOptions } from '
|
|
8
|
-
export { d as deriveJWKFromPassword, e as exportJWKToPEM, g as generateJWK, i as importJWKFromPEM } from '
|
|
7
|
+
import { c as JWSSignOptions } from '../shared/unjwt.BsLgL0DQ.mjs';
|
|
8
|
+
export { d as deriveJWKFromPassword, e as exportJWKToPEM, g as generateJWK, i as importJWKFromPEM } from '../shared/unjwt.BjP_dGnX.mjs';
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* This is a fork of h3 library's session utility functions.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
export { k as deriveJWKFromPassword, h as exportJWKToPEM, c as generateJWK, f as importJWKFromPEM } from '
|
|
1
|
+
export { k as deriveJWKFromPassword, h as exportJWKToPEM, c as generateJWK, f as importJWKFromPEM } from '../shared/unjwt.epPK1KFr.mjs';
|
|
2
2
|
import { isEvent, setCookie } from 'h3';
|
|
3
3
|
import { parse } from 'cookie-es';
|
|
4
|
-
import { e as encrypt, d as decrypt } from '
|
|
5
|
-
import { p as isSymmetricJWK, q as isPrivateJWK, u as isPublicJWK } from '
|
|
6
|
-
import { s as sign, v as verify } from '
|
|
4
|
+
import { e as encrypt, d as decrypt } from '../shared/unjwt.CiBEuvtQ.mjs';
|
|
5
|
+
import { p as isSymmetricJWK, q as isPrivateJWK, u as isPublicJWK } from '../shared/unjwt.C86hA0o-.mjs';
|
|
6
|
+
import { s as sign, v as verify } from '../shared/unjwt.D-FATaw1.mjs';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* This is a fork of h3 library's session utility functions.
|
|
@@ -215,12 +215,12 @@ async function unsealJWESession(_event, config, sealed) {
|
|
|
215
215
|
}
|
|
216
216
|
throw new Error(error_);
|
|
217
217
|
});
|
|
218
|
-
const { jti, iat, ...data } = payload;
|
|
218
|
+
const { jti, iat, exp, ...data } = payload;
|
|
219
219
|
return {
|
|
220
220
|
id: jti,
|
|
221
221
|
createdAt: iat * 1e3,
|
|
222
222
|
// Convert back to ms
|
|
223
|
-
expiresAt:
|
|
223
|
+
expiresAt: exp ? exp * 1e3 : void 0,
|
|
224
224
|
data: data && typeof data === "object" ? data : /* @__PURE__ */ Object.create(null)
|
|
225
225
|
};
|
|
226
226
|
}
|
|
@@ -481,12 +481,12 @@ async function verifyJWSSession(_event, config, token) {
|
|
|
481
481
|
const message = error_ instanceof Error ? error_.message : String(error_);
|
|
482
482
|
throw new Error(`Invalid session token: ${message}`);
|
|
483
483
|
});
|
|
484
|
-
const { jti, iat, ...data } = payload;
|
|
484
|
+
const { jti, iat, exp, ...data } = payload;
|
|
485
485
|
return {
|
|
486
486
|
id: jti,
|
|
487
487
|
createdAt: iat * 1e3,
|
|
488
488
|
// Convert back to ms
|
|
489
|
-
expiresAt:
|
|
489
|
+
expiresAt: exp ? exp * 1e3 : void 0,
|
|
490
490
|
data: data && typeof data === "object" ? data : /* @__PURE__ */ Object.create(null)
|
|
491
491
|
};
|
|
492
492
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { d as JWEDecryptOptions, e as JWEDecryptResult, b as JWEEncryptOptions, J as JWEHeaderParameters, c as JWEKeyLookupFunction, a as JWEKeyManagementHeaderParameters } from '../shared/unjwt.9mL1vSFP.mjs';
|
|
2
|
+
export { C as CompositeKey, f as ContentEncryptionAlgorithm, D as DeriveKeyOptions, e as DeriveKeyReturn, c as GenerateJWKOptions, d as GenerateJWKReturn, G as GenerateKeyAlgorithm, a as GenerateKeyOptions, b as GenerateKeyReturn, v as JWK, S as JWKAlgorithm, T as JWKPEMAlgorithm, J as JWKParameters, w as JWKSet, I as JWK_AES_CBC_HMAC, L as JWK_AES_GCM, H as JWK_AES_GCM_KW, F as JWK_AES_KW, s as JWK_Asymmetric, R as JWK_Asymmetric_Algorithm, j as JWK_EC, O as JWK_ECDH_ES, A as JWK_ECDSA, i as JWK_EC_Private, h as JWK_EC_Public, x as JWK_HMAC, p as JWK_OKP, o as JWK_OKP_Private, n as JWK_OKP_Public, P as JWK_OKP_SIGN, N as JWK_PBES2, u as JWK_Private, t as JWK_Public, m as JWK_RSA, B as JWK_RSA_ENC, z as JWK_RSA_PSS, l as JWK_RSA_Private, k as JWK_RSA_Public, y as JWK_RSA_SIGN, r as JWK_Symmetric, Q as JWK_Symmetric_Algorithm, q as JWK_oct, Y as JWT, Z as JWTClaimValidationOptions, X as JWTClaims, V as JoseHeaderParameters, K as KeyManagementAlgorithm, U as UnwrapKeyOptions, W as WrapKeyOptions, g as WrapKeyResult } from '../shared/unjwt.CtHrClHc.mjs';
|
|
3
|
+
export { SessionConfigJWE, SessionConfigJWS, SessionData, SessionHooksJWE, SessionHooksJWS, SessionJWE, SessionJWEConfig, SessionJWS, SessionJWSConfig, SessionManager, clearJWESession, clearJWSSession, getJWESession, getJWSSession, sealJWESession, signJWSSession, unsealJWESession, updateJWESession, updateJWSSession, useJWESession, useJWSSession, verifyJWSSession } from './h3.mjs';
|
|
4
|
+
export { d as deriveJWKFromPassword, e as exportJWKToPEM, g as generateJWK, i as importJWKFromPEM } from '../shared/unjwt.BjP_dGnX.mjs';
|
|
5
|
+
import 'cookie-es';
|
|
6
|
+
import 'h3';
|
|
7
|
+
import '../shared/unjwt.BsLgL0DQ.mjs';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { k as deriveJWKFromPassword, h as exportJWKToPEM, c as generateJWK, f as importJWKFromPEM } from '../shared/unjwt.epPK1KFr.mjs';
|
|
2
|
+
export { clearJWESession, clearJWSSession, getJWESession, getJWSSession, sealJWESession, signJWSSession, unsealJWESession, updateJWESession, updateJWSSession, useJWESession, useJWSSession, verifyJWSSession } from './h3.mjs';
|
|
3
|
+
import '../shared/unjwt.C86hA0o-.mjs';
|
|
4
|
+
import 'h3';
|
|
5
|
+
import 'cookie-es';
|
|
6
|
+
import '../shared/unjwt.CiBEuvtQ.mjs';
|
|
7
|
+
import '../shared/unjwt.D-FATaw1.mjs';
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { b as JWEEncryptOptions } from '../shared/unjwt.9mL1vSFP.mjs';
|
|
2
|
+
export { d as JWEDecryptOptions, e as JWEDecryptResult, J as JWEHeaderParameters, c as JWEKeyLookupFunction, a as JWEKeyManagementHeaderParameters } from '../shared/unjwt.9mL1vSFP.mjs';
|
|
3
|
+
import { X as JWTClaims, r as JWK_Symmetric, u as JWK_Private, t as JWK_Public, Z as JWTClaimValidationOptions, w as JWKSet } from '../shared/unjwt.CtHrClHc.mjs';
|
|
4
|
+
export { C as CompositeKey, f as ContentEncryptionAlgorithm, D as DeriveKeyOptions, e as DeriveKeyReturn, c as GenerateJWKOptions, d as GenerateJWKReturn, G as GenerateKeyAlgorithm, a as GenerateKeyOptions, b as GenerateKeyReturn, v as JWK, S as JWKAlgorithm, T as JWKPEMAlgorithm, J as JWKParameters, I as JWK_AES_CBC_HMAC, L as JWK_AES_GCM, H as JWK_AES_GCM_KW, F as JWK_AES_KW, s as JWK_Asymmetric, R as JWK_Asymmetric_Algorithm, j as JWK_EC, O as JWK_ECDH_ES, A as JWK_ECDSA, i as JWK_EC_Private, h as JWK_EC_Public, x as JWK_HMAC, p as JWK_OKP, o as JWK_OKP_Private, n as JWK_OKP_Public, P as JWK_OKP_SIGN, N as JWK_PBES2, m as JWK_RSA, B as JWK_RSA_ENC, z as JWK_RSA_PSS, l as JWK_RSA_Private, k as JWK_RSA_Public, y as JWK_RSA_SIGN, Q as JWK_Symmetric_Algorithm, q as JWK_oct, Y as JWT, V as JoseHeaderParameters, K as KeyManagementAlgorithm, U as UnwrapKeyOptions, W as WrapKeyOptions, g as WrapKeyResult } from '../shared/unjwt.CtHrClHc.mjs';
|
|
5
|
+
import { HTTPEvent } from 'h3';
|
|
6
|
+
import { CookieSerializeOptions } from 'cookie-es';
|
|
7
|
+
import { c as JWSSignOptions } from '../shared/unjwt.BsLgL0DQ.mjs';
|
|
8
|
+
export { d as deriveJWKFromPassword, e as exportJWKToPEM, g as generateJWK, i as importJWKFromPEM } from '../shared/unjwt.BjP_dGnX.mjs';
|
|
9
|
+
|
|
10
|
+
type SessionDataT$1 = Omit<JWTClaims, "jti" | "iat" | "exp">;
|
|
11
|
+
type SessionData<T extends SessionDataT$1 = SessionDataT$1> = T;
|
|
12
|
+
declare const kGetSessionPromise$1: unique symbol;
|
|
13
|
+
interface SessionJWE<T extends SessionDataT$1 = SessionDataT$1> {
|
|
14
|
+
id: string;
|
|
15
|
+
createdAt: number;
|
|
16
|
+
expiresAt?: number;
|
|
17
|
+
data: SessionData<T>;
|
|
18
|
+
[kGetSessionPromise$1]?: Promise<SessionJWE<T>>;
|
|
19
|
+
}
|
|
20
|
+
interface SessionHooksJWE {
|
|
21
|
+
onRead?: (session: SessionJWE, event: HTTPEvent, config: SessionConfigJWE) => void | Promise<void>;
|
|
22
|
+
onUpdate?: (session: SessionJWE, event: HTTPEvent, config: SessionConfigJWE) => void | Promise<void>;
|
|
23
|
+
onClear?: (event: HTTPEvent, config: Partial<SessionConfigJWE>) => void | Promise<void>;
|
|
24
|
+
onExpire?: (event: HTTPEvent, error: any | undefined, config: SessionConfigJWE) => void | Promise<void>;
|
|
25
|
+
onError?: (event: HTTPEvent, error: any, config: SessionConfigJWE) => void | Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
interface SessionManager<T extends SessionDataT$1 = SessionDataT$1> {
|
|
28
|
+
readonly id: string | undefined;
|
|
29
|
+
readonly createdAt: number;
|
|
30
|
+
readonly expiresAt: number | undefined;
|
|
31
|
+
readonly data: SessionData<T>;
|
|
32
|
+
update: (update: SessionUpdate$1<T>) => Promise<SessionManager<T>>;
|
|
33
|
+
clear: () => Promise<SessionManager<T>>;
|
|
34
|
+
}
|
|
35
|
+
interface SessionConfigJWE {
|
|
36
|
+
/** Shared secret used, string for PBES2 or Json Web Key (JWK) */
|
|
37
|
+
key: string | JWK_Symmetric | {
|
|
38
|
+
privateKey: JWK_Private | JWK_Symmetric;
|
|
39
|
+
publicKey?: JWK_Public;
|
|
40
|
+
};
|
|
41
|
+
/** Session lifetime in seconds (used to derive exp from iat) */
|
|
42
|
+
maxAge?: number;
|
|
43
|
+
/** Default is "h3" */
|
|
44
|
+
name?: string;
|
|
45
|
+
/** Default is secure, httpOnly, path="/" */
|
|
46
|
+
cookie?: false | (CookieSerializeOptions & {
|
|
47
|
+
chunkMaxLength?: number;
|
|
48
|
+
});
|
|
49
|
+
/** Default is x-h3-session / x-{name}-session */
|
|
50
|
+
sessionHeader?: false | string;
|
|
51
|
+
/** Default is crypto.randomUUID */
|
|
52
|
+
generateId?: () => string;
|
|
53
|
+
/** JWE configuration overrides */
|
|
54
|
+
jwe?: {
|
|
55
|
+
encryptOptions?: Omit<JWEEncryptOptions, "expiresIn">;
|
|
56
|
+
decryptOptions?: JWTClaimValidationOptions;
|
|
57
|
+
};
|
|
58
|
+
hooks?: SessionHooksJWE;
|
|
59
|
+
}
|
|
60
|
+
type SessionUpdate$1<T extends SessionDataT$1 = SessionDataT$1> = Partial<SessionData<T>> | ((oldData: SessionData<T>) => Partial<SessionData<T>> | undefined);
|
|
61
|
+
/**
|
|
62
|
+
* Create a session manager for the current request.
|
|
63
|
+
*/
|
|
64
|
+
declare function useJWESession<T extends SessionDataT$1 = SessionDataT$1>(event: HTTPEvent, config: SessionConfigJWE): Promise<SessionManager<T>>;
|
|
65
|
+
/**
|
|
66
|
+
* Get the session for the current request.
|
|
67
|
+
*/
|
|
68
|
+
declare function getJWESession<T extends SessionDataT$1 = SessionDataT$1>(event: HTTPEvent, config: SessionConfigJWE): Promise<SessionJWE<T>>;
|
|
69
|
+
/**
|
|
70
|
+
* Update the session data for the current request.
|
|
71
|
+
*/
|
|
72
|
+
declare function updateJWESession<T extends SessionDataT$1 = SessionDataT$1>(event: HTTPEvent, config: SessionConfigJWE, update?: SessionUpdate$1<T>): Promise<SessionJWE<T>>;
|
|
73
|
+
/**
|
|
74
|
+
* Produce a JWE for the current session.
|
|
75
|
+
*/
|
|
76
|
+
declare function sealJWESession<T extends SessionDataT$1 = SessionDataT$1>(event: HTTPEvent, config: SessionConfigJWE): Promise<string>;
|
|
77
|
+
/**
|
|
78
|
+
* Decrypt the JWE and return a Session-compatible object.
|
|
79
|
+
*/
|
|
80
|
+
declare function unsealJWESession(_event: HTTPEvent, config: SessionConfigJWE, sealed: string): Promise<Partial<SessionJWE>>;
|
|
81
|
+
/**
|
|
82
|
+
* Clear the session (delete from context and drop cookie).
|
|
83
|
+
*/
|
|
84
|
+
declare function clearJWESession(event: HTTPEvent, config: Partial<SessionConfigJWE>): Promise<void>;
|
|
85
|
+
|
|
86
|
+
type SessionDataT = Omit<JWTClaims, "jti" | "iat" | "exp">;
|
|
87
|
+
declare const kGetSessionPromise: unique symbol;
|
|
88
|
+
interface SessionJWS<T extends SessionDataT = SessionDataT> {
|
|
89
|
+
id: string;
|
|
90
|
+
createdAt: number;
|
|
91
|
+
expiresAt?: number;
|
|
92
|
+
data: SessionData<T>;
|
|
93
|
+
[kGetSessionPromise]?: Promise<SessionJWS<T>>;
|
|
94
|
+
}
|
|
95
|
+
interface SessionHooksJWS {
|
|
96
|
+
onRead?: (session: SessionJWS, event: HTTPEvent, config: SessionConfigJWS) => void | Promise<void>;
|
|
97
|
+
onUpdate?: (session: SessionJWS, event: HTTPEvent, config: SessionConfigJWS) => void | Promise<void>;
|
|
98
|
+
onClear?: (event: HTTPEvent, config: Partial<SessionConfigJWS>) => void | Promise<void>;
|
|
99
|
+
onExpire?: (event: HTTPEvent, error: any | undefined, config: SessionConfigJWS) => void | Promise<void>;
|
|
100
|
+
onError?: (event: HTTPEvent, error: any, config: SessionConfigJWS) => void | Promise<void>;
|
|
101
|
+
}
|
|
102
|
+
interface SessionConfigJWS {
|
|
103
|
+
key: JWK_Symmetric | {
|
|
104
|
+
privateKey: JWK_Private;
|
|
105
|
+
publicKey: JWK_Public | JWK_Public[] | JWKSet;
|
|
106
|
+
};
|
|
107
|
+
maxAge?: number;
|
|
108
|
+
name?: string;
|
|
109
|
+
cookie?: false | (CookieSerializeOptions & {
|
|
110
|
+
chunkMaxLength?: number;
|
|
111
|
+
});
|
|
112
|
+
sessionHeader?: false | string;
|
|
113
|
+
generateId?: () => string;
|
|
114
|
+
jws?: {
|
|
115
|
+
signOptions?: Omit<JWSSignOptions, "expiresIn">;
|
|
116
|
+
verifyOptions?: JWTClaimValidationOptions;
|
|
117
|
+
};
|
|
118
|
+
hooks?: SessionHooksJWS;
|
|
119
|
+
}
|
|
120
|
+
type SessionUpdate<T extends SessionDataT = SessionDataT> = Partial<SessionData<T>> | ((oldData: SessionData<T>) => Partial<SessionData<T>> | undefined);
|
|
121
|
+
declare function useJWSSession<T extends SessionDataT = SessionDataT>(event: HTTPEvent, config: SessionConfigJWS): Promise<SessionManager<T>>;
|
|
122
|
+
declare function getJWSSession<T extends SessionDataT = SessionDataT>(event: HTTPEvent, config: SessionConfigJWS): Promise<SessionJWS<T>>;
|
|
123
|
+
declare function updateJWSSession<T extends SessionDataT = SessionDataT>(event: HTTPEvent, config: SessionConfigJWS, update?: SessionUpdate<T>): Promise<SessionJWS<T>>;
|
|
124
|
+
declare function signJWSSession<T extends SessionDataT = SessionDataT>(event: HTTPEvent, config: SessionConfigJWS): Promise<string>;
|
|
125
|
+
declare function verifyJWSSession(_event: HTTPEvent, config: SessionConfigJWS, token: string): Promise<Partial<SessionJWS>>;
|
|
126
|
+
declare function clearJWSSession(event: HTTPEvent, config: Partial<SessionConfigJWS>): Promise<void>;
|
|
127
|
+
|
|
128
|
+
export { JWEEncryptOptions, JWKSet, JWK_Private, JWK_Public, JWK_Symmetric, JWTClaimValidationOptions, JWTClaims, clearJWESession, clearJWSSession, getJWESession, getJWSSession, sealJWESession, signJWSSession, unsealJWESession, updateJWESession, updateJWSSession, useJWESession, useJWSSession, verifyJWSSession };
|
|
129
|
+
export type { SessionConfigJWE, SessionConfigJWS, SessionData, SessionHooksJWE, SessionHooksJWS, SessionJWE, SessionJWS, SessionManager };
|
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
export { k as deriveJWKFromPassword, h as exportJWKToPEM, c as generateJWK, f as importJWKFromPEM } from '../shared/unjwt.epPK1KFr.mjs';
|
|
2
|
+
import { getEventContext, getChunkedCookie, setChunkedCookie } from 'h3';
|
|
3
|
+
import { NullProtoObj } from 'rou3';
|
|
4
|
+
import { e as encrypt, d as decrypt } from '../shared/unjwt.CiBEuvtQ.mjs';
|
|
5
|
+
import { p as isSymmetricJWK, q as isPrivateJWK, u as isPublicJWK } from '../shared/unjwt.C86hA0o-.mjs';
|
|
6
|
+
import { s as sign, v as verify } from '../shared/unjwt.D-FATaw1.mjs';
|
|
7
|
+
|
|
8
|
+
const kGetSessionPromise$1 = Symbol("h3_jwe_getSession");
|
|
9
|
+
const DEFAULT_NAME$1 = "h3-jwe";
|
|
10
|
+
const DEFAULT_COOKIE$1 = {
|
|
11
|
+
path: "/",
|
|
12
|
+
secure: true,
|
|
13
|
+
httpOnly: true
|
|
14
|
+
};
|
|
15
|
+
async function useJWESession(event, config) {
|
|
16
|
+
const sessionName = config.name || DEFAULT_NAME$1;
|
|
17
|
+
await getJWESession(event, config);
|
|
18
|
+
const sessionManager = {
|
|
19
|
+
get id() {
|
|
20
|
+
return getSessionFromContext$1(event, sessionName)?.id;
|
|
21
|
+
},
|
|
22
|
+
get createdAt() {
|
|
23
|
+
return getSessionFromContext$1(event, sessionName)?.createdAt ?? Date.now();
|
|
24
|
+
},
|
|
25
|
+
get expiresAt() {
|
|
26
|
+
return getSessionFromContext$1(event, sessionName)?.expiresAt;
|
|
27
|
+
},
|
|
28
|
+
get data() {
|
|
29
|
+
return getSessionFromContext$1(event, sessionName)?.data || {};
|
|
30
|
+
},
|
|
31
|
+
update: async (update) => {
|
|
32
|
+
await updateJWESession(event, config, update);
|
|
33
|
+
return sessionManager;
|
|
34
|
+
},
|
|
35
|
+
clear: async () => {
|
|
36
|
+
await clearJWESession(event, config);
|
|
37
|
+
return sessionManager;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
return sessionManager;
|
|
41
|
+
}
|
|
42
|
+
async function getJWESession(event, config) {
|
|
43
|
+
const sessionName = config.name || DEFAULT_NAME$1;
|
|
44
|
+
const context = getEventContext(event);
|
|
45
|
+
if (!context.sessions) {
|
|
46
|
+
context.sessions = new NullProtoObj();
|
|
47
|
+
}
|
|
48
|
+
const existingSession = context.sessions[sessionName];
|
|
49
|
+
if (existingSession) {
|
|
50
|
+
const session2 = existingSession[kGetSessionPromise$1] ? await existingSession[kGetSessionPromise$1] : existingSession;
|
|
51
|
+
if (session2.expiresAt !== void 0 && session2.expiresAt < Date.now() && hasWritableResponse$1(event)) {
|
|
52
|
+
await config.hooks?.onExpire?.(event, void 0, config);
|
|
53
|
+
return clearJWESession(event, config).then(
|
|
54
|
+
() => getJWESession(event, config)
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
await config.hooks?.onRead?.(session2, event, config);
|
|
58
|
+
return session2;
|
|
59
|
+
}
|
|
60
|
+
const session = {
|
|
61
|
+
id: "",
|
|
62
|
+
createdAt: 0,
|
|
63
|
+
expiresAt: void 0,
|
|
64
|
+
data: new NullProtoObj()
|
|
65
|
+
};
|
|
66
|
+
context.sessions[sessionName] = session;
|
|
67
|
+
let jweToken;
|
|
68
|
+
if (config.sessionHeader !== false) {
|
|
69
|
+
const headerName = typeof config.sessionHeader === "string" ? config.sessionHeader.toLowerCase() : `x-${sessionName.toLowerCase()}-session`;
|
|
70
|
+
const headerValue = event.req.headers.get(headerName);
|
|
71
|
+
if (typeof headerValue === "string") {
|
|
72
|
+
jweToken = headerValue;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (!jweToken) {
|
|
76
|
+
jweToken = getChunkedCookie(event, sessionName);
|
|
77
|
+
}
|
|
78
|
+
if (jweToken) {
|
|
79
|
+
const promise = unsealJWESession(event, config, jweToken).catch(async (error_) => {
|
|
80
|
+
if (error_ instanceof Error) {
|
|
81
|
+
const message = error_.message;
|
|
82
|
+
if (message.includes("Token has expired") || message.includes("Token is too old")) {
|
|
83
|
+
await config.hooks?.onExpire?.(event, error_, config);
|
|
84
|
+
return void 0;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
await config.hooks?.onError?.(event, error_, config);
|
|
88
|
+
return void 0;
|
|
89
|
+
}).then((unsealed) => {
|
|
90
|
+
if (unsealed) {
|
|
91
|
+
Object.assign(session, unsealed);
|
|
92
|
+
}
|
|
93
|
+
delete session[kGetSessionPromise$1];
|
|
94
|
+
return session;
|
|
95
|
+
});
|
|
96
|
+
session[kGetSessionPromise$1] = promise;
|
|
97
|
+
await promise;
|
|
98
|
+
}
|
|
99
|
+
if (!session.id) {
|
|
100
|
+
session.id = config.generateId?.() ?? crypto.randomUUID();
|
|
101
|
+
session.createdAt = config.jwe?.encryptOptions?.currentDate?.getTime() ?? Date.now();
|
|
102
|
+
session.expiresAt = config.maxAge ? session.createdAt + config.maxAge * 1e3 : void 0;
|
|
103
|
+
await updateJWESession(event, config);
|
|
104
|
+
}
|
|
105
|
+
await config.hooks?.onRead?.(session, event, config);
|
|
106
|
+
return session;
|
|
107
|
+
}
|
|
108
|
+
async function updateJWESession(event, config, update) {
|
|
109
|
+
const sessionName = config.name || DEFAULT_NAME$1;
|
|
110
|
+
const context = getEventContext(event);
|
|
111
|
+
const session = context.sessions?.[sessionName] || await getJWESession(event, config);
|
|
112
|
+
if (typeof update === "function") {
|
|
113
|
+
update = update(session.data);
|
|
114
|
+
}
|
|
115
|
+
if (update) {
|
|
116
|
+
Object.assign(session.data, update);
|
|
117
|
+
await config.hooks?.onUpdate?.(session, event, config);
|
|
118
|
+
}
|
|
119
|
+
if (config.cookie !== false && hasWritableResponse$1(event)) {
|
|
120
|
+
const sealed = await sealJWESession(event, config);
|
|
121
|
+
setChunkedCookie(event, sessionName, sealed, {
|
|
122
|
+
...DEFAULT_COOKIE$1,
|
|
123
|
+
expires: config.maxAge ? new Date(session.createdAt + config.maxAge * 1e3) : void 0,
|
|
124
|
+
...config.cookie
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
return session;
|
|
128
|
+
}
|
|
129
|
+
async function sealJWESession(event, config) {
|
|
130
|
+
const key = getEncryptKey(config.key);
|
|
131
|
+
const sessionName = config.name || DEFAULT_NAME$1;
|
|
132
|
+
const context = getEventContext(event);
|
|
133
|
+
const session = context.sessions?.[sessionName] || await getJWESession(event, config);
|
|
134
|
+
const iatSeconds = Math.floor(session.createdAt / 1e3);
|
|
135
|
+
const expSeconds = config.maxAge == null ? void 0 : iatSeconds + config.maxAge;
|
|
136
|
+
const payload = {
|
|
137
|
+
...session.data,
|
|
138
|
+
jti: session.id,
|
|
139
|
+
iat: iatSeconds
|
|
140
|
+
};
|
|
141
|
+
if (expSeconds) {
|
|
142
|
+
payload.exp = expSeconds;
|
|
143
|
+
}
|
|
144
|
+
let typ = void 0;
|
|
145
|
+
if (config.jwe?.encryptOptions?.protectedHeader?.typ && typeof config.jwe.encryptOptions.protectedHeader.typ === "string" && config.jwe.encryptOptions.protectedHeader.typ.toLowerCase().includes("jwt")) {
|
|
146
|
+
typ = config.jwe.encryptOptions.protectedHeader.typ;
|
|
147
|
+
}
|
|
148
|
+
const token = await encrypt(payload, key, {
|
|
149
|
+
...config.jwe?.encryptOptions,
|
|
150
|
+
expiresIn: void 0,
|
|
151
|
+
// controlled via 'exp' claim
|
|
152
|
+
protectedHeader: {
|
|
153
|
+
...config.jwe?.encryptOptions?.protectedHeader,
|
|
154
|
+
typ: typ || "JWT",
|
|
155
|
+
cty: "application/json"
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
return token;
|
|
159
|
+
}
|
|
160
|
+
async function unsealJWESession(_event, config, sealed) {
|
|
161
|
+
const key = getDecryptKey(config.key);
|
|
162
|
+
const alg = config.jwe?.encryptOptions?.alg;
|
|
163
|
+
const enc = config.jwe?.encryptOptions?.enc;
|
|
164
|
+
if (config.jwe?.encryptOptions?.protectedHeader?.typ && typeof config.jwe.encryptOptions.protectedHeader.typ === "string" && config.jwe.encryptOptions.protectedHeader.typ.toLowerCase().includes("jwt")) {
|
|
165
|
+
config.jwe.encryptOptions.protectedHeader.typ;
|
|
166
|
+
}
|
|
167
|
+
const { payload } = await decrypt(sealed, key, {
|
|
168
|
+
...config.jwe?.decryptOptions,
|
|
169
|
+
requiredClaims: [
|
|
170
|
+
...config.jwe?.decryptOptions?.requiredClaims?.filter(
|
|
171
|
+
(claim) => claim !== "jti" && claim !== "iat"
|
|
172
|
+
) || [],
|
|
173
|
+
"jti",
|
|
174
|
+
"iat"
|
|
175
|
+
],
|
|
176
|
+
maxTokenAge: config.maxAge,
|
|
177
|
+
algorithms: alg ? [alg] : void 0,
|
|
178
|
+
encryptionAlgorithms: enc ? [enc] : void 0,
|
|
179
|
+
unwrappedKeyAlgorithm: void 0,
|
|
180
|
+
keyUsage: void 0,
|
|
181
|
+
forceUint8Array: false,
|
|
182
|
+
validateJWT: true
|
|
183
|
+
}).catch((error_) => {
|
|
184
|
+
if (error_ instanceof Error) {
|
|
185
|
+
throw error_;
|
|
186
|
+
}
|
|
187
|
+
throw new Error(String(error_));
|
|
188
|
+
});
|
|
189
|
+
const { jti, iat, exp, ...data } = payload;
|
|
190
|
+
return {
|
|
191
|
+
id: jti,
|
|
192
|
+
createdAt: iat * 1e3,
|
|
193
|
+
expiresAt: exp ? exp * 1e3 : void 0,
|
|
194
|
+
data: data && typeof data === "object" ? data : new NullProtoObj()
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
async function clearJWESession(event, config) {
|
|
198
|
+
const context = getEventContext(event);
|
|
199
|
+
const sessionName = config.name || DEFAULT_NAME$1;
|
|
200
|
+
if (context.sessions?.[sessionName]) {
|
|
201
|
+
delete context.sessions[sessionName];
|
|
202
|
+
}
|
|
203
|
+
if (config.cookie !== false && hasWritableResponse$1(event)) {
|
|
204
|
+
setChunkedCookie(event, sessionName, "", {
|
|
205
|
+
...DEFAULT_COOKIE$1,
|
|
206
|
+
...config.cookie,
|
|
207
|
+
expires: /* @__PURE__ */ new Date(0),
|
|
208
|
+
maxAge: void 0
|
|
209
|
+
});
|
|
210
|
+
await config.hooks?.onClear?.(event, config);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
function getSessionFromContext$1(event, sessionName) {
|
|
214
|
+
const context = getEventContext(event);
|
|
215
|
+
return context.sessions?.[sessionName];
|
|
216
|
+
}
|
|
217
|
+
function hasWritableResponse$1(event) {
|
|
218
|
+
return Boolean(event.res);
|
|
219
|
+
}
|
|
220
|
+
function getEncryptKey(key) {
|
|
221
|
+
if (!key) {
|
|
222
|
+
throw new Error("Session: JWE key is required.");
|
|
223
|
+
}
|
|
224
|
+
let _key;
|
|
225
|
+
if (typeof key === "string") {
|
|
226
|
+
_key = key;
|
|
227
|
+
} else if (isSymmetricJWK(key) || isPrivateJWK(key)) {
|
|
228
|
+
_key = key;
|
|
229
|
+
} else if ("publicKey" in key && isPublicJWK(key.publicKey)) {
|
|
230
|
+
_key = key.publicKey;
|
|
231
|
+
} else if ("privateKey" in key && isPrivateJWK(key.privateKey)) {
|
|
232
|
+
_key = key.privateKey;
|
|
233
|
+
}
|
|
234
|
+
if (!_key) {
|
|
235
|
+
throw new Error(
|
|
236
|
+
"Session: Invalid JWE key. It must be a password string or valid JWK.",
|
|
237
|
+
{ cause: key }
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
return _key;
|
|
241
|
+
}
|
|
242
|
+
function getDecryptKey(key) {
|
|
243
|
+
if (!key) {
|
|
244
|
+
throw new Error("Session: JWE key is required.");
|
|
245
|
+
}
|
|
246
|
+
let _key = void 0;
|
|
247
|
+
if (typeof key === "string") {
|
|
248
|
+
_key = key;
|
|
249
|
+
} else if (isSymmetricJWK(key)) {
|
|
250
|
+
_key = key;
|
|
251
|
+
} else if ("privateKey" in key) {
|
|
252
|
+
_key = key.privateKey;
|
|
253
|
+
}
|
|
254
|
+
if (!_key) {
|
|
255
|
+
throw new Error(
|
|
256
|
+
"Session: Invalid JWE key. It must be a password string or a valid private JWK.",
|
|
257
|
+
{ cause: key }
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
return _key;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const kGetSessionPromise = Symbol("h3_jws_getSession");
|
|
264
|
+
const DEFAULT_NAME = "h3-jws";
|
|
265
|
+
const DEFAULT_COOKIE = {
|
|
266
|
+
path: "/",
|
|
267
|
+
secure: true,
|
|
268
|
+
httpOnly: false
|
|
269
|
+
};
|
|
270
|
+
async function useJWSSession(event, config) {
|
|
271
|
+
const sessionName = config.name || DEFAULT_NAME;
|
|
272
|
+
await getJWSSession(event, config);
|
|
273
|
+
const sessionManager = {
|
|
274
|
+
get id() {
|
|
275
|
+
return getSessionFromContext(event, sessionName)?.id;
|
|
276
|
+
},
|
|
277
|
+
get createdAt() {
|
|
278
|
+
return getSessionFromContext(event, sessionName)?.createdAt ?? Date.now();
|
|
279
|
+
},
|
|
280
|
+
get expiresAt() {
|
|
281
|
+
return getSessionFromContext(event, sessionName)?.expiresAt;
|
|
282
|
+
},
|
|
283
|
+
get data() {
|
|
284
|
+
return getSessionFromContext(event, sessionName)?.data || {};
|
|
285
|
+
},
|
|
286
|
+
update: async (update) => {
|
|
287
|
+
await updateJWSSession(event, config, update);
|
|
288
|
+
return sessionManager;
|
|
289
|
+
},
|
|
290
|
+
clear: async () => {
|
|
291
|
+
await clearJWSSession(event, config);
|
|
292
|
+
return sessionManager;
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
return sessionManager;
|
|
296
|
+
}
|
|
297
|
+
async function getJWSSession(event, config) {
|
|
298
|
+
const sessionName = config.name || DEFAULT_NAME;
|
|
299
|
+
const context = getEventContext(event);
|
|
300
|
+
if (!context.sessions) {
|
|
301
|
+
context.sessions = new NullProtoObj();
|
|
302
|
+
}
|
|
303
|
+
const existingSession = context.sessions[sessionName];
|
|
304
|
+
if (existingSession) {
|
|
305
|
+
const session2 = existingSession[kGetSessionPromise] ? await existingSession[kGetSessionPromise] : existingSession;
|
|
306
|
+
if (session2.expiresAt !== void 0 && session2.expiresAt < Date.now() && hasWritableResponse(event)) {
|
|
307
|
+
await config.hooks?.onExpire?.(event, void 0, config);
|
|
308
|
+
return clearJWSSession(event, config).then(
|
|
309
|
+
() => getJWSSession(event, config)
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
await config.hooks?.onRead?.(session2, event, config);
|
|
313
|
+
return session2;
|
|
314
|
+
}
|
|
315
|
+
const session = {
|
|
316
|
+
id: "",
|
|
317
|
+
createdAt: 0,
|
|
318
|
+
expiresAt: void 0,
|
|
319
|
+
data: new NullProtoObj()
|
|
320
|
+
};
|
|
321
|
+
context.sessions[sessionName] = session;
|
|
322
|
+
let token;
|
|
323
|
+
if (config.sessionHeader !== false) {
|
|
324
|
+
const headerName = typeof config.sessionHeader === "string" ? config.sessionHeader.toLowerCase() : `x-${sessionName.toLowerCase()}-session`;
|
|
325
|
+
const headerValue = event.req.headers.get(headerName);
|
|
326
|
+
if (typeof headerValue === "string") {
|
|
327
|
+
token = headerValue;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
if (!token) {
|
|
331
|
+
token = getChunkedCookie(event, sessionName);
|
|
332
|
+
}
|
|
333
|
+
if (token) {
|
|
334
|
+
const promise = verifyJWSSession(event, config, token).catch(async (error_) => {
|
|
335
|
+
if (error_ instanceof Error && (error_.message.includes("Token has expired") || error_.message.includes("Token is too old"))) {
|
|
336
|
+
await config.hooks?.onExpire?.(event, error_, config);
|
|
337
|
+
return void 0;
|
|
338
|
+
}
|
|
339
|
+
await config.hooks?.onError?.(event, error_, config);
|
|
340
|
+
return void 0;
|
|
341
|
+
}).then((unsealed) => {
|
|
342
|
+
if (unsealed) {
|
|
343
|
+
Object.assign(session, unsealed);
|
|
344
|
+
}
|
|
345
|
+
delete session[kGetSessionPromise];
|
|
346
|
+
return session;
|
|
347
|
+
});
|
|
348
|
+
session[kGetSessionPromise] = promise;
|
|
349
|
+
await promise;
|
|
350
|
+
}
|
|
351
|
+
if (!session.id) {
|
|
352
|
+
session.id = config.generateId?.() ?? crypto.randomUUID();
|
|
353
|
+
session.createdAt = config.jws?.signOptions?.currentDate?.getTime() ?? Date.now();
|
|
354
|
+
session.expiresAt = config.maxAge ? session.createdAt + config.maxAge * 1e3 : void 0;
|
|
355
|
+
await updateJWSSession(event, config);
|
|
356
|
+
}
|
|
357
|
+
await config.hooks?.onRead?.(session, event, config);
|
|
358
|
+
return session;
|
|
359
|
+
}
|
|
360
|
+
async function updateJWSSession(event, config, update) {
|
|
361
|
+
const sessionName = config.name || DEFAULT_NAME;
|
|
362
|
+
const context = getEventContext(event);
|
|
363
|
+
const session = context.sessions?.[sessionName] || await getJWSSession(event, config);
|
|
364
|
+
if (typeof update === "function") {
|
|
365
|
+
update = update(session.data);
|
|
366
|
+
}
|
|
367
|
+
if (update) {
|
|
368
|
+
Object.assign(session.data, update);
|
|
369
|
+
await config.hooks?.onUpdate?.(session, event, config);
|
|
370
|
+
}
|
|
371
|
+
if (config.cookie !== false && hasWritableResponse(event)) {
|
|
372
|
+
const token = await signJWSSession(event, config);
|
|
373
|
+
setChunkedCookie(event, sessionName, token, {
|
|
374
|
+
...DEFAULT_COOKIE,
|
|
375
|
+
expires: config.maxAge ? new Date(session.createdAt + config.maxAge * 1e3) : void 0,
|
|
376
|
+
...config.cookie
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
return session;
|
|
380
|
+
}
|
|
381
|
+
async function signJWSSession(event, config) {
|
|
382
|
+
const key = getSignKey(config.key);
|
|
383
|
+
const sessionName = config.name || DEFAULT_NAME;
|
|
384
|
+
const context = getEventContext(event);
|
|
385
|
+
const session = context.sessions?.[sessionName] || await getJWSSession(event, config);
|
|
386
|
+
const iatSeconds = Math.floor(session.createdAt / 1e3);
|
|
387
|
+
const expSeconds = config.maxAge == null ? void 0 : iatSeconds + config.maxAge;
|
|
388
|
+
const payload = {
|
|
389
|
+
...session.data,
|
|
390
|
+
jti: session.id,
|
|
391
|
+
iat: iatSeconds
|
|
392
|
+
};
|
|
393
|
+
if (expSeconds) {
|
|
394
|
+
payload.exp = expSeconds;
|
|
395
|
+
}
|
|
396
|
+
let typ;
|
|
397
|
+
if (config.jws?.signOptions?.protectedHeader?.typ && typeof config.jws.signOptions.protectedHeader.typ === "string" && config.jws.signOptions.protectedHeader.typ.toLowerCase().includes("jwt")) {
|
|
398
|
+
typ = config.jws.signOptions.protectedHeader.typ;
|
|
399
|
+
}
|
|
400
|
+
const token = await sign(payload, key, {
|
|
401
|
+
...config.jws?.signOptions,
|
|
402
|
+
expiresIn: void 0,
|
|
403
|
+
// controlled via 'exp' claim
|
|
404
|
+
protectedHeader: {
|
|
405
|
+
...config.jws?.signOptions?.protectedHeader,
|
|
406
|
+
kid: "kid" in key ? key.kid : void 0,
|
|
407
|
+
typ: typ || "JWT",
|
|
408
|
+
cty: "application/json"
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
return token;
|
|
412
|
+
}
|
|
413
|
+
async function verifyJWSSession(_event, config, token) {
|
|
414
|
+
const jwk = getVerifyKey(config.key);
|
|
415
|
+
const alg = config.jws?.signOptions?.alg;
|
|
416
|
+
let typ;
|
|
417
|
+
if (config.jws?.signOptions?.protectedHeader?.typ && typeof config.jws.signOptions.protectedHeader.typ === "string" && config.jws.signOptions.protectedHeader.typ.toLowerCase().includes("jwt")) {
|
|
418
|
+
typ = config.jws.signOptions.protectedHeader.typ;
|
|
419
|
+
}
|
|
420
|
+
const { payload } = await verify(token, jwk, {
|
|
421
|
+
...config.jws?.verifyOptions,
|
|
422
|
+
requiredClaims: [
|
|
423
|
+
...config.jws?.verifyOptions?.requiredClaims?.filter(
|
|
424
|
+
(claim) => claim !== "jti" && claim !== "iat"
|
|
425
|
+
) || [],
|
|
426
|
+
"jti",
|
|
427
|
+
"iat"
|
|
428
|
+
],
|
|
429
|
+
typ: typ || "JWT",
|
|
430
|
+
algorithms: alg ? [alg] : void 0,
|
|
431
|
+
forceUint8Array: false,
|
|
432
|
+
validateJWT: true
|
|
433
|
+
}).catch((error_) => {
|
|
434
|
+
const message = error_ instanceof Error ? error_.message : String(error_);
|
|
435
|
+
throw new Error(`Invalid session token: ${message}`);
|
|
436
|
+
});
|
|
437
|
+
const { jti, iat, exp, ...data } = payload;
|
|
438
|
+
return {
|
|
439
|
+
id: jti,
|
|
440
|
+
createdAt: iat * 1e3,
|
|
441
|
+
expiresAt: exp ? exp * 1e3 : void 0,
|
|
442
|
+
data: data && typeof data === "object" ? data : new NullProtoObj()
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
async function clearJWSSession(event, config) {
|
|
446
|
+
const context = getEventContext(event);
|
|
447
|
+
const sessionName = config.name || DEFAULT_NAME;
|
|
448
|
+
if (context.sessions?.[sessionName]) {
|
|
449
|
+
delete context.sessions[sessionName];
|
|
450
|
+
}
|
|
451
|
+
if (config.cookie !== false && hasWritableResponse(event)) {
|
|
452
|
+
setChunkedCookie(event, sessionName, "", {
|
|
453
|
+
...DEFAULT_COOKIE,
|
|
454
|
+
...config.cookie,
|
|
455
|
+
expires: /* @__PURE__ */ new Date(0),
|
|
456
|
+
maxAge: void 0
|
|
457
|
+
});
|
|
458
|
+
await config.hooks?.onClear?.(event, config);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
function getSessionFromContext(event, sessionName) {
|
|
462
|
+
const context = getEventContext(event);
|
|
463
|
+
return context.sessions?.[sessionName];
|
|
464
|
+
}
|
|
465
|
+
function hasWritableResponse(event) {
|
|
466
|
+
return Boolean(event.res);
|
|
467
|
+
}
|
|
468
|
+
function getSignKey(key) {
|
|
469
|
+
if (!key) {
|
|
470
|
+
throw new Error("Session: JWS key is required.");
|
|
471
|
+
}
|
|
472
|
+
let _key;
|
|
473
|
+
if (isSymmetricJWK(key)) {
|
|
474
|
+
_key = key;
|
|
475
|
+
} else if ("privateKey" in key && isPrivateJWK(key.privateKey)) {
|
|
476
|
+
_key = key.privateKey;
|
|
477
|
+
}
|
|
478
|
+
if (!_key) {
|
|
479
|
+
throw new Error(
|
|
480
|
+
"Session: Invalid JWS key. It must be a symmetric JWK or a private JWK.",
|
|
481
|
+
{ cause: key }
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
return _key;
|
|
485
|
+
}
|
|
486
|
+
function getVerifyKey(key) {
|
|
487
|
+
if (!key) {
|
|
488
|
+
throw new Error("Session: JWS key is required.");
|
|
489
|
+
}
|
|
490
|
+
let _key;
|
|
491
|
+
if (isSymmetricJWK(key)) {
|
|
492
|
+
_key = key;
|
|
493
|
+
} else if ("publicKey" in key) {
|
|
494
|
+
const publicKey = key.publicKey;
|
|
495
|
+
if (isPublicJWK(publicKey)) {
|
|
496
|
+
_key = publicKey;
|
|
497
|
+
} else if (Array.isArray(publicKey)) {
|
|
498
|
+
const keys = publicKey.filter(
|
|
499
|
+
(candidate) => isPublicJWK(candidate)
|
|
500
|
+
);
|
|
501
|
+
if (keys.length > 0) {
|
|
502
|
+
_key = { keys };
|
|
503
|
+
}
|
|
504
|
+
} else if (publicKey && typeof publicKey === "object" && Array.isArray(publicKey.keys)) {
|
|
505
|
+
const keys = publicKey.keys.filter(
|
|
506
|
+
(candidate) => isPublicJWK(candidate)
|
|
507
|
+
);
|
|
508
|
+
if (keys.length > 0) {
|
|
509
|
+
_key = { keys };
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
if (!_key) {
|
|
514
|
+
throw new Error(
|
|
515
|
+
"Session: Invalid JWS key. It must be a symmetric JWK or a public JWK/set.",
|
|
516
|
+
{ cause: key }
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
return _key;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
export { clearJWESession, clearJWSSession, getJWESession, getJWSSession, sealJWESession, signJWSSession, unsealJWESession, updateJWESession, updateJWSSession, useJWESession, useJWSSession, verifyJWSSession };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "unjwt",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.25",
|
|
4
4
|
"description": "Low-level JWT utilities",
|
|
5
5
|
"repository": "sandros94/unjwt",
|
|
6
6
|
"license": "MIT",
|
|
@@ -31,9 +31,17 @@
|
|
|
31
31
|
"types": "./dist/utils.d.mts",
|
|
32
32
|
"default": "./dist/utils.mjs"
|
|
33
33
|
},
|
|
34
|
-
"./h3": {
|
|
35
|
-
"types": "./dist/h3.d.mts",
|
|
36
|
-
"default": "./dist/h3.mjs"
|
|
34
|
+
"./adapters/h3": {
|
|
35
|
+
"types": "./dist/adapters/h3.d.mts",
|
|
36
|
+
"default": "./dist/adapters/h3.mjs"
|
|
37
|
+
},
|
|
38
|
+
"./adapters/h3v1": {
|
|
39
|
+
"types": "./dist/adapters/h3v1.d.mts",
|
|
40
|
+
"default": "./dist/adapters/h3v1.mjs"
|
|
41
|
+
},
|
|
42
|
+
"./adapters/h3v2": {
|
|
43
|
+
"types": "./dist/adapters/h3v2.d.mts",
|
|
44
|
+
"default": "./dist/adapters/h3v2.mjs"
|
|
37
45
|
}
|
|
38
46
|
},
|
|
39
47
|
"main": "./dist/index.mjs",
|
|
@@ -60,8 +68,12 @@
|
|
|
60
68
|
"@vitest/coverage-v8": "^3.2.4",
|
|
61
69
|
"automd": "^0.4.2",
|
|
62
70
|
"changelogen": "^0.6.2",
|
|
71
|
+
"cookie-esv1": "npm:cookie-es@^1.2.2",
|
|
72
|
+
"cookie-esv2": "npm:cookie-es@^2.0.0",
|
|
63
73
|
"eslint": "^9.36.0",
|
|
64
74
|
"eslint-config-unjs": "^0.5.0",
|
|
75
|
+
"h3v1": "npm:h3@^1.15.4",
|
|
76
|
+
"h3v2": "npm:h3@^2.0.1-rc.2",
|
|
65
77
|
"jose": "^6.1.0",
|
|
66
78
|
"prettier": "^3.6.2",
|
|
67
79
|
"supertest": "^7.1.4",
|
|
@@ -71,7 +83,8 @@
|
|
|
71
83
|
},
|
|
72
84
|
"peerDependencies": {
|
|
73
85
|
"cookie-es": "^1.2.2 || ^2.0.0",
|
|
74
|
-
"h3": "^1.15.4 ||
|
|
86
|
+
"h3": "^1.15.4 || >=2.0.1-rc.2 || ^2.0.1",
|
|
87
|
+
"rou3": "^0.7.7"
|
|
75
88
|
},
|
|
76
89
|
"peerDependenciesMeta": {
|
|
77
90
|
"cookie-es": {
|
|
@@ -81,5 +94,5 @@
|
|
|
81
94
|
"optional": true
|
|
82
95
|
}
|
|
83
96
|
},
|
|
84
|
-
"packageManager": "pnpm@10.
|
|
97
|
+
"packageManager": "pnpm@10.18.3"
|
|
85
98
|
}
|