unjwt 0.7.1 → 0.7.3
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/dist/_chunks/_internal.mjs +5 -5
- package/dist/_chunks/index.d.mts +42 -17
- package/dist/_chunks/jwe.d.mts +2 -2
- package/dist/_chunks/jwe.mjs +5 -5
- package/dist/_chunks/jwk.mjs +51 -35
- package/dist/_chunks/jws.d.mts +2 -2
- package/dist/_chunks/jws.mjs +6 -6
- package/dist/_chunks/libs/unsecure.d.mts +1 -1
- package/dist/_chunks/libs/unsecure.mjs +3 -3
- package/dist/_chunks/utils.mjs +8 -6
- package/dist/adapters/elysia/index.d.mts +244 -0
- package/dist/adapters/elysia/index.mjs +608 -0
- package/dist/adapters/h3v1/index.d.mts +120 -42
- package/dist/adapters/h3v1/index.mjs +18 -15
- package/dist/adapters/h3v2/index.d.mts +140 -20
- package/dist/adapters/h3v2/index.mjs +18 -15
- package/dist/core/jwe.d.mts +3 -3
- package/dist/core/jwe.mjs +2 -2
- package/dist/core/jwk.d.mts +10 -10
- package/dist/core/jwk.mjs +2 -2
- package/dist/core/jws.d.mts +3 -3
- package/dist/core/jws.mjs +2 -2
- package/dist/core/utils/index.d.mts +24 -8
- package/dist/core/utils/index.mjs +2 -2
- package/dist/index.d.mts +4 -4
- package/dist/index.mjs +4 -4
- package/package.json +34 -21
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
import { computeDurationInSeconds, isJWTError, isPrivateJWK, isPublicJWK, isSymmetricJWK } from "../../_chunks/utils.mjs";
|
|
2
|
+
import { sanitizeObjectCopy } from "../../_chunks/libs/unsecure.mjs";
|
|
3
|
+
import { deriveJWKFromPassword, exportPEM, generateJWK, importPEM } from "../../_chunks/jwk.mjs";
|
|
4
|
+
import { sign, verify } from "../../_chunks/jws.mjs";
|
|
5
|
+
import { decrypt, encrypt } from "../../_chunks/jwe.mjs";
|
|
6
|
+
import { Elysia } from "elysia";
|
|
7
|
+
function guardName(contextKey) {
|
|
8
|
+
return `require${contextKey.charAt(0).toUpperCase()}${contextKey.slice(1)}`;
|
|
9
|
+
}
|
|
10
|
+
const CHUNK_MARKER = "__chunked__";
|
|
11
|
+
const DEFAULT_CHUNK_MAX_LENGTH = 4e3;
|
|
12
|
+
const MAX_CHUNK_COUNT = 100;
|
|
13
|
+
function readChunkedCookie(jar, name) {
|
|
14
|
+
const main = asString(jar[name]?.value);
|
|
15
|
+
if (main === void 0) return void 0;
|
|
16
|
+
if (!main.startsWith(CHUNK_MARKER)) return main;
|
|
17
|
+
const count = parseChunkCount(main);
|
|
18
|
+
if (count === 0) return void 0;
|
|
19
|
+
const parts = [];
|
|
20
|
+
for (let i = 1; i <= count; i++) {
|
|
21
|
+
const part = asString(jar[chunkName(name, i)]?.value);
|
|
22
|
+
if (part === void 0) return void 0;
|
|
23
|
+
parts.push(part);
|
|
24
|
+
}
|
|
25
|
+
return parts.join("");
|
|
26
|
+
}
|
|
27
|
+
function writeChunkedCookie(jar, name, value, options = {}) {
|
|
28
|
+
const { chunkMaxLength, ...attrs } = options;
|
|
29
|
+
const max = chunkMaxLength && chunkMaxLength > 0 ? chunkMaxLength : DEFAULT_CHUNK_MAX_LENGTH;
|
|
30
|
+
const chunkCount = Math.ceil(value.length / max);
|
|
31
|
+
if (chunkCount > MAX_CHUNK_COUNT) throw new Error(`[unjwt/elysia] Session token requires ${chunkCount} cookie chunks, exceeding the limit of ${MAX_CHUNK_COUNT}.`);
|
|
32
|
+
const previousCount = parseChunkCount(asString(jar[name]?.value));
|
|
33
|
+
const firstSurplus = chunkCount <= 1 ? 1 : chunkCount + 1;
|
|
34
|
+
for (let i = firstSurplus; i <= previousCount; i++) expireCookie(jar, chunkName(name, i), attrs);
|
|
35
|
+
if (chunkCount <= 1) {
|
|
36
|
+
jar[name].set({
|
|
37
|
+
...attrs,
|
|
38
|
+
value
|
|
39
|
+
});
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
jar[name].set({
|
|
43
|
+
...attrs,
|
|
44
|
+
value: `${CHUNK_MARKER}${chunkCount}`
|
|
45
|
+
});
|
|
46
|
+
for (let i = 1; i <= chunkCount; i++) {
|
|
47
|
+
const slice = value.slice((i - 1) * max, i * max);
|
|
48
|
+
jar[chunkName(name, i)].set({
|
|
49
|
+
...attrs,
|
|
50
|
+
value: slice
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function removeChunkedCookie(jar, name, attrs = {}) {
|
|
55
|
+
const count = parseChunkCount(asString(jar[name]?.value));
|
|
56
|
+
expireCookie(jar, name, attrs);
|
|
57
|
+
for (let i = 1; i <= count; i++) expireCookie(jar, chunkName(name, i), attrs);
|
|
58
|
+
}
|
|
59
|
+
function expireCookie(jar, fullName, attrs) {
|
|
60
|
+
jar[fullName].set({
|
|
61
|
+
...attrs,
|
|
62
|
+
value: "",
|
|
63
|
+
maxAge: 0,
|
|
64
|
+
expires: /* @__PURE__ */ new Date(0)
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
function chunkName(name, index) {
|
|
68
|
+
return `${name}.${index}`;
|
|
69
|
+
}
|
|
70
|
+
function parseChunkCount(main) {
|
|
71
|
+
if (main === void 0 || !main.startsWith(CHUNK_MARKER)) return 0;
|
|
72
|
+
const suffix = main.slice(11);
|
|
73
|
+
if (!/^\d+$/.test(suffix)) return 0;
|
|
74
|
+
const count = Number.parseInt(suffix, 10);
|
|
75
|
+
if (count < 1 || count > MAX_CHUNK_COUNT) return 0;
|
|
76
|
+
return count;
|
|
77
|
+
}
|
|
78
|
+
function asString(value) {
|
|
79
|
+
return typeof value === "string" ? value : void 0;
|
|
80
|
+
}
|
|
81
|
+
const DEFAULT_NAME$1 = "elysia-jws";
|
|
82
|
+
const DEFAULT_COOKIE$1 = {
|
|
83
|
+
path: "/",
|
|
84
|
+
secure: true,
|
|
85
|
+
httpOnly: false
|
|
86
|
+
};
|
|
87
|
+
async function createJWSSession(context, config) {
|
|
88
|
+
const state = emptySession$1(config);
|
|
89
|
+
await initFromToken$1(context, config, state);
|
|
90
|
+
return buildManager$1(context, config, state);
|
|
91
|
+
}
|
|
92
|
+
function jwsSession(config) {
|
|
93
|
+
const contextKey = config.contextKey ?? "session";
|
|
94
|
+
return new Elysia({
|
|
95
|
+
name: "unjwt/elysia-jws",
|
|
96
|
+
seed: contextKey
|
|
97
|
+
}).resolve({ as: "scoped" }, async ({ cookie, request }) => {
|
|
98
|
+
const session = await createJWSSession({
|
|
99
|
+
cookie,
|
|
100
|
+
request
|
|
101
|
+
}, config);
|
|
102
|
+
return { [contextKey]: session };
|
|
103
|
+
}).macro({ [guardName(contextKey)]: { resolve(ctx) {
|
|
104
|
+
if (!ctx[contextKey]?.id) return ctx.status(401, "Unauthorized");
|
|
105
|
+
return {};
|
|
106
|
+
} } });
|
|
107
|
+
}
|
|
108
|
+
function buildManager$1(context, config, state) {
|
|
109
|
+
const manager = {
|
|
110
|
+
get id() {
|
|
111
|
+
return state.id;
|
|
112
|
+
},
|
|
113
|
+
get createdAt() {
|
|
114
|
+
return state.createdAt;
|
|
115
|
+
},
|
|
116
|
+
get expiresAt() {
|
|
117
|
+
return state.expiresAt;
|
|
118
|
+
},
|
|
119
|
+
get data() {
|
|
120
|
+
return state.data;
|
|
121
|
+
},
|
|
122
|
+
get token() {
|
|
123
|
+
return state.token;
|
|
124
|
+
},
|
|
125
|
+
update: async (update) => {
|
|
126
|
+
await updateSession$1(context, config, state, update);
|
|
127
|
+
return manager;
|
|
128
|
+
},
|
|
129
|
+
clear: async () => {
|
|
130
|
+
await clearSession$1(context, config, state);
|
|
131
|
+
return manager;
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
return manager;
|
|
135
|
+
}
|
|
136
|
+
async function initFromToken$1(context, config, state) {
|
|
137
|
+
const token = readToken$1(context, config);
|
|
138
|
+
if (!token) return;
|
|
139
|
+
state.token = token;
|
|
140
|
+
let exclusiveHookFired = false;
|
|
141
|
+
try {
|
|
142
|
+
const unsealed = await verifySession(context, config, token);
|
|
143
|
+
Object.assign(state, unsealed);
|
|
144
|
+
} catch (error_) {
|
|
145
|
+
exclusiveHookFired = true;
|
|
146
|
+
if (isJWTError(error_, "ERR_JWT_EXPIRED")) await config.hooks?.onExpire?.({
|
|
147
|
+
session: {
|
|
148
|
+
id: error_.cause.jti,
|
|
149
|
+
createdAt: error_.cause.iat ? error_.cause.iat * 1e3 : void 0,
|
|
150
|
+
expiresAt: error_.cause.exp ? error_.cause.exp * 1e3 : void 0,
|
|
151
|
+
token
|
|
152
|
+
},
|
|
153
|
+
context,
|
|
154
|
+
error: error_,
|
|
155
|
+
config
|
|
156
|
+
});
|
|
157
|
+
else await config.hooks?.onError?.({
|
|
158
|
+
session: state,
|
|
159
|
+
context,
|
|
160
|
+
error: error_,
|
|
161
|
+
config
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
if (!exclusiveHookFired && state.id !== void 0 && state.token !== void 0) await config.hooks?.onRead?.({
|
|
165
|
+
session: state,
|
|
166
|
+
context,
|
|
167
|
+
config
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
async function updateSession$1(context, config, state, update) {
|
|
171
|
+
const oldSession = {
|
|
172
|
+
...state,
|
|
173
|
+
data: sanitizeObjectCopy(state.data)
|
|
174
|
+
};
|
|
175
|
+
const resolvedUpdate = typeof update === "function" ? update(state.data) : update;
|
|
176
|
+
if (resolvedUpdate) Object.assign(state.data, resolvedUpdate);
|
|
177
|
+
const now = config.jws?.signOptions?.currentDate?.getTime() ?? Date.now();
|
|
178
|
+
const createdAt = now - now % 1e3;
|
|
179
|
+
state.id = config.generateId?.() || crypto.randomUUID();
|
|
180
|
+
state.createdAt = createdAt;
|
|
181
|
+
state.expiresAt = config.maxAge === void 0 ? void 0 : createdAt + computeDurationInSeconds(config.maxAge) * 1e3;
|
|
182
|
+
let token;
|
|
183
|
+
try {
|
|
184
|
+
token = await signSession(config, state);
|
|
185
|
+
} catch (error_) {
|
|
186
|
+
state.id = oldSession.id;
|
|
187
|
+
state.createdAt = oldSession.createdAt;
|
|
188
|
+
state.expiresAt = oldSession.expiresAt;
|
|
189
|
+
state.data = oldSession.data;
|
|
190
|
+
state.token = oldSession.token;
|
|
191
|
+
await config.hooks?.onError?.({
|
|
192
|
+
session: state,
|
|
193
|
+
context,
|
|
194
|
+
error: error_,
|
|
195
|
+
config
|
|
196
|
+
});
|
|
197
|
+
throw error_;
|
|
198
|
+
}
|
|
199
|
+
state.token = token;
|
|
200
|
+
if (config.cookie !== false) {
|
|
201
|
+
const { chunkMaxLength, ...attrs } = {
|
|
202
|
+
...DEFAULT_COOKIE$1,
|
|
203
|
+
...config.cookie
|
|
204
|
+
};
|
|
205
|
+
writeChunkedCookie(context.cookie, config.name || DEFAULT_NAME$1, token, {
|
|
206
|
+
...attrs,
|
|
207
|
+
chunkMaxLength,
|
|
208
|
+
expires: config.maxAge === void 0 ? void 0 : new Date(createdAt + computeDurationInSeconds(config.maxAge) * 1e3)
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
await config.hooks?.onUpdate?.({
|
|
212
|
+
session: state,
|
|
213
|
+
oldSession,
|
|
214
|
+
context,
|
|
215
|
+
config
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
async function clearSession$1(context, config, state) {
|
|
219
|
+
const oldSession = state.id !== void 0 || state.token !== void 0 ? {
|
|
220
|
+
...state,
|
|
221
|
+
data: sanitizeObjectCopy(state.data)
|
|
222
|
+
} : void 0;
|
|
223
|
+
const fresh = emptySession$1(config);
|
|
224
|
+
state.id = fresh.id;
|
|
225
|
+
state.createdAt = fresh.createdAt;
|
|
226
|
+
state.expiresAt = fresh.expiresAt;
|
|
227
|
+
state.data = fresh.data;
|
|
228
|
+
state.token = fresh.token;
|
|
229
|
+
if (config.cookie !== false) {
|
|
230
|
+
const { chunkMaxLength: _chunkMaxLength, ...attrs } = {
|
|
231
|
+
...DEFAULT_COOKIE$1,
|
|
232
|
+
...config.cookie
|
|
233
|
+
};
|
|
234
|
+
removeChunkedCookie(context.cookie, config.name || DEFAULT_NAME$1, attrs);
|
|
235
|
+
}
|
|
236
|
+
await config.hooks?.onClear?.({
|
|
237
|
+
oldSession,
|
|
238
|
+
context,
|
|
239
|
+
config
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
async function signSession(config, state) {
|
|
243
|
+
const key = getSignKey(config.key);
|
|
244
|
+
const iat = Math.floor(state.createdAt / 1e3);
|
|
245
|
+
const exp = state.expiresAt ? Math.floor(state.expiresAt / 1e3) : void 0;
|
|
246
|
+
const payload = {
|
|
247
|
+
...state.data,
|
|
248
|
+
jti: state.id,
|
|
249
|
+
iat
|
|
250
|
+
};
|
|
251
|
+
if (exp) payload.exp = exp;
|
|
252
|
+
return sign(payload, key, {
|
|
253
|
+
...config.jws?.signOptions,
|
|
254
|
+
expiresIn: void 0,
|
|
255
|
+
protectedHeader: {
|
|
256
|
+
...config.jws?.signOptions?.protectedHeader,
|
|
257
|
+
kid: "kid" in key ? key.kid : void 0,
|
|
258
|
+
typ: resolveTyp$1(config) || "JWT",
|
|
259
|
+
cty: "application/json"
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
async function verifySession(context, config, token) {
|
|
264
|
+
const alg = config.jws?.signOptions?.alg;
|
|
265
|
+
const { payload } = await verify(token, config.hooks?.onVerifyKeyLookup ? (header) => config.hooks.onVerifyKeyLookup({
|
|
266
|
+
header,
|
|
267
|
+
context,
|
|
268
|
+
config
|
|
269
|
+
}) : getVerifyKey(config.key), {
|
|
270
|
+
...config.jws?.verifyOptions,
|
|
271
|
+
requiredClaims: [.../* @__PURE__ */ new Set([
|
|
272
|
+
...config.jws?.verifyOptions?.requiredClaims || [],
|
|
273
|
+
"jti",
|
|
274
|
+
"iat"
|
|
275
|
+
])],
|
|
276
|
+
typ: resolveTyp$1(config) || "JWT",
|
|
277
|
+
algorithms: alg ? [alg] : void 0,
|
|
278
|
+
forceUint8Array: false,
|
|
279
|
+
validateClaims: true
|
|
280
|
+
}).catch((error_) => {
|
|
281
|
+
if (isJWTError(error_, "ERR_JWT_EXPIRED")) throw error_;
|
|
282
|
+
const message = error_ instanceof Error ? error_.message : String(error_);
|
|
283
|
+
throw new Error(`Invalid session token: ${message}`);
|
|
284
|
+
});
|
|
285
|
+
const { jti, iat, exp, ...data } = payload;
|
|
286
|
+
return {
|
|
287
|
+
id: jti,
|
|
288
|
+
createdAt: iat * 1e3,
|
|
289
|
+
expiresAt: exp ? exp * 1e3 : void 0,
|
|
290
|
+
data: data && typeof data === "object" ? data : emptyData$1()
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
function emptySession$1(config) {
|
|
294
|
+
const now = config.jws?.signOptions?.currentDate?.getTime() ?? Date.now();
|
|
295
|
+
const createdAt = now - now % 1e3;
|
|
296
|
+
return {
|
|
297
|
+
id: void 0,
|
|
298
|
+
createdAt,
|
|
299
|
+
expiresAt: config.maxAge === void 0 ? void 0 : createdAt + computeDurationInSeconds(config.maxAge) * 1e3,
|
|
300
|
+
data: emptyData$1(),
|
|
301
|
+
token: void 0
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
function emptyData$1() {
|
|
305
|
+
return Object.create(null);
|
|
306
|
+
}
|
|
307
|
+
function readToken$1(context, config) {
|
|
308
|
+
const sessionName = config.name || DEFAULT_NAME$1;
|
|
309
|
+
if (config.sessionHeader !== false) {
|
|
310
|
+
const headerName = typeof config.sessionHeader === "string" ? config.sessionHeader.toLowerCase() : `x-${sessionName.toLowerCase()}-session`;
|
|
311
|
+
const headerValue = context.request.headers.get(headerName);
|
|
312
|
+
if (typeof headerValue === "string") return headerValue.startsWith("Bearer ") ? headerValue.slice(7).trim() : headerValue;
|
|
313
|
+
}
|
|
314
|
+
if (config.cookie !== false) return readChunkedCookie(context.cookie, sessionName);
|
|
315
|
+
}
|
|
316
|
+
function resolveTyp$1(config) {
|
|
317
|
+
const typ = config.jws?.signOptions?.protectedHeader?.typ;
|
|
318
|
+
if (typeof typ === "string" && typ.toLowerCase().includes("jwt")) return typ;
|
|
319
|
+
}
|
|
320
|
+
function getSignKey(key) {
|
|
321
|
+
if (!key) throw new Error("[unjwt/elysia] JWS key is required.");
|
|
322
|
+
let _key;
|
|
323
|
+
if (isSymmetricJWK(key)) _key = key;
|
|
324
|
+
else if ("privateKey" in key && isPrivateJWK(key.privateKey)) _key = key.privateKey;
|
|
325
|
+
if (!_key) throw new Error("[unjwt/elysia] Invalid JWS key. It must be a symmetric JWK or a private JWK.", { cause: key });
|
|
326
|
+
return _key;
|
|
327
|
+
}
|
|
328
|
+
function getVerifyKey(key) {
|
|
329
|
+
if (!key) throw new Error("[unjwt/elysia] JWS key is required.");
|
|
330
|
+
let _key;
|
|
331
|
+
if (isSymmetricJWK(key)) _key = key;
|
|
332
|
+
else if ("publicKey" in key) {
|
|
333
|
+
const publicKey = key.publicKey;
|
|
334
|
+
if (isPublicJWK(publicKey)) _key = publicKey;
|
|
335
|
+
else if (Array.isArray(publicKey)) {
|
|
336
|
+
const keys = publicKey.filter((candidate) => isPublicJWK(candidate));
|
|
337
|
+
if (keys.length > 0) _key = { keys };
|
|
338
|
+
} else if (publicKey && typeof publicKey === "object" && Array.isArray(publicKey.keys)) {
|
|
339
|
+
const keys = publicKey.keys.filter((candidate) => isPublicJWK(candidate));
|
|
340
|
+
if (keys.length > 0) _key = { keys };
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
if (!_key) throw new Error("[unjwt/elysia] Invalid JWS key. It must be a symmetric JWK or a public JWK/set.", { cause: key });
|
|
344
|
+
return _key;
|
|
345
|
+
}
|
|
346
|
+
const DEFAULT_NAME = "elysia-jwe";
|
|
347
|
+
const DEFAULT_COOKIE = {
|
|
348
|
+
path: "/",
|
|
349
|
+
secure: true,
|
|
350
|
+
httpOnly: true
|
|
351
|
+
};
|
|
352
|
+
async function createJWESession(context, config) {
|
|
353
|
+
const state = emptySession(config);
|
|
354
|
+
await initFromToken(context, config, state);
|
|
355
|
+
return buildManager(context, config, state);
|
|
356
|
+
}
|
|
357
|
+
function jweSession(config) {
|
|
358
|
+
const contextKey = config.contextKey ?? "session";
|
|
359
|
+
return new Elysia({
|
|
360
|
+
name: "unjwt/elysia-jwe",
|
|
361
|
+
seed: contextKey
|
|
362
|
+
}).resolve({ as: "scoped" }, async ({ cookie, request }) => {
|
|
363
|
+
const session = await createJWESession({
|
|
364
|
+
cookie,
|
|
365
|
+
request
|
|
366
|
+
}, config);
|
|
367
|
+
return { [contextKey]: session };
|
|
368
|
+
}).macro({ [guardName(contextKey)]: { resolve(ctx) {
|
|
369
|
+
if (!ctx[contextKey]?.id) return ctx.status(401, "Unauthorized");
|
|
370
|
+
return {};
|
|
371
|
+
} } });
|
|
372
|
+
}
|
|
373
|
+
function buildManager(context, config, state) {
|
|
374
|
+
const manager = {
|
|
375
|
+
get id() {
|
|
376
|
+
return state.id;
|
|
377
|
+
},
|
|
378
|
+
get createdAt() {
|
|
379
|
+
return state.createdAt;
|
|
380
|
+
},
|
|
381
|
+
get expiresAt() {
|
|
382
|
+
return state.expiresAt;
|
|
383
|
+
},
|
|
384
|
+
get data() {
|
|
385
|
+
return state.data;
|
|
386
|
+
},
|
|
387
|
+
get token() {
|
|
388
|
+
return state.token;
|
|
389
|
+
},
|
|
390
|
+
update: async (update) => {
|
|
391
|
+
await updateSession(context, config, state, update);
|
|
392
|
+
return manager;
|
|
393
|
+
},
|
|
394
|
+
clear: async () => {
|
|
395
|
+
await clearSession(context, config, state);
|
|
396
|
+
return manager;
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
return manager;
|
|
400
|
+
}
|
|
401
|
+
async function initFromToken(context, config, state) {
|
|
402
|
+
const token = readToken(context, config);
|
|
403
|
+
if (!token) return;
|
|
404
|
+
state.token = token;
|
|
405
|
+
let exclusiveHookFired = false;
|
|
406
|
+
try {
|
|
407
|
+
const unsealed = await unsealSession(context, config, token);
|
|
408
|
+
Object.assign(state, unsealed);
|
|
409
|
+
} catch (error_) {
|
|
410
|
+
exclusiveHookFired = true;
|
|
411
|
+
if (isJWTError(error_, "ERR_JWT_EXPIRED")) await config.hooks?.onExpire?.({
|
|
412
|
+
session: {
|
|
413
|
+
id: error_.cause.jti,
|
|
414
|
+
createdAt: error_.cause.iat ? error_.cause.iat * 1e3 : void 0,
|
|
415
|
+
expiresAt: error_.cause.exp ? error_.cause.exp * 1e3 : void 0,
|
|
416
|
+
token
|
|
417
|
+
},
|
|
418
|
+
context,
|
|
419
|
+
error: error_,
|
|
420
|
+
config
|
|
421
|
+
});
|
|
422
|
+
else await config.hooks?.onError?.({
|
|
423
|
+
session: state,
|
|
424
|
+
context,
|
|
425
|
+
error: error_,
|
|
426
|
+
config
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
if (!exclusiveHookFired && state.id !== void 0 && state.token !== void 0) await config.hooks?.onRead?.({
|
|
430
|
+
session: state,
|
|
431
|
+
context,
|
|
432
|
+
config
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
async function updateSession(context, config, state, update) {
|
|
436
|
+
const oldSession = {
|
|
437
|
+
...state,
|
|
438
|
+
data: sanitizeObjectCopy(state.data)
|
|
439
|
+
};
|
|
440
|
+
const resolvedUpdate = typeof update === "function" ? update(state.data) : update;
|
|
441
|
+
if (resolvedUpdate) Object.assign(state.data, resolvedUpdate);
|
|
442
|
+
const now = config.jwe?.encryptOptions?.currentDate?.getTime() ?? Date.now();
|
|
443
|
+
const createdAt = now - now % 1e3;
|
|
444
|
+
state.id = config.generateId?.() || crypto.randomUUID();
|
|
445
|
+
state.createdAt = createdAt;
|
|
446
|
+
state.expiresAt = config.maxAge === void 0 ? void 0 : createdAt + computeDurationInSeconds(config.maxAge) * 1e3;
|
|
447
|
+
let token;
|
|
448
|
+
try {
|
|
449
|
+
token = await sealSession(config, state);
|
|
450
|
+
} catch (error_) {
|
|
451
|
+
state.id = oldSession.id;
|
|
452
|
+
state.createdAt = oldSession.createdAt;
|
|
453
|
+
state.expiresAt = oldSession.expiresAt;
|
|
454
|
+
state.data = oldSession.data;
|
|
455
|
+
state.token = oldSession.token;
|
|
456
|
+
await config.hooks?.onError?.({
|
|
457
|
+
session: state,
|
|
458
|
+
context,
|
|
459
|
+
error: error_,
|
|
460
|
+
config
|
|
461
|
+
});
|
|
462
|
+
throw error_;
|
|
463
|
+
}
|
|
464
|
+
state.token = token;
|
|
465
|
+
if (config.cookie !== false) {
|
|
466
|
+
const { chunkMaxLength, ...attrs } = {
|
|
467
|
+
...DEFAULT_COOKIE,
|
|
468
|
+
...config.cookie
|
|
469
|
+
};
|
|
470
|
+
writeChunkedCookie(context.cookie, config.name || DEFAULT_NAME, token, {
|
|
471
|
+
...attrs,
|
|
472
|
+
chunkMaxLength,
|
|
473
|
+
expires: config.maxAge === void 0 ? void 0 : new Date(createdAt + computeDurationInSeconds(config.maxAge) * 1e3)
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
await config.hooks?.onUpdate?.({
|
|
477
|
+
session: state,
|
|
478
|
+
oldSession,
|
|
479
|
+
context,
|
|
480
|
+
config
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
async function clearSession(context, config, state) {
|
|
484
|
+
const oldSession = state.id !== void 0 || state.token !== void 0 ? {
|
|
485
|
+
...state,
|
|
486
|
+
data: sanitizeObjectCopy(state.data)
|
|
487
|
+
} : void 0;
|
|
488
|
+
const fresh = emptySession(config);
|
|
489
|
+
state.id = fresh.id;
|
|
490
|
+
state.createdAt = fresh.createdAt;
|
|
491
|
+
state.expiresAt = fresh.expiresAt;
|
|
492
|
+
state.data = fresh.data;
|
|
493
|
+
state.token = fresh.token;
|
|
494
|
+
if (config.cookie !== false) {
|
|
495
|
+
const { chunkMaxLength: _chunkMaxLength, ...attrs } = {
|
|
496
|
+
...DEFAULT_COOKIE,
|
|
497
|
+
...config.cookie
|
|
498
|
+
};
|
|
499
|
+
removeChunkedCookie(context.cookie, config.name || DEFAULT_NAME, attrs);
|
|
500
|
+
}
|
|
501
|
+
await config.hooks?.onClear?.({
|
|
502
|
+
oldSession,
|
|
503
|
+
context,
|
|
504
|
+
config
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
async function sealSession(config, state) {
|
|
508
|
+
const key = getEncryptKey(config.key);
|
|
509
|
+
const iat = Math.floor(state.createdAt / 1e3);
|
|
510
|
+
const exp = state.expiresAt ? Math.floor(state.expiresAt / 1e3) : void 0;
|
|
511
|
+
const payload = {
|
|
512
|
+
...state.data,
|
|
513
|
+
jti: state.id,
|
|
514
|
+
iat
|
|
515
|
+
};
|
|
516
|
+
if (exp) payload.exp = exp;
|
|
517
|
+
return encrypt(payload, key, {
|
|
518
|
+
...config.jwe?.encryptOptions,
|
|
519
|
+
expiresIn: void 0,
|
|
520
|
+
protectedHeader: {
|
|
521
|
+
...config.jwe?.encryptOptions?.protectedHeader,
|
|
522
|
+
typ: resolveTyp(config) || "JWT",
|
|
523
|
+
cty: "application/json"
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
async function unsealSession(context, config, sealed) {
|
|
528
|
+
const key = config.hooks?.onUnsealKeyLookup ? (header) => config.hooks.onUnsealKeyLookup({
|
|
529
|
+
header,
|
|
530
|
+
context,
|
|
531
|
+
config
|
|
532
|
+
}) : getDecryptKey(config.key);
|
|
533
|
+
const alg = config.jwe?.encryptOptions?.alg;
|
|
534
|
+
const enc = config.jwe?.encryptOptions?.enc;
|
|
535
|
+
const { payload } = await decrypt(sealed, key, {
|
|
536
|
+
...config.jwe?.decryptOptions,
|
|
537
|
+
requiredClaims: [.../* @__PURE__ */ new Set([
|
|
538
|
+
...config.jwe?.decryptOptions?.requiredClaims || [],
|
|
539
|
+
"jti",
|
|
540
|
+
"iat"
|
|
541
|
+
])],
|
|
542
|
+
typ: resolveTyp(config) || "JWT",
|
|
543
|
+
maxTokenAge: config.maxAge,
|
|
544
|
+
algorithms: alg ? [alg] : void 0,
|
|
545
|
+
encryptionAlgorithms: enc ? [enc] : void 0,
|
|
546
|
+
unwrappedKeyAlgorithm: void 0,
|
|
547
|
+
keyUsage: void 0,
|
|
548
|
+
forceUint8Array: false,
|
|
549
|
+
validateClaims: true
|
|
550
|
+
}).catch((error_) => {
|
|
551
|
+
if (error_ instanceof Error) throw error_;
|
|
552
|
+
throw new Error(String(error_));
|
|
553
|
+
});
|
|
554
|
+
const { jti, iat, exp, ...data } = payload;
|
|
555
|
+
return {
|
|
556
|
+
id: jti,
|
|
557
|
+
createdAt: iat * 1e3,
|
|
558
|
+
expiresAt: exp ? exp * 1e3 : void 0,
|
|
559
|
+
data: data && typeof data === "object" ? data : emptyData()
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
function emptySession(config) {
|
|
563
|
+
const now = config.jwe?.encryptOptions?.currentDate?.getTime() ?? Date.now();
|
|
564
|
+
const createdAt = now - now % 1e3;
|
|
565
|
+
return {
|
|
566
|
+
id: void 0,
|
|
567
|
+
createdAt,
|
|
568
|
+
expiresAt: config.maxAge === void 0 ? void 0 : createdAt + computeDurationInSeconds(config.maxAge) * 1e3,
|
|
569
|
+
data: emptyData(),
|
|
570
|
+
token: void 0
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
function emptyData() {
|
|
574
|
+
return Object.create(null);
|
|
575
|
+
}
|
|
576
|
+
function readToken(context, config) {
|
|
577
|
+
const sessionName = config.name || DEFAULT_NAME;
|
|
578
|
+
if (config.sessionHeader !== false) {
|
|
579
|
+
const headerName = typeof config.sessionHeader === "string" ? config.sessionHeader.toLowerCase() : `x-${sessionName.toLowerCase()}-session`;
|
|
580
|
+
const headerValue = context.request.headers.get(headerName);
|
|
581
|
+
if (typeof headerValue === "string") return headerValue.startsWith("Bearer ") ? headerValue.slice(7).trim() : headerValue;
|
|
582
|
+
}
|
|
583
|
+
if (config.cookie !== false) return readChunkedCookie(context.cookie, sessionName);
|
|
584
|
+
}
|
|
585
|
+
function resolveTyp(config) {
|
|
586
|
+
const typ = config.jwe?.encryptOptions?.protectedHeader?.typ;
|
|
587
|
+
if (typeof typ === "string" && typ.toLowerCase().includes("jwt")) return typ;
|
|
588
|
+
}
|
|
589
|
+
function getEncryptKey(key) {
|
|
590
|
+
if (!key) throw new Error("[unjwt/elysia] JWE key is required.");
|
|
591
|
+
let _key;
|
|
592
|
+
if (typeof key === "string") _key = key;
|
|
593
|
+
else if (isSymmetricJWK(key)) _key = key;
|
|
594
|
+
else if ("publicKey" in key && key.publicKey && isPublicJWK(key.publicKey)) _key = key.publicKey;
|
|
595
|
+
else if ("privateKey" in key && isPrivateJWK(key.privateKey)) _key = key.privateKey;
|
|
596
|
+
if (!_key) throw new Error("[unjwt/elysia] Invalid JWE key. It must be a password string or valid JWK.", { cause: key });
|
|
597
|
+
return _key;
|
|
598
|
+
}
|
|
599
|
+
function getDecryptKey(key) {
|
|
600
|
+
if (!key) throw new Error("[unjwt/elysia] JWE key is required.");
|
|
601
|
+
let _key;
|
|
602
|
+
if (typeof key === "string") _key = key;
|
|
603
|
+
else if (isSymmetricJWK(key)) _key = key;
|
|
604
|
+
else if ("privateKey" in key) _key = key.privateKey;
|
|
605
|
+
if (!_key) throw new Error("[unjwt/elysia] Invalid JWE key. It must be a password string or a valid private JWK.", { cause: key });
|
|
606
|
+
return _key;
|
|
607
|
+
}
|
|
608
|
+
export { createJWESession, createJWSSession, deriveJWKFromPassword, exportPEM, generateJWK, importPEM, jweSession, jwsSession };
|