unjwt 0.5.38 → 0.5.40
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 +148 -670
- package/dist/_chunks/chunk.mjs +13 -0
- package/dist/_chunks/index.d.mts +558 -0
- package/dist/_chunks/index2.d.mts +108 -0
- package/dist/_chunks/jwe.d.mts +43 -0
- package/dist/_chunks/jwe.mjs +132 -0
- package/dist/_chunks/jwk.d.mts +611 -0
- package/dist/_chunks/jwk.mjs +1592 -0
- package/dist/_chunks/jws.d.mts +242 -0
- package/dist/_chunks/jws.mjs +2070 -0
- package/dist/_chunks/jws2.mjs +598 -0
- package/dist/_chunks/utils.mjs +375 -0
- package/dist/adapters/h3.d.mts +3 -267
- package/dist/adapters/h3.mjs +3 -704
- package/dist/adapters/h3v1.d.mts +3 -8
- package/dist/adapters/h3v1.mjs +3 -7
- package/dist/adapters/h3v2.d.mts +145 -147
- package/dist/adapters/h3v2.mjs +501 -594
- package/dist/index.d.mts +6 -8
- package/dist/index.mjs +5 -4
- package/dist/jwe.d.mts +3 -4
- package/dist/jwe.mjs +2 -3
- package/dist/jwk.d.mts +3 -2
- package/dist/jwk.mjs +2 -2
- package/dist/jws.d.mts +3 -3
- package/dist/jws.mjs +2 -3
- package/dist/utils.d.mts +3 -3
- package/dist/utils.mjs +2 -1
- package/package.json +33 -31
- package/dist/shared/unjwt.4JQCoID_.d.mts +0 -161
- package/dist/shared/unjwt.B-v62plj.mjs +0 -249
- package/dist/shared/unjwt.BCi3FEWr.d.mts +0 -328
- package/dist/shared/unjwt.BYDTEWa-.d.mts +0 -269
- package/dist/shared/unjwt.Be8NxwFK.mjs +0 -438
- package/dist/shared/unjwt.BqdNUKDr.d.mts +0 -64
- package/dist/shared/unjwt.CMNtw8ek.d.mts +0 -155
- package/dist/shared/unjwt.Cy2MlkUe.d.mts +0 -174
- package/dist/shared/unjwt.D0_ccT7g.d.mts +0 -166
- package/dist/shared/unjwt.DdLN9vwI.mjs +0 -163
- package/dist/shared/unjwt.Dpl__ZAR.mjs +0 -2121
- package/dist/shared/unjwt.JdKqVANs.d.mts +0 -5
|
@@ -0,0 +1,598 @@
|
|
|
1
|
+
import { d as isSymmetricJWK, f as computeExpiresInSeconds, i as encrypt, l as isPrivateJWK, n as verify, r as decrypt, t as sign, u as isPublicJWK } from "./jws.mjs";
|
|
2
|
+
import { isEvent, setCookie } from "h3";
|
|
3
|
+
import { parse } from "cookie-es";
|
|
4
|
+
//#region src/adapters/h3v1/session/jwe.ts
|
|
5
|
+
const kGetSessionPromise$1 = Symbol("h3_jwe_getSession");
|
|
6
|
+
const DEFAULT_NAME$1 = "h3-jwe";
|
|
7
|
+
const DEFAULT_COOKIE$1 = {
|
|
8
|
+
path: "/",
|
|
9
|
+
secure: true,
|
|
10
|
+
httpOnly: true
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Create a session manager for the current request.
|
|
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 event.context.sessions?.[sessionName]?.id;
|
|
21
|
+
},
|
|
22
|
+
get createdAt() {
|
|
23
|
+
return event.context.sessions?.[sessionName]?.createdAt || Date.now();
|
|
24
|
+
},
|
|
25
|
+
get expiresAt() {
|
|
26
|
+
return event.context.sessions?.[sessionName]?.expiresAt;
|
|
27
|
+
},
|
|
28
|
+
get data() {
|
|
29
|
+
return event.context.sessions?.[sessionName]?.data || {};
|
|
30
|
+
},
|
|
31
|
+
get token() {
|
|
32
|
+
return getJWESessionToken(event, config);
|
|
33
|
+
},
|
|
34
|
+
update: async (update) => {
|
|
35
|
+
if (!isEvent(event)) throw new Error("[h3] Cannot update read-only session.");
|
|
36
|
+
await updateJWESession(event, config, update);
|
|
37
|
+
return sessionManager;
|
|
38
|
+
},
|
|
39
|
+
clear: async () => {
|
|
40
|
+
if (!isEvent(event)) throw new Error("[h3] Cannot clear read-only session.");
|
|
41
|
+
await clearJWESession(event, config);
|
|
42
|
+
return sessionManager;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
return sessionManager;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Get (and lazily initialize) the session for the current request.
|
|
49
|
+
*/
|
|
50
|
+
async function getJWESession(event, config) {
|
|
51
|
+
const sessionName = config.name || DEFAULT_NAME$1;
|
|
52
|
+
if (!event.context.sessions) event.context.sessions = Object.create(null);
|
|
53
|
+
const existingSession = event.context.sessions[sessionName];
|
|
54
|
+
if (existingSession) {
|
|
55
|
+
const session = existingSession[kGetSessionPromise$1] ? await existingSession[kGetSessionPromise$1] : existingSession;
|
|
56
|
+
/**
|
|
57
|
+
* We check if a session is expired before returning it. If it is expired we clear it and create a new one,
|
|
58
|
+
* unless we have a read-only event in which case we just return it as it was valid at the time of reading
|
|
59
|
+
* the cookie/header (like in a websocket upgrade)
|
|
60
|
+
*/
|
|
61
|
+
if (session.expiresAt !== void 0 && session.expiresAt < Date.now() && isEvent(event)) {
|
|
62
|
+
await config.hooks?.onExpire?.({
|
|
63
|
+
event,
|
|
64
|
+
error: /* @__PURE__ */ new Error(`JWT "exp" (Expiration Time) Claim validation failed: Token has expired (exp: ${(/* @__PURE__ */ new Date(session.expiresAt * 1e3)).toISOString()})`),
|
|
65
|
+
config
|
|
66
|
+
});
|
|
67
|
+
return clearJWESession(event, config).then(() => getJWESession(event, config));
|
|
68
|
+
}
|
|
69
|
+
await config.hooks?.onRead?.({
|
|
70
|
+
event,
|
|
71
|
+
session,
|
|
72
|
+
config
|
|
73
|
+
});
|
|
74
|
+
return session;
|
|
75
|
+
}
|
|
76
|
+
const now = config.jwe?.encryptOptions?.currentDate?.getTime() ?? Date.now();
|
|
77
|
+
const createdAt = now - now % 1e3;
|
|
78
|
+
const session = {
|
|
79
|
+
id: void 0,
|
|
80
|
+
createdAt,
|
|
81
|
+
expiresAt: config.maxAge === void 0 ? void 0 : createdAt + computeExpiresInSeconds(config.maxAge) * 1e3,
|
|
82
|
+
data: Object.create(null)
|
|
83
|
+
};
|
|
84
|
+
event.context.sessions[sessionName] = session;
|
|
85
|
+
const token = getJWESessionToken(event, config);
|
|
86
|
+
if (token) {
|
|
87
|
+
const promise = unsealJWESession(event, config, token).catch(async (error_) => {
|
|
88
|
+
if (error_ instanceof Error && (error_.message.includes("Token has expired") || error_.message.includes("Token is too old"))) {
|
|
89
|
+
await config.hooks?.onExpire?.({
|
|
90
|
+
event,
|
|
91
|
+
error: error_,
|
|
92
|
+
config
|
|
93
|
+
});
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
await config.hooks?.onError?.({
|
|
97
|
+
event,
|
|
98
|
+
error: error_,
|
|
99
|
+
config
|
|
100
|
+
});
|
|
101
|
+
}).then((unsealed) => {
|
|
102
|
+
if (unsealed) Object.assign(session, unsealed);
|
|
103
|
+
delete event.context.sessions[sessionName][kGetSessionPromise$1];
|
|
104
|
+
return session;
|
|
105
|
+
});
|
|
106
|
+
event.context.sessions[sessionName][kGetSessionPromise$1] = promise;
|
|
107
|
+
await promise;
|
|
108
|
+
}
|
|
109
|
+
await config.hooks?.onRead?.({
|
|
110
|
+
event,
|
|
111
|
+
session,
|
|
112
|
+
config
|
|
113
|
+
});
|
|
114
|
+
return session;
|
|
115
|
+
}
|
|
116
|
+
function getJWESessionToken(event, config) {
|
|
117
|
+
const sessionName = config.name || DEFAULT_NAME$1;
|
|
118
|
+
let token;
|
|
119
|
+
if (config.sessionHeader !== false) {
|
|
120
|
+
const headerValue = _getReqHeader$1(event, typeof config.sessionHeader === "string" ? config.sessionHeader.toLowerCase() : `x-${sessionName.toLowerCase()}-session`);
|
|
121
|
+
if (typeof headerValue === "string") token = headerValue.startsWith("Bearer ") ? headerValue.slice(7).trim() : headerValue;
|
|
122
|
+
}
|
|
123
|
+
if (config.cookie !== false) {
|
|
124
|
+
const setCookie = _getResHeader$1(event, "set-cookie");
|
|
125
|
+
if (typeof setCookie === "string") token = findSetCookie$1(setCookie, sessionName);
|
|
126
|
+
else if (Array.isArray(setCookie)) for (const sc of setCookie) {
|
|
127
|
+
token = findSetCookie$1(sc, sessionName);
|
|
128
|
+
if (token) break;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (!token) {
|
|
132
|
+
const cookieHeader = _getReqHeader$1(event, "cookie");
|
|
133
|
+
if (cookieHeader) token = parse(String(cookieHeader))[sessionName];
|
|
134
|
+
}
|
|
135
|
+
return token;
|
|
136
|
+
}
|
|
137
|
+
function _getReqHeader$1(event, name) {
|
|
138
|
+
if (event.node) return event.node?.req.headers[name];
|
|
139
|
+
if (event.request) return event.request.headers?.get(name);
|
|
140
|
+
if (event.headers) return event.headers.get(name);
|
|
141
|
+
}
|
|
142
|
+
function _getResHeader$1(event, name) {
|
|
143
|
+
if (event.node) return event.node?.res.getHeader(name);
|
|
144
|
+
if (event.response) return event.response.headers?.get(name);
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Update the session (optionally mutating the session data) and re-issue the JWE token.
|
|
148
|
+
*/
|
|
149
|
+
async function updateJWESession(event, config, update) {
|
|
150
|
+
if (!isEvent(event)) throw new Error("Cannot update a new session. Use `useSession(event)` within the main handler.");
|
|
151
|
+
const sessionName = config.name || DEFAULT_NAME$1;
|
|
152
|
+
const session = event.context.sessions?.[sessionName] || await getJWESession(event, config);
|
|
153
|
+
if (typeof update === "function") update = update(session.data);
|
|
154
|
+
if (update) {
|
|
155
|
+
const oldSession = {
|
|
156
|
+
...session,
|
|
157
|
+
data: { ...session.data }
|
|
158
|
+
};
|
|
159
|
+
Object.assign(session.data, update);
|
|
160
|
+
await config.hooks?.onUpdate?.({
|
|
161
|
+
session,
|
|
162
|
+
oldSession,
|
|
163
|
+
event,
|
|
164
|
+
config
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
const now = config.jwe?.encryptOptions?.currentDate?.getTime() ?? Date.now();
|
|
168
|
+
const createdAt = now - now % 1e3;
|
|
169
|
+
Object.assign(session, {
|
|
170
|
+
id: config.generateId?.() || crypto.randomUUID(),
|
|
171
|
+
createdAt,
|
|
172
|
+
expiresAt: config.maxAge === void 0 ? void 0 : createdAt + computeExpiresInSeconds(config.maxAge) * 1e3
|
|
173
|
+
});
|
|
174
|
+
if (config.cookie !== false) setCookie(event, sessionName, await sealJWESession(event, config), {
|
|
175
|
+
...DEFAULT_COOKIE$1,
|
|
176
|
+
...config.cookie,
|
|
177
|
+
expires: config.maxAge === void 0 ? void 0 : new Date(session.createdAt + computeExpiresInSeconds(config.maxAge) * 1e3)
|
|
178
|
+
});
|
|
179
|
+
return session;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Produce a JWE for the current session.
|
|
183
|
+
* Payload structure:
|
|
184
|
+
* {
|
|
185
|
+
* jti: string;
|
|
186
|
+
* iat: number; (seconds)
|
|
187
|
+
* exp?: number; (seconds)
|
|
188
|
+
* data: Record<string, any>;
|
|
189
|
+
* }
|
|
190
|
+
*/
|
|
191
|
+
async function sealJWESession(event, config) {
|
|
192
|
+
const key = getEncryptKey(config.key || config.secret);
|
|
193
|
+
const sessionName = config.name || DEFAULT_NAME$1;
|
|
194
|
+
const session = event.context.sessions?.[sessionName] || await getJWESession(event, config);
|
|
195
|
+
const iat = Math.floor(session.createdAt / 1e3);
|
|
196
|
+
const exp = session.expiresAt ? Math.floor(session.expiresAt / 1e3) : void 0;
|
|
197
|
+
const payload = {
|
|
198
|
+
...session.data,
|
|
199
|
+
jti: session.id,
|
|
200
|
+
iat
|
|
201
|
+
};
|
|
202
|
+
if (exp) payload.exp = exp;
|
|
203
|
+
let typ = void 0;
|
|
204
|
+
if (config.jwe?.encryptOptions?.protectedHeader?.typ && typeof config.jwe.encryptOptions.protectedHeader.typ === "string" && config.jwe.encryptOptions.protectedHeader.typ.toLowerCase().includes("jwt")) typ = config.jwe.encryptOptions.protectedHeader.typ;
|
|
205
|
+
return await encrypt(payload, key, {
|
|
206
|
+
...config.jwe?.encryptOptions,
|
|
207
|
+
expiresIn: void 0,
|
|
208
|
+
protectedHeader: {
|
|
209
|
+
...config.jwe?.encryptOptions?.protectedHeader,
|
|
210
|
+
kid: typeof key === "string" ? void 0 : key.kid,
|
|
211
|
+
typ: typ || "JWT",
|
|
212
|
+
cty: "application/json"
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Decrypt the JWE and return a Session-compatible object.
|
|
218
|
+
*/
|
|
219
|
+
async function unsealJWESession(event, config, sealed) {
|
|
220
|
+
const key = config.hooks?.onUnsealKeyLookup ? (header) => config.hooks.onUnsealKeyLookup({
|
|
221
|
+
header,
|
|
222
|
+
event,
|
|
223
|
+
config
|
|
224
|
+
}) : getDecryptKey(config.key || config.secret);
|
|
225
|
+
const alg = config.jwe?.encryptOptions?.alg;
|
|
226
|
+
const enc = config.jwe?.encryptOptions?.enc;
|
|
227
|
+
let typ = void 0;
|
|
228
|
+
if (config.jwe?.encryptOptions?.protectedHeader?.typ && typeof config.jwe.encryptOptions.protectedHeader.typ === "string" && config.jwe.encryptOptions.protectedHeader.typ.toLowerCase().includes("jwt")) typ = config.jwe.encryptOptions.protectedHeader.typ;
|
|
229
|
+
const { payload } = await decrypt(sealed, key, {
|
|
230
|
+
...config.jwe?.decryptOptions,
|
|
231
|
+
requiredClaims: [...new Set([
|
|
232
|
+
...config.jwe?.decryptOptions?.requiredClaims || [],
|
|
233
|
+
"jti",
|
|
234
|
+
"iat"
|
|
235
|
+
])],
|
|
236
|
+
typ: typ || "JWT",
|
|
237
|
+
maxTokenAge: config.maxAge,
|
|
238
|
+
algorithms: alg ? [alg] : void 0,
|
|
239
|
+
encryptionAlgorithms: enc ? [enc] : void 0,
|
|
240
|
+
unwrappedKeyAlgorithm: void 0,
|
|
241
|
+
keyUsage: void 0,
|
|
242
|
+
forceUint8Array: false,
|
|
243
|
+
validateJWT: true
|
|
244
|
+
}).catch((error_) => {
|
|
245
|
+
if (error_ instanceof Error) {
|
|
246
|
+
const message = error_.message;
|
|
247
|
+
throw new Error(`Invalid session token: ${message}`);
|
|
248
|
+
}
|
|
249
|
+
throw new Error(error_);
|
|
250
|
+
});
|
|
251
|
+
const { jti, iat, exp, ...data } = payload;
|
|
252
|
+
return {
|
|
253
|
+
id: jti,
|
|
254
|
+
createdAt: iat * 1e3,
|
|
255
|
+
expiresAt: exp ? exp * 1e3 : void 0,
|
|
256
|
+
data: data && typeof data === "object" ? data : Object.create(null)
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Clear the session (delete from context and drop cookie).
|
|
261
|
+
*/
|
|
262
|
+
async function clearJWESession(event, config) {
|
|
263
|
+
const sessionName = config.name || DEFAULT_NAME$1;
|
|
264
|
+
let session = event.context.sessions?.[sessionName];
|
|
265
|
+
if (session && session[kGetSessionPromise$1]) session = await session[kGetSessionPromise$1];
|
|
266
|
+
if (event.context.sessions?.[sessionName]) delete event.context.sessions[sessionName];
|
|
267
|
+
setCookie(event, sessionName, "", {
|
|
268
|
+
...DEFAULT_COOKIE$1,
|
|
269
|
+
...config.cookie,
|
|
270
|
+
expires: /* @__PURE__ */ new Date(0),
|
|
271
|
+
maxAge: void 0
|
|
272
|
+
});
|
|
273
|
+
await config.hooks?.onClear?.({
|
|
274
|
+
session,
|
|
275
|
+
event,
|
|
276
|
+
config
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
function getEncryptKey(key) {
|
|
280
|
+
if (!key) throw new Error("Session: JWE key is required.");
|
|
281
|
+
let _key;
|
|
282
|
+
if (typeof key === "string") _key = key;
|
|
283
|
+
else if (isSymmetricJWK(key) || isPrivateJWK(key)) _key = key;
|
|
284
|
+
else if ("publicKey" in key && isPublicJWK(key.publicKey)) _key = key.publicKey;
|
|
285
|
+
else if ("privateKey" in key && isPrivateJWK(key.privateKey)) _key = key.privateKey;
|
|
286
|
+
if (!_key) throw new Error("Session: Invalid JWE key. It must be a password string or valid JWK.", { cause: key });
|
|
287
|
+
return _key;
|
|
288
|
+
}
|
|
289
|
+
function getDecryptKey(key) {
|
|
290
|
+
if (!key) throw new Error("Session: JWE key is required.");
|
|
291
|
+
let _key = void 0;
|
|
292
|
+
if (typeof key === "string") _key = key;
|
|
293
|
+
else if (isSymmetricJWK(key)) _key = key;
|
|
294
|
+
else if ("privateKey" in key) _key = key.privateKey;
|
|
295
|
+
if (!_key) throw new Error("Session: Invalid JWE key. It must be a password string or a valid private JWK.", { cause: key });
|
|
296
|
+
return _key;
|
|
297
|
+
}
|
|
298
|
+
function findSetCookie$1(setCookie, name) {
|
|
299
|
+
const regex = new RegExp(`(?:^|,\\s*)${name}=([^;]+)`);
|
|
300
|
+
const match = setCookie.match(regex);
|
|
301
|
+
return match ? match[1] : void 0;
|
|
302
|
+
}
|
|
303
|
+
//#endregion
|
|
304
|
+
//#region src/adapters/h3v1/session/jws.ts
|
|
305
|
+
const kGetSessionPromise = Symbol("h3_jws_getSession");
|
|
306
|
+
const DEFAULT_NAME = "h3-jws";
|
|
307
|
+
const DEFAULT_COOKIE = {
|
|
308
|
+
path: "/",
|
|
309
|
+
secure: true,
|
|
310
|
+
httpOnly: false
|
|
311
|
+
};
|
|
312
|
+
/**
|
|
313
|
+
* Create a session manager for the current request using JWS (signed only, not encrypted).
|
|
314
|
+
* NOTE: Contents are visible to clients (Base64URL-decoded JSON). Do not store secrets in session data.
|
|
315
|
+
*/
|
|
316
|
+
async function useJWSSession(event, config) {
|
|
317
|
+
const sessionName = config.name || DEFAULT_NAME;
|
|
318
|
+
await getJWSSession(event, config);
|
|
319
|
+
const sessionManager = {
|
|
320
|
+
get id() {
|
|
321
|
+
return event.context.sessions?.[sessionName]?.id;
|
|
322
|
+
},
|
|
323
|
+
get createdAt() {
|
|
324
|
+
return event.context.sessions?.[sessionName]?.createdAt || Date.now();
|
|
325
|
+
},
|
|
326
|
+
get expiresAt() {
|
|
327
|
+
return event.context.sessions?.[sessionName]?.expiresAt;
|
|
328
|
+
},
|
|
329
|
+
get data() {
|
|
330
|
+
return event.context.sessions?.[sessionName]?.data || {};
|
|
331
|
+
},
|
|
332
|
+
get token() {
|
|
333
|
+
return getJWSSessionToken(event, config);
|
|
334
|
+
},
|
|
335
|
+
update: async (update) => {
|
|
336
|
+
if (!isEvent(event)) throw new Error("[h3] Cannot update read-only session.");
|
|
337
|
+
await updateJWSSession(event, config, update);
|
|
338
|
+
return sessionManager;
|
|
339
|
+
},
|
|
340
|
+
clear: async () => {
|
|
341
|
+
if (!isEvent(event)) throw new Error("[h3] Cannot clear read-only session.");
|
|
342
|
+
await clearJWSSession(event, config);
|
|
343
|
+
return sessionManager;
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
return sessionManager;
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Retrieve (and lazily initialize) the session.
|
|
350
|
+
*/
|
|
351
|
+
async function getJWSSession(event, config) {
|
|
352
|
+
const sessionName = config.name || DEFAULT_NAME;
|
|
353
|
+
if (!event.context.sessions) event.context.sessions = Object.create(null);
|
|
354
|
+
const existingSession = event.context.sessions[sessionName];
|
|
355
|
+
if (existingSession) {
|
|
356
|
+
const session = existingSession[kGetSessionPromise] ? await existingSession[kGetSessionPromise] : existingSession;
|
|
357
|
+
/**
|
|
358
|
+
* We check if a session is expired before returning it. If it is expired we clear it and create a new one,
|
|
359
|
+
* unless we have a read-only event in which case we just return it as it was valid at the time of reading
|
|
360
|
+
* the cookie/header (like in a websocket upgrade)
|
|
361
|
+
*/
|
|
362
|
+
if (session.expiresAt !== void 0 && session.expiresAt < Date.now() && isEvent(event)) {
|
|
363
|
+
await config.hooks?.onExpire?.({
|
|
364
|
+
event,
|
|
365
|
+
error: /* @__PURE__ */ new Error(`JWT "exp" (Expiration Time) Claim validation failed: Token has expired (exp: ${(/* @__PURE__ */ new Date(session.expiresAt * 1e3)).toISOString()})`),
|
|
366
|
+
config
|
|
367
|
+
});
|
|
368
|
+
return clearJWSSession(event, config).then(() => getJWSSession(event, config));
|
|
369
|
+
}
|
|
370
|
+
await config.hooks?.onRead?.({
|
|
371
|
+
event,
|
|
372
|
+
session,
|
|
373
|
+
config
|
|
374
|
+
});
|
|
375
|
+
return session;
|
|
376
|
+
}
|
|
377
|
+
const now = config.jws?.signOptions?.currentDate?.getTime() ?? Date.now();
|
|
378
|
+
const createdAt = now - now % 1e3;
|
|
379
|
+
const session = {
|
|
380
|
+
id: void 0,
|
|
381
|
+
createdAt,
|
|
382
|
+
expiresAt: config.maxAge === void 0 ? void 0 : createdAt + computeExpiresInSeconds(config.maxAge) * 1e3,
|
|
383
|
+
data: Object.create(null)
|
|
384
|
+
};
|
|
385
|
+
event.context.sessions[sessionName] = session;
|
|
386
|
+
const token = getJWSSessionToken(event, config);
|
|
387
|
+
if (token) {
|
|
388
|
+
const promise = verifyJWSSession(event, config, token).catch(async (error_) => {
|
|
389
|
+
if (error_ instanceof Error && (error_.message.includes("Token has expired") || error_.message.includes("Token is too old"))) {
|
|
390
|
+
await config.hooks?.onExpire?.({
|
|
391
|
+
event,
|
|
392
|
+
error: error_,
|
|
393
|
+
config
|
|
394
|
+
});
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
await config.hooks?.onError?.({
|
|
398
|
+
event,
|
|
399
|
+
error: error_,
|
|
400
|
+
config
|
|
401
|
+
});
|
|
402
|
+
}).then((unsealed) => {
|
|
403
|
+
if (unsealed) Object.assign(session, unsealed);
|
|
404
|
+
delete event.context.sessions[sessionName][kGetSessionPromise];
|
|
405
|
+
return session;
|
|
406
|
+
});
|
|
407
|
+
session[kGetSessionPromise] = promise;
|
|
408
|
+
await promise;
|
|
409
|
+
}
|
|
410
|
+
await config.hooks?.onRead?.({
|
|
411
|
+
event,
|
|
412
|
+
session,
|
|
413
|
+
config
|
|
414
|
+
});
|
|
415
|
+
return session;
|
|
416
|
+
}
|
|
417
|
+
function getJWSSessionToken(event, config) {
|
|
418
|
+
const sessionName = config.name || DEFAULT_NAME;
|
|
419
|
+
let token;
|
|
420
|
+
if (config.sessionHeader !== false) {
|
|
421
|
+
const headerValue = _getReqHeader(event, typeof config.sessionHeader === "string" ? config.sessionHeader.toLowerCase() : `x-${sessionName.toLowerCase()}-session`);
|
|
422
|
+
if (typeof headerValue === "string") token = headerValue.startsWith("Bearer ") ? headerValue.slice(7).trim() : headerValue;
|
|
423
|
+
}
|
|
424
|
+
if (config.cookie !== false) {
|
|
425
|
+
const setCookie = _getResHeader(event, "set-cookie");
|
|
426
|
+
if (typeof setCookie === "string") token = findSetCookie(setCookie, sessionName);
|
|
427
|
+
else if (Array.isArray(setCookie)) for (const sc of setCookie) {
|
|
428
|
+
token = findSetCookie(sc, sessionName);
|
|
429
|
+
if (token) break;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
if (!token) {
|
|
433
|
+
const cookieHeader = _getReqHeader(event, "cookie");
|
|
434
|
+
if (cookieHeader) token = parse(String(cookieHeader))[sessionName];
|
|
435
|
+
}
|
|
436
|
+
return token;
|
|
437
|
+
}
|
|
438
|
+
function _getReqHeader(event, name) {
|
|
439
|
+
if (event.node) return event.node.req.headers[name];
|
|
440
|
+
if (event.request) return event.request.headers.get(name);
|
|
441
|
+
if (event.headers) return event.headers.get(name);
|
|
442
|
+
}
|
|
443
|
+
function _getResHeader(event, name) {
|
|
444
|
+
if (event.node) return event.node?.res.getHeader(name);
|
|
445
|
+
if (event.response) return event.response.headers?.get(name);
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Update session data (if provided) and reissue JWS.
|
|
449
|
+
*/
|
|
450
|
+
async function updateJWSSession(event, config, update) {
|
|
451
|
+
if (!isEvent(event)) throw new Error("Cannot update a new session. Use `useSession(event)` within the main handler.");
|
|
452
|
+
const sessionName = config.name || DEFAULT_NAME;
|
|
453
|
+
const session = event.context.sessions?.[sessionName] || await getJWSSession(event, config);
|
|
454
|
+
if (typeof update === "function") update = update(session.data);
|
|
455
|
+
if (update) {
|
|
456
|
+
const oldSession = {
|
|
457
|
+
...session,
|
|
458
|
+
data: { ...session.data }
|
|
459
|
+
};
|
|
460
|
+
Object.assign(session.data, update);
|
|
461
|
+
await config.hooks?.onUpdate?.({
|
|
462
|
+
session,
|
|
463
|
+
oldSession,
|
|
464
|
+
event,
|
|
465
|
+
config
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
const now = config.jws?.signOptions?.currentDate?.getTime() ?? Date.now();
|
|
469
|
+
const createdAt = now - now % 1e3;
|
|
470
|
+
Object.assign(session, {
|
|
471
|
+
id: config.generateId?.() || crypto.randomUUID(),
|
|
472
|
+
createdAt,
|
|
473
|
+
expiresAt: config.maxAge === void 0 ? void 0 : createdAt + computeExpiresInSeconds(config.maxAge) * 1e3
|
|
474
|
+
});
|
|
475
|
+
if (config.cookie !== false) setCookie(event, sessionName, await signJWSSession(event, config), {
|
|
476
|
+
...DEFAULT_COOKIE,
|
|
477
|
+
...config.cookie,
|
|
478
|
+
expires: config.maxAge === void 0 ? void 0 : new Date(session.createdAt + computeExpiresInSeconds(config.maxAge) * 1e3)
|
|
479
|
+
});
|
|
480
|
+
return session;
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Sign current session as a compact JWS.
|
|
484
|
+
* Payload claims:
|
|
485
|
+
* jti, iat, exp?, data, plus optional extraClaims (cannot override reserved)
|
|
486
|
+
*/
|
|
487
|
+
async function signJWSSession(event, config) {
|
|
488
|
+
const key = getSignKey(config.key);
|
|
489
|
+
const sessionName = config.name || DEFAULT_NAME;
|
|
490
|
+
const session = event.context.sessions?.[sessionName] || await getJWSSession(event, config);
|
|
491
|
+
const iat = Math.floor(session.createdAt / 1e3);
|
|
492
|
+
const exp = session.expiresAt ? Math.floor(session.expiresAt / 1e3) : void 0;
|
|
493
|
+
const payload = {
|
|
494
|
+
...session.data,
|
|
495
|
+
jti: session.id,
|
|
496
|
+
iat
|
|
497
|
+
};
|
|
498
|
+
if (exp) payload.exp = exp;
|
|
499
|
+
let typ = void 0;
|
|
500
|
+
if (config.jws?.signOptions?.protectedHeader?.typ && typeof config.jws.signOptions.protectedHeader.typ === "string" && config.jws.signOptions.protectedHeader.typ.toLowerCase().includes("jwt")) typ = config.jws.signOptions.protectedHeader.typ;
|
|
501
|
+
return await sign(payload, key, {
|
|
502
|
+
...config.jws?.signOptions,
|
|
503
|
+
expiresIn: void 0,
|
|
504
|
+
protectedHeader: {
|
|
505
|
+
...config.jws?.signOptions?.protectedHeader,
|
|
506
|
+
kid: key.kid,
|
|
507
|
+
typ: typ || "JWT",
|
|
508
|
+
cty: "application/json"
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Verify and parse a compact JWS into a Session structure.
|
|
514
|
+
* Performs:
|
|
515
|
+
* - cryptographic signature verification
|
|
516
|
+
* - (optional) standard claim checks if validateJWT true
|
|
517
|
+
* - ensures jti & iat presence
|
|
518
|
+
* - enforces maxAge if provided
|
|
519
|
+
*/
|
|
520
|
+
async function verifyJWSSession(event, config, token) {
|
|
521
|
+
const alg = config.jws?.signOptions?.alg;
|
|
522
|
+
const jwk = config.hooks?.onVerifyKeyLookup ? (header) => config.hooks.onVerifyKeyLookup({
|
|
523
|
+
header,
|
|
524
|
+
event,
|
|
525
|
+
config
|
|
526
|
+
}) : getVerifyKey(config.key);
|
|
527
|
+
let typ = void 0;
|
|
528
|
+
if (config.jws?.signOptions?.protectedHeader?.typ && typeof config.jws.signOptions.protectedHeader.typ === "string" && config.jws.signOptions.protectedHeader.typ.toLowerCase().includes("jwt")) typ = config.jws.signOptions.protectedHeader.typ;
|
|
529
|
+
const { payload } = await verify(token, jwk, {
|
|
530
|
+
...config.jws?.verifyOptions,
|
|
531
|
+
requiredClaims: [...new Set([
|
|
532
|
+
...config.jws?.verifyOptions?.requiredClaims || [],
|
|
533
|
+
"jti",
|
|
534
|
+
"iat"
|
|
535
|
+
])],
|
|
536
|
+
typ: typ || "JWT",
|
|
537
|
+
algorithms: alg ? [alg] : void 0,
|
|
538
|
+
forceUint8Array: false,
|
|
539
|
+
validateJWT: true
|
|
540
|
+
}).catch((error_) => {
|
|
541
|
+
const message = error_ instanceof Error ? error_.message : String(error_);
|
|
542
|
+
throw new Error(`Invalid session token: ${message}`);
|
|
543
|
+
});
|
|
544
|
+
const { jti, iat, exp, ...data } = payload;
|
|
545
|
+
return {
|
|
546
|
+
id: jti,
|
|
547
|
+
createdAt: iat * 1e3,
|
|
548
|
+
expiresAt: exp ? exp * 1e3 : void 0,
|
|
549
|
+
data: data && typeof data === "object" ? data : Object.create(null)
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Destroy the session (context + cookie).
|
|
554
|
+
*/
|
|
555
|
+
async function clearJWSSession(event, config) {
|
|
556
|
+
const sessionName = config.name || DEFAULT_NAME;
|
|
557
|
+
let session = event.context.sessions?.[sessionName];
|
|
558
|
+
if (session && session[kGetSessionPromise]) session = await session[kGetSessionPromise];
|
|
559
|
+
if (event.context.sessions?.[sessionName]) delete event.context.sessions[sessionName];
|
|
560
|
+
setCookie(event, sessionName, "", {
|
|
561
|
+
...DEFAULT_COOKIE,
|
|
562
|
+
...config.cookie,
|
|
563
|
+
expires: /* @__PURE__ */ new Date(0),
|
|
564
|
+
maxAge: void 0
|
|
565
|
+
});
|
|
566
|
+
await config.hooks?.onClear?.({
|
|
567
|
+
session,
|
|
568
|
+
event,
|
|
569
|
+
config
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
function getSignKey(key) {
|
|
573
|
+
if (!key) throw new Error("Session: JWS key is required.");
|
|
574
|
+
let _key = void 0;
|
|
575
|
+
if (isSymmetricJWK(key)) _key = key;
|
|
576
|
+
else if ("privateKey" in key && isPrivateJWK(key.privateKey)) _key = key.privateKey;
|
|
577
|
+
if (!_key) throw new Error("Session: Invalid JWS key. It must be a symmetric JWK or a private JWK.", { cause: key });
|
|
578
|
+
return _key;
|
|
579
|
+
}
|
|
580
|
+
function getVerifyKey(key) {
|
|
581
|
+
if (!key) throw new Error("Session: JWS key is required.");
|
|
582
|
+
let _key = void 0;
|
|
583
|
+
if (isSymmetricJWK(key)) _key = key;
|
|
584
|
+
else if ("publicKey" in key && isPublicJWK(key.publicKey)) _key = key.publicKey;
|
|
585
|
+
else if ("publicKey" in key && Array.isArray(key.publicKey)) {
|
|
586
|
+
const keys = key.publicKey.filter((k) => isPublicJWK(k));
|
|
587
|
+
if (keys && keys.length > 0) _key = { keys };
|
|
588
|
+
}
|
|
589
|
+
if (!_key) throw new Error("Session: Invalid JWS key. It must be a symmetric JWK or a public JWK/set.", { cause: key });
|
|
590
|
+
return _key;
|
|
591
|
+
}
|
|
592
|
+
function findSetCookie(setCookie, name) {
|
|
593
|
+
const regex = new RegExp(`(?:^|,\\s*)${name}=([^;]+)`);
|
|
594
|
+
const match = setCookie.match(regex);
|
|
595
|
+
return match ? match[1] : void 0;
|
|
596
|
+
}
|
|
597
|
+
//#endregion
|
|
598
|
+
export { useJWSSession as a, getJWESession as c, unsealJWESession as d, updateJWESession as f, updateJWSSession as i, getJWESessionToken as l, getJWSSession as n, verifyJWSSession as o, useJWESession as p, signJWSSession as r, clearJWESession as s, clearJWSSession as t, sealJWESession as u };
|