unjwt 0.8.5 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +34 -21
  2. package/dist/_chunks/algorithms.mjs +44 -20
  3. package/dist/_chunks/digest.d.mts +4 -4
  4. package/dist/_chunks/digest.mjs +23 -15
  5. package/dist/_chunks/error.d.mts +29 -42
  6. package/dist/_chunks/export.mjs +1 -4
  7. package/dist/_chunks/index.d.mts +270 -234
  8. package/dist/_chunks/index2.d.mts +82 -189
  9. package/dist/_chunks/index4.d.mts +223 -60
  10. package/dist/_chunks/index5.d.mts +68 -31
  11. package/dist/_chunks/index6.d.mts +45 -64
  12. package/dist/_chunks/jwe.mjs +53 -48
  13. package/dist/_chunks/jwk.mjs +9 -3
  14. package/dist/_chunks/jws.mjs +55 -50
  15. package/dist/_chunks/jwt.d.mts +13 -49
  16. package/dist/_chunks/jwt.mjs +4 -158
  17. package/dist/_chunks/jwt2.mjs +162 -0
  18. package/dist/_chunks/libs/unsecure.d.mts +0 -6
  19. package/dist/_chunks/limits.d.mts +3 -2
  20. package/dist/_chunks/payload.mjs +13 -7
  21. package/dist/_chunks/wrap.mjs +3 -1
  22. package/dist/core/internal.d.mts +50 -2
  23. package/dist/core/internal.mjs +3 -1
  24. package/dist/core/jwe/index.d.mts +1 -1
  25. package/dist/core/jwk/index.d.mts +2 -2
  26. package/dist/core/jwk/index.mjs +2 -2
  27. package/dist/dpop/index.d.mts +77 -18
  28. package/dist/index.d.mts +6 -6
  29. package/dist/index.mjs +4 -4
  30. package/dist/session/index.d.mts +568 -22
  31. package/dist/session/index.mjs +1386 -34
  32. package/dist/utils/index.d.mts +4 -4
  33. package/dist/utils/index.mjs +2 -10
  34. package/package.json +4 -32
  35. package/dist/_chunks/codec-jwe.d.mts +0 -280
  36. package/dist/_chunks/codec-jws.mjs +0 -1005
  37. package/dist/_chunks/cookie-attributes.d.mts +0 -12
  38. package/dist/_chunks/cookie.mjs +0 -50
  39. package/dist/adapters/elysia/index.d.mts +0 -123
  40. package/dist/adapters/elysia/index.mjs +0 -120
  41. package/dist/adapters/h3/index.d.mts +0 -135
  42. package/dist/adapters/h3/index.mjs +0 -117
  43. package/dist/adapters/h3v1/index.d.mts +0 -148
  44. package/dist/adapters/h3v1/index.mjs +0 -128
@@ -1,73 +1,1425 @@
1
- import { thumbprint } from "../_chunks/algorithms.mjs";
1
+ import { safeJsonParse } from "../_chunks/libs/unsecure.mjs";
2
+ import { JWTError, isJWTError } from "../error.mjs";
3
+ import { computeDurationInSeconds, expiredClaimMessage, isAsymmetricJWK, isCryptoKey, isJWK, isJWKSet, isPlainObject, isPrivateJWK, isPublicJWK, isSymmetricJWK } from "../_chunks/jwt.mjs";
4
+ import { quoteTokenValue } from "../_chunks/jwt2.mjs";
5
+ import { importKey, resolveLimit, thumbprint } from "../_chunks/algorithms.mjs";
6
+ import { PBES2_ALGORITHMS, inferJWEAllowedAlgorithms, inferJWSAllowedAlgorithms, isContentEncryptionAlgorithm, lengthOf } from "../_chunks/digest.mjs";
7
+ import { sign, verify } from "../_chunks/jws.mjs";
8
+ import { decrypt, encrypt } from "../_chunks/jwe.mjs";
2
9
  import { toPublicJWK } from "../_chunks/export.mjs";
3
10
  import { deriveJWKFromPassword, exportPEM, generateJWK, importPEM } from "../_chunks/jwk.mjs";
4
- import { createJWECodec, createJWSCodec, defineSession, invalidConfigError } from "../_chunks/codec-jws.mjs";
5
- import { webCookieJar } from "../_chunks/cookie.mjs";
6
- const PREFIX = "[unjwt/session]";
7
- const SESSIONS = /* @__PURE__ */ new WeakMap();
8
- const webSessionDriver = {
9
- prefix: PREFIX,
10
- state: (event) => {
11
- if (isHanded(event)) return sessionsOf(event);
12
- if (event.context) return contextSessions(event.context);
13
- return sessionsOf(event.req ? webRequest(event.req) : event);
11
+ import { parse, parseSetCookie, serialize } from "../_chunks/libs/cookie-es.mjs";
12
+ const RESERVED_CLAIMS = [
13
+ "jti",
14
+ "iat",
15
+ "exp",
16
+ "nbf"
17
+ ];
18
+ const SESSION_TYP = "session+jwt";
19
+ const FORBIDDEN_KEYS = [
20
+ "__proto__",
21
+ "constructor",
22
+ "prototype"
23
+ ];
24
+ const importedKeys = /* @__PURE__ */ new WeakMap();
25
+ function invalidKeyError(message, cause) {
26
+ return new JWTError(message, "ERR_SESSION_KEY_INVALID", cause);
27
+ }
28
+ function invalidConfigError(message, cause) {
29
+ return new JWTError(message, "ERR_SESSION_CONFIG_INVALID", cause);
30
+ }
31
+ function invalidTokenError(message, cause) {
32
+ return new JWTError(message, "ERR_SESSION_TOKEN_INVALID", cause);
33
+ }
34
+ function readOnlyEventError(message) {
35
+ return new JWTError(message, "ERR_SESSION_READONLY_EVENT");
36
+ }
37
+ function expiredSessionError(options) {
38
+ const { id, createdAt, expiresAt } = options;
39
+ const exp = Math.floor(expiresAt / 1e3);
40
+ const iat = createdAt === void 0 ? void 0 : Math.floor(createdAt / 1e3);
41
+ return new JWTError(expiredClaimMessage(exp), "ERR_JWT_EXPIRED", {
42
+ jti: id,
43
+ iat,
44
+ exp
45
+ });
46
+ }
47
+ function readFailedError(cause, options) {
48
+ return new JWTError(`${options.prefix} Session could not be read: ${messageOf(cause)}`, "ERR_SESSION_READ_FAILED", cause);
49
+ }
50
+ function malformedHeaderError(header, options) {
51
+ return invalidTokenError(`${options.prefix} Invalid session token: the "${header}" header holds neither a token nor "Bearer <token>".`);
52
+ }
53
+ function rethrowAsInvalidToken(error, options) {
54
+ if (isJWTError(error, "ERR_JWT_EXPIRED") || isJWTError(error, "ERR_SESSION_READ_FAILED")) throw error;
55
+ throw invalidTokenError(`${options.prefix} Invalid session token: ${messageOf(error)}`, error);
56
+ }
57
+ const TOKEN = /^[!#$%&'*+\-.^_`|~\w]+$/;
58
+ const CHUNK_SUFFIX = /\.\d+$/;
59
+ function assertSessionName(name, options) {
60
+ if (!isToken(name)) throw invalidConfigError(`${options.prefix} Session "name" must be a valid cookie name (got ${quoteTokenValue(name)}).`);
61
+ if (CHUNK_SUFFIX.test(name)) throw invalidConfigError(`${options.prefix} Session "name" ${quoteTokenValue(name)} cannot end in a dot and digits.`);
62
+ }
63
+ function isToken(value) {
64
+ return typeof value === "string" && TOKEN.test(value);
65
+ }
66
+ function assertCookiePrefix(name, attrs, options) {
67
+ const lower = name.toLowerCase();
68
+ const host = lower.startsWith("__host-");
69
+ if (!host && !lower.startsWith("__secure-")) return;
70
+ const rule = host ? "\"__Host-\" cookie must be Secure, with Path=/ and no Domain" : "\"__Secure-\" cookie must be Secure";
71
+ if (!attrs.secure || host && (attrs.domain !== void 0 || attrs.path !== void 0 && attrs.path !== "/")) throw invalidConfigError(`${options.prefix} Session ${quoteTokenValue(name)}: a ${rule}.`);
72
+ }
73
+ function assertRolling(config, options) {
74
+ const { rolling } = config;
75
+ if (rolling === void 0) return;
76
+ if (config.maxAge === void 0) throw invalidConfigError(`${options.prefix} "rolling" needs a "maxAge" to extend.`);
77
+ if (rolling === true) return;
78
+ if (!isExpiresIn(rolling)) throw new JWTError(`${options.prefix} "rolling" must be true or a duration.`, "ERR_INVALID_OPTION");
79
+ if (computeDurationInSeconds(rolling) >= computeDurationInSeconds(config.maxAge)) throw invalidConfigError(`${options.prefix} "rolling" must be shorter than "maxAge".`);
80
+ }
81
+ function assertKeyMaterial(key, options) {
82
+ if (typeof key === "string") throw invalidKeyError(`${options.prefix} Session "key" cannot be a string, use deriveJWKFromPassword().`);
83
+ if (!key) throw invalidKeyError(`${options.prefix} Session "key" is required.`);
84
+ }
85
+ async function assertImportableKey(key, options) {
86
+ if (!isJWK(key)) return;
87
+ const alg = key.alg ?? options.alg;
88
+ if (key.kty !== "oct" && !alg) return;
89
+ let imports = importedKeys.get(key);
90
+ if (!imports) {
91
+ imports = /* @__PURE__ */ new Map();
92
+ importedKeys.set(key, imports);
93
+ }
94
+ const slot = alg ?? "";
95
+ let pending = imports.get(slot);
96
+ if (!pending) {
97
+ pending = importKey(key, { alg }).then(() => void 0);
98
+ imports.set(slot, pending);
99
+ }
100
+ try {
101
+ await pending;
102
+ } catch (error) {
103
+ if (imports.get(slot) === pending) imports.delete(slot);
104
+ throw invalidKeyError(`${options.prefix} Invalid session key: ${messageOf(error)}`, error);
105
+ }
106
+ }
107
+ function sessionTyp(configured) {
108
+ return typeof configured === "string" && configured ? configured : SESSION_TYP;
109
+ }
110
+ function resolveExpiresAt(createdAt, options) {
111
+ return options.maxAge === void 0 ? void 0 : createdAt + computeDurationInSeconds(options.maxAge) * 1e3;
112
+ }
113
+ function sessionRequiredClaims(configured, options) {
114
+ const required = /* @__PURE__ */ new Set([
115
+ ...configured || [],
116
+ "jti",
117
+ "iat"
118
+ ]);
119
+ if (options.maxAge !== void 0) required.add("exp");
120
+ return [...required];
121
+ }
122
+ function sanitizeSessionUpdate(update, options) {
123
+ if (!isPlainObject(update)) throw new JWTError(`${options.prefix} Session data must be a plain object.`, "ERR_INVALID_OPTION");
124
+ const sanitized = Object.create(null);
125
+ for (const key of Object.keys(update)) {
126
+ if (FORBIDDEN_KEYS.includes(key)) throw new JWTError(`${options.prefix} Session data cannot carry the key "${key}".`, "ERR_INVALID_OPTION");
127
+ if (RESERVED_CLAIMS.includes(key)) continue;
128
+ sanitized[key] = update[key];
129
+ }
130
+ return sanitized;
131
+ }
132
+ function messageOf(error) {
133
+ return error instanceof Error ? error.message : String(error);
134
+ }
135
+ function isExpiresIn(value) {
136
+ return typeof value === "number" || typeof value === "string";
137
+ }
138
+ function createSessionCodec(spec) {
139
+ const { prefix } = spec;
140
+ const now = (config) => spec.issueOptions(config)?.currentDate?.getTime() ?? Date.now();
141
+ const openWith = async (config, token, key, opening) => {
142
+ const options = spec.openOptions(config);
143
+ const payload = await spec.open(config, token, key, sessionOpenOptions({
144
+ maxAge: config.maxAge,
145
+ requiredClaims: options?.requiredClaims,
146
+ typ: spec.issueOptions(config)?.protectedHeader?.typ,
147
+ algorithms: opening.algorithms
148
+ })).catch((error) => rethrowOpenFailure(error, {
149
+ lookup: opening.lookup,
150
+ prefix
151
+ }));
152
+ const clock = {
153
+ now: now(config),
154
+ clockTolerance: options?.clockTolerance
155
+ };
156
+ assertNotFromTheFuture(payload, {
157
+ ...clock,
158
+ prefix
159
+ });
160
+ const opened = openedSession(payload, { maxAge: config.maxAge });
161
+ assertNotPastMaxAge(opened, clock);
162
+ return opened;
163
+ };
164
+ return {
165
+ family: spec.family,
166
+ defaultName: spec.defaultName,
167
+ now,
168
+ keys: (config) => {
169
+ const { issueKey, issueAlg, key, algorithms } = spec.resolveKeys(config);
170
+ const hook = spec.lookup(config);
171
+ const keyFor = (args) => hook ? ({ header }) => lookUpKey(hook, {
172
+ header,
173
+ ...args,
174
+ config
175
+ }, { prefix }) : key;
176
+ const importable = hook ? [] : isJWKSet(key) ? key.keys : [key];
177
+ const opening = {
178
+ algorithms,
179
+ lookup: hook !== void 0
180
+ };
181
+ const assertOpenKeys = async () => {
182
+ for (const entry of importable) await assertImportableKey(entry, {
183
+ alg: void 0,
184
+ prefix
185
+ });
186
+ };
187
+ return {
188
+ assertKeys: async () => {
189
+ await assertImportableKey(issueKey, {
190
+ alg: issueAlg ?? spec.issueOptions(config)?.alg,
191
+ prefix
192
+ });
193
+ await assertOpenKeys();
194
+ },
195
+ issue: (state) => spec.issue(config, sessionPayload(state), issueKey, issueAlg),
196
+ opener: (args) => {
197
+ const keyed = keyFor(args);
198
+ return (token) => openWith(config, token, keyed, opening);
199
+ },
200
+ open: async (token, args) => {
201
+ await assertOpenKeys();
202
+ return openWith(config, token, keyFor(args), opening);
203
+ }
204
+ };
205
+ }
206
+ };
207
+ }
208
+ function sessionIssueOptions(options) {
209
+ return Object.assign({}, options, {
210
+ expiresIn: void 0,
211
+ expiresAt: void 0,
212
+ notBeforeIn: void 0,
213
+ notBeforeAt: void 0,
214
+ protectedHeader: {
215
+ ...options?.protectedHeader,
216
+ typ: sessionTyp(options?.protectedHeader?.typ)
217
+ }
218
+ });
219
+ }
220
+ function sessionOpenOptions(options) {
221
+ const { maxAge, requiredClaims, typ, algorithms } = options;
222
+ return {
223
+ requiredClaims: sessionRequiredClaims(requiredClaims, { maxAge }),
224
+ typ: sessionTyp(typ),
225
+ maxTokenAge: void 0,
226
+ algorithms,
227
+ payload: "json",
228
+ validateClaims: true
229
+ };
230
+ }
231
+ function sessionPayload(state) {
232
+ const iat = Math.floor(state.createdAt / 1e3);
233
+ const exp = state.expiresAt ? Math.floor(state.expiresAt / 1e3) : void 0;
234
+ const payload = {};
235
+ for (const key of Object.keys(state.data)) if (!RESERVED_CLAIMS.includes(key)) payload[key] = state.data[key];
236
+ payload.jti = state.id;
237
+ payload.iat = iat;
238
+ if (exp) payload.exp = exp;
239
+ return payload;
240
+ }
241
+ function assertNotFromTheFuture(payload, options) {
242
+ if (payload.iat > Math.floor(options.now / 1e3) + toleranceSeconds(options.clockTolerance)) throw invalidTokenError(`${options.prefix} Invalid session token: "iat" is in the future (${(/* @__PURE__ */ new Date(payload.iat * 1e3)).toISOString()}).`);
243
+ }
244
+ function assertNotPastMaxAge(opened, options) {
245
+ const { expiresAt } = opened;
246
+ if (expiresAt === void 0) return;
247
+ if (expiresAt <= options.now - toleranceSeconds(options.clockTolerance) * 1e3) throw expiredSessionError({
248
+ id: opened.id,
249
+ createdAt: opened.createdAt,
250
+ expiresAt
251
+ });
252
+ }
253
+ async function lookUpKey(hook, args, options) {
254
+ try {
255
+ return await hook(args);
256
+ } catch (error) {
257
+ throw readFailedError(error, options);
258
+ }
259
+ }
260
+ function rethrowOpenFailure(error, options) {
261
+ if (options.lookup && isJWTError(error, "ERR_JWK_INVALID")) throw readFailedError(error, options);
262
+ return rethrowAsInvalidToken(error, options);
263
+ }
264
+ function toleranceSeconds(clockTolerance) {
265
+ return clockTolerance === void 0 ? 0 : computeDurationInSeconds(clockTolerance);
266
+ }
267
+ function openedSession(payload, options) {
268
+ const data = Object.create(null);
269
+ for (const key of Object.keys(payload)) {
270
+ if (RESERVED_CLAIMS.includes(key)) continue;
271
+ data[key] = payload[key];
272
+ }
273
+ const createdAt = payload.iat * 1e3;
274
+ const exp = payload.exp ? payload.exp * 1e3 : void 0;
275
+ const bound = resolveExpiresAt(createdAt, options);
276
+ return {
277
+ id: payload.jti,
278
+ createdAt,
279
+ expiresAt: exp === void 0 || bound === void 0 ? exp ?? bound : Math.min(exp, bound),
280
+ data
281
+ };
282
+ }
283
+ function assertJWSKeyPair(pair, prefix) {
284
+ const { privateKey, publicKey } = pair;
285
+ if (!isPrivateJWK(privateKey) && !isCryptoKeyOf(privateKey, "private")) throw invalidKeyError(`${prefix} Invalid JWS session key: "privateKey" must be a private JWK or a private CryptoKey.`, pair);
286
+ if (!isPublicHalf(publicKey)) throw invalidKeyError(`${prefix} Invalid JWS session key: "publicKey" must be a public JWK, a non-empty list or JWK Set of them, or a public CryptoKey.`, pair);
287
+ }
288
+ function assertVerifiesIssued(entries, options) {
289
+ const { alg, by, list, rules, prefix } = options;
290
+ const served = entries.map(({ key }) => rules.own(key) ?? rules.carried(key));
291
+ if (served.some((algorithms) => algorithms.includes(alg))) return;
292
+ const [{ key }] = entries;
293
+ const own = list ? void 0 : rules.own(key);
294
+ throw invalidKeyError(`${prefix} The session key "publicKey" ${own ? `names "${(isJWK(key) ? key.alg : void 0) ?? own[0]}"` : `serves ${[...new Set(served.flat())].join(", ") || "no algorithm"}`}, not the "${alg}" that ${by}.`, list ? entries.map((entry) => entry.key) : key);
295
+ }
296
+ function isPublicHalf(half) {
297
+ if (isCryptoKeyOf(half, "public") || isPublicJWK(half)) return true;
298
+ const keys = Array.isArray(half) ? half : isJWKSet(half) ? half.keys : void 0;
299
+ return keys !== void 0 && keys.length > 0 && keys.every((candidate) => isPublicJWK(candidate));
300
+ }
301
+ function isCryptoKeyOf(key, type) {
302
+ return isCryptoKey(key) && key.type === type;
303
+ }
304
+ function sessionKeySetEntries(keys, options) {
305
+ const { rules, prefix } = options;
306
+ if (keys.length === 0) throw invalidKeyError(`${prefix} The session key is an empty JWK Set.`);
307
+ const entries = keys.map((key, index) => {
308
+ const subject = `The session key "keys[${index}]"${kidLabel(key)}`;
309
+ if (!isSymmetricJWK(key)) throw invalidKeyError(`${prefix} ${subject} must be an "oct" JWK.`, key);
310
+ return {
311
+ key,
312
+ subject,
313
+ pin: index ? void 0 : options.pin
314
+ };
315
+ });
316
+ const limit = resolveLimit(options.limits, "keyCandidates");
317
+ for (const entry of entries) {
318
+ if (entry.key.kid !== void 0) continue;
319
+ for (const alg of servedBy(entry, options)) {
320
+ const met = entries.filter(({ key }) => rules.own(key)?.includes(alg) ?? true).length;
321
+ if (met > limit) throw invalidConfigError(`${prefix} ${entry.subject} carries no "kid", so a token of "${alg}" meets ${met} keys, over the limit of ${limit} ("${rules.limitPath}").`);
322
+ }
323
+ }
324
+ return entries;
325
+ }
326
+ function assertPinnedKey(entry, options) {
327
+ const { rules, prefix } = options;
328
+ const { key, pin, subject } = entry;
329
+ const own = rules.own(key);
330
+ if (own === void 0 && (!pin || !options.pinned)) throw invalidConfigError(`${prefix} ${subject} names no algorithm, set ${rules.hint}${isJWK(key) ? ", or its \"alg\"" : ""}.`);
331
+ const served = own ?? rules.carried(key);
332
+ if (!pin || served.includes(pin)) return;
333
+ throw invalidKeyError(`${prefix} ${subject} serves ${served.join(", ") || "no algorithm"}, not the "${pin}" that ${entry.pinnedBy ?? `"${rules.pinPath}" pins`}.`, key);
334
+ }
335
+ function servedAlgorithms(entries, options) {
336
+ const { rules, listed, prefix } = options;
337
+ const { listPath } = rules;
338
+ if (listed?.length === 0) throw invalidConfigError(`${prefix} "${listPath}" is empty, list the algorithms the session keys serve or leave it unset.`);
339
+ const all = /* @__PURE__ */ new Set();
340
+ const each = entries.map((entry) => {
341
+ const served = servedBy(entry, options);
342
+ if (served.length === 0) throw unservedKeyError(entry, {
343
+ rules,
344
+ prefix
345
+ });
346
+ for (const alg of served) all.add(alg);
347
+ return served;
348
+ });
349
+ for (const alg of listed ?? []) {
350
+ if (all.has(alg)) continue;
351
+ if (!options.lookup) throw invalidConfigError(`${prefix} "${listPath}" lists "${alg}", which no session key serves. Remove it, or add a key for it.`);
352
+ all.add(alg);
353
+ }
354
+ return {
355
+ each,
356
+ all: [...all]
357
+ };
358
+ }
359
+ function isKeyPair(key) {
360
+ return typeof key === "object" && key !== null && ("privateKey" in key || "publicKey" in key);
361
+ }
362
+ function kidLabel(key) {
363
+ return isJWK(key) && key.kid !== void 0 ? ` (kid ${quoteTokenValue(key.kid)})` : "";
364
+ }
365
+ function octBits(key) {
366
+ return Math.floor(key.k.length * 6 / 8) * 8;
367
+ }
368
+ function servedBy(entry, options) {
369
+ const { rules, listed } = options;
370
+ return rules.own(entry.key) ?? rules.carried(entry.key).filter((alg) => alg === entry.pin || listed?.includes(alg));
371
+ }
372
+ function unservedKeyError(entry, options) {
373
+ const { rules, prefix } = options;
374
+ const carried = rules.own(entry.key) ? [] : rules.carried(entry.key);
375
+ if (carried.length === 0) return invalidKeyError(`${prefix} ${entry.subject} serves no algorithm.`, entry.key);
376
+ return invalidConfigError(`${prefix} ${entry.subject} names no algorithm, list one it serves (${carried.join(", ")}) in "${rules.listPath}", or set its "alg".`);
377
+ }
378
+ const JWS_KEY_RULES = {
379
+ own: (key) => {
380
+ if (!isJWK(key)) return void 0;
381
+ const inferred = inferJWSAllowedAlgorithms(key);
382
+ return key.alg ? inferred ?? [] : inferred;
14
383
  },
15
- header: (event, name) => isHanded(event) ? void 0 : requestHeaders(event).get(name),
16
- cookies: (event) => isHanded(event) ? { read: () => void 0 } : webCookieJar(requestHeaders(event), event.res),
17
- handed: (event) => isHanded(event) ? { token: event.token } : void 0
384
+ carried: (key) => {
385
+ if (isCryptoKey(key)) return inferJWSAllowedAlgorithms(key) ?? [];
386
+ if (isJWK(key) && key.kty === "RSA") return RSA_SIGNATURES;
387
+ if (!isSymmetricJWK(key)) return [];
388
+ const bits = octBits(key);
389
+ return HMAC_SIGNATURES.filter((alg) => bits >= Number(alg.slice(2)));
390
+ },
391
+ pinPath: "jws.signOptions.alg",
392
+ hint: "\"jws.signOptions.alg\"",
393
+ listPath: "jws.verifyOptions.algorithms",
394
+ limitPath: "jws.verifyOptions.limits.keyCandidates"
18
395
  };
19
- function isHanded(event) {
396
+ function resolveSessionKeys$1(key, options) {
397
+ const { alg: pin, prefix } = options;
398
+ assertKeyMaterial(key, { prefix });
399
+ let issuing;
400
+ let entries;
401
+ let list;
402
+ let issued;
403
+ if (isCryptoKey(key) && key.type === "secret" || isSymmetricJWK(key)) {
404
+ issuing = {
405
+ key,
406
+ subject: "The session key",
407
+ pin
408
+ };
409
+ entries = [issuing];
410
+ } else if (isJWKSet(key)) {
411
+ const set = sessionKeySetEntries(key.keys, {
412
+ ...options,
413
+ pin,
414
+ rules: JWS_KEY_RULES
415
+ });
416
+ [issuing] = set;
417
+ entries = set;
418
+ } else if (isKeyPair(key)) {
419
+ assertJWSKeyPair(key, prefix);
420
+ const half = key.publicKey;
421
+ list = Array.isArray(half) ? half : isJWKSet(half) ? half.keys : void 0;
422
+ const signing = namedAlgorithm(key.privateKey);
423
+ const alg = pin ?? signing;
424
+ if (alg) issued = {
425
+ alg,
426
+ by: pin ? `"${JWS_KEY_RULES.pinPath}" pins` : "the session key \"privateKey\" names"
427
+ };
428
+ issuing = {
429
+ key: key.privateKey,
430
+ subject: "The session key \"privateKey\"",
431
+ ...pairPin(key.privateKey, {
432
+ pin,
433
+ named: list ? void 0 : namedAlgorithm(half),
434
+ by: "publicKey"
435
+ })
436
+ };
437
+ entries = (list ?? [half]).map((entry, index) => ({
438
+ key: entry,
439
+ subject: list ? `The session key "publicKey[${index}]"${kidLabel(entry)}` : "The session key \"publicKey\"",
440
+ ...pairPin(entry, {
441
+ pin,
442
+ named: signing,
443
+ by: "privateKey"
444
+ })
445
+ }));
446
+ } else throw invalidJWSKeyError(key, prefix);
447
+ const pinning = {
448
+ rules: JWS_KEY_RULES,
449
+ pinned: true,
450
+ prefix
451
+ };
452
+ assertPinnedKey(issuing, pinning);
453
+ if (!list && issuing !== entries[0]) assertPinnedKey(entries[0], pinning);
454
+ if (issued) assertVerifiesIssued(entries, {
455
+ ...issued,
456
+ list: list !== void 0,
457
+ rules: JWS_KEY_RULES,
458
+ prefix
459
+ });
460
+ const { each, all } = servedAlgorithms(entries, {
461
+ ...options,
462
+ rules: JWS_KEY_RULES
463
+ });
464
+ const verifying = isJWKSet(key) ? key : list ? { keys: entries.flatMap(({ key: entry }, index) => servedCopies(entry, each[index])) } : entries[0].key;
465
+ const issueAlg = issuing.pin;
466
+ return {
467
+ issueKey: issuing.key,
468
+ issueAlg,
469
+ key: verifying,
470
+ algorithms: all
471
+ };
472
+ }
473
+ function namedAlgorithm(key) {
474
+ return isJWK(key) ? key.alg ?? JWS_KEY_RULES.own(key)?.[0] : void 0;
475
+ }
476
+ function pairPin(key, options) {
477
+ const { pin, named, by } = options;
478
+ if (pin || !named || JWS_KEY_RULES.own(key)) return { pin };
479
+ return {
480
+ pin: named,
481
+ pinnedBy: `the session key "${by}" names`
482
+ };
483
+ }
484
+ function servedCopies(key, served) {
485
+ if (!isJWK(key)) return [];
486
+ if (JWS_KEY_RULES.own(key)) return [key];
487
+ return served.map((alg) => ({
488
+ ...key,
489
+ alg
490
+ }));
491
+ }
492
+ function invalidJWSKeyError(key, prefix) {
493
+ return invalidKeyError(isCryptoKey(key) || isAsymmetricJWK(key) ? `${prefix} Invalid JWS session key: an asymmetric key signs with its private half and verifies with its public half, pass both as { privateKey, publicKey }.` : `${prefix} Invalid JWS session key: expected an HMAC "oct" JWK, a JWK Set of them, a secret CryptoKey or a key pair.`, key);
494
+ }
495
+ const HMAC_SIGNATURES = [
496
+ "HS256",
497
+ "HS384",
498
+ "HS512"
499
+ ];
500
+ const RSA_SIGNATURES = [
501
+ "RS256",
502
+ "RS384",
503
+ "RS512",
504
+ "PS256",
505
+ "PS384",
506
+ "PS512"
507
+ ];
508
+ function createJWSCodec(options) {
509
+ const { prefix, defaultName } = options;
510
+ return createSessionCodec({
511
+ family: "jws",
512
+ prefix,
513
+ defaultName,
514
+ issueOptions: (config) => config.jws?.signOptions,
515
+ openOptions: (config) => config.jws?.verifyOptions,
516
+ resolveKeys: (config) => resolveSessionKeys$1(config.key, {
517
+ alg: config.jws?.signOptions?.alg,
518
+ listed: config.jws?.verifyOptions?.algorithms,
519
+ limits: config.jws?.verifyOptions?.limits,
520
+ lookup: typeof config.hooks?.onKeyLookup === "function",
521
+ prefix
522
+ }),
523
+ lookup: (config) => config.hooks?.onKeyLookup,
524
+ issue: (config, payload, key, alg) => {
525
+ const signOptions = config.jws?.signOptions;
526
+ const issuing = sessionIssueOptions(alg ? {
527
+ ...signOptions,
528
+ alg
529
+ } : signOptions);
530
+ return sign(payload, key, issuing);
531
+ },
532
+ open: async (config, token, key, overrides) => {
533
+ const { payload } = await verify(token, key, {
534
+ ...config.jws?.verifyOptions,
535
+ ...overrides
536
+ });
537
+ return payload;
538
+ }
539
+ });
540
+ }
541
+ const DEFAULT_SESSION_COOKIE = /* @__PURE__ */ Object.freeze({
542
+ path: "/",
543
+ secure: true,
544
+ httpOnly: true,
545
+ sameSite: "lax"
546
+ });
547
+ const kSessionPending = Symbol("unjwt_session_pending");
548
+ function sessionInstant(source) {
549
+ const now = source.codec.now(source.config);
550
+ return now - now % 1e3;
551
+ }
552
+ function mintSessionId(config) {
553
+ return config.generateId?.() || crypto.randomUUID();
554
+ }
555
+ function emptySession(binding) {
556
+ const createdAt = sessionInstant(binding);
557
+ return typedSession({
558
+ id: void 0,
559
+ createdAt,
560
+ expiresAt: resolveExpiresAt(createdAt, { maxAge: binding.config.maxAge }),
561
+ data: Object.create(null),
562
+ token: void 0,
563
+ source: binding.transport.source,
564
+ error: void 0
565
+ });
566
+ }
567
+ function storedSession(store) {
568
+ const state = store.get();
569
+ return state === void 0 ? void 0 : typedSession(state);
570
+ }
571
+ function blankSession() {
572
+ return typedSession({
573
+ expiresAt: void 0,
574
+ data: {}
575
+ });
576
+ }
577
+ function openSessionToken(keys, options) {
578
+ return keys.open(options.token, options.args).then((opened) => typedOpened({
579
+ ...opened,
580
+ source: "token"
581
+ }));
582
+ }
583
+ function typedSession(state) {
584
+ return state;
585
+ }
586
+ function typedOpened(opened) {
587
+ return opened;
588
+ }
589
+ const CHUNK_MARKER = "__chunked__";
590
+ const DEFAULT_CHUNK_MAX_LENGTH = 4e3;
591
+ function readChunked(store, name) {
592
+ const main = store.read(name);
593
+ if (main === void 0) return void 0;
594
+ if (!main.startsWith("__chunked__")) return main;
595
+ const count = parseChunkCount(main);
596
+ if (count === 0) return void 0;
597
+ const parts = [];
598
+ for (let index = 1; index <= count; index++) {
599
+ const part = store.read(chunkName(name, index));
600
+ if (part === void 0) return void 0;
601
+ parts.push(part);
602
+ }
603
+ return parts.join("");
604
+ }
605
+ function writeChunked(store, name, value, options) {
606
+ const { attrs, chunkMaxLength, prefix } = options;
607
+ const max = chunkMaxLength && chunkMaxLength > 0 ? chunkMaxLength : DEFAULT_CHUNK_MAX_LENGTH;
608
+ const chunkCount = Math.ceil(value.length / max);
609
+ if (chunkCount > 16) throw invalidConfigError(`${prefix} Session token requires ${chunkCount} cookie chunks, exceeding the limit of 16.`);
610
+ const previousCount = parseChunkCount(store.read(name));
611
+ const firstSurplus = chunkCount <= 1 ? 1 : chunkCount + 1;
612
+ for (let index = firstSurplus; index <= previousCount; index++) expire(store, chunkName(name, index), attrs);
613
+ if (chunkCount <= 1) {
614
+ store.write(name, value, attrs);
615
+ return;
616
+ }
617
+ store.write(name, `${CHUNK_MARKER}${chunkCount}`, attrs);
618
+ for (let index = 1; index <= chunkCount; index++) store.write(chunkName(name, index), value.slice((index - 1) * max, index * max), attrs);
619
+ }
620
+ function removeChunked(store, name, options) {
621
+ const count = parseChunkCount(store.read(name));
622
+ expire(store, name, options.attrs);
623
+ for (let index = 1; index <= count; index++) expire(store, chunkName(name, index), options.attrs);
624
+ }
625
+ function setCookieValues(setCookies) {
626
+ const cookies = Object.create(null);
627
+ for (const setCookie of setCookies) {
628
+ const pair = setCookiePair(setCookie);
629
+ if (pair) cookies[pair.name] = decodeValue(pair.value);
630
+ }
631
+ return cookies;
632
+ }
633
+ function setCookieName(setCookie) {
634
+ return setCookiePair(setCookie)?.name;
635
+ }
636
+ function mirrorSetCookie(name, headers) {
637
+ const { from, to } = headers;
638
+ const isThisCookie = (line) => setCookieName(line) === name;
639
+ const lines = [...to.getSetCookie().filter((line) => !isThisCookie(line)), ...from.getSetCookie().filter((line) => isThisCookie(line))];
640
+ to.delete("set-cookie");
641
+ for (const line of lines) to.append("set-cookie", line);
642
+ }
643
+ function chunkName(name, index) {
644
+ return `${name}.${index}`;
645
+ }
646
+ function parseChunkCount(main) {
647
+ if (main === void 0 || !main.startsWith("__chunked__")) return 0;
648
+ const suffix = main.slice(11);
649
+ if (!/^\d+$/.test(suffix)) return 0;
650
+ const count = Number.parseInt(suffix, 10);
651
+ if (count < 1 || count > 16) return 0;
652
+ return count;
653
+ }
654
+ function expire(store, name, attrs) {
655
+ store.write(name, "", {
656
+ ...attrs,
657
+ maxAge: 0,
658
+ expires: /* @__PURE__ */ new Date(0)
659
+ });
660
+ }
661
+ function setCookiePair(setCookie) {
662
+ const [pair = ""] = setCookie.split(";", 1);
663
+ const separator = pair.indexOf("=");
664
+ if (separator === -1) return void 0;
665
+ return {
666
+ name: pair.slice(0, separator).trim(),
667
+ value: pair.slice(separator + 1).trim()
668
+ };
669
+ }
670
+ function decodeValue(value) {
671
+ if (!value.includes("%")) return value;
672
+ try {
673
+ return decodeURIComponent(value);
674
+ } catch {
675
+ return value;
676
+ }
677
+ }
678
+ function resolveSessionTransport(driver, event, options) {
679
+ const handed = driver.handed?.(event);
680
+ if (handed) return {
681
+ source: "token",
682
+ read: () => handed.token,
683
+ readonly: false,
684
+ writable: () => true
685
+ };
686
+ const { name, cookie } = options;
687
+ const header = presentedHeader(driver, event, options);
688
+ if (cookie === false) return headerTransport(options, header);
689
+ const jar = driver.cookies(event);
690
+ if (header && (header.token === void 0 || header.token !== readChunked(jar, name))) return headerTransport(options, header);
691
+ const store = responseStore(jar);
692
+ const transport = {
693
+ source: "cookie",
694
+ read: () => cookieToken(jar, name),
695
+ readonly: !store,
696
+ writable: () => driver.writable?.(event) ?? true
697
+ };
698
+ if (!store) return transport;
699
+ const { attrs, chunkMaxLength } = cookie;
700
+ transport.write = (token, { expiresAt } = {}) => {
701
+ if (!transport.writable()) return false;
702
+ const written = expiresAt === void 0 ? { ...attrs } : {
703
+ ...attrs,
704
+ expires: new Date(expiresAt),
705
+ maxAge: void 0
706
+ };
707
+ writeChunked(store, name, token, {
708
+ attrs: written,
709
+ chunkMaxLength,
710
+ prefix: driver.prefix
711
+ });
712
+ return true;
713
+ };
714
+ transport.drop = () => {
715
+ if (!transport.writable()) return false;
716
+ removeChunked(store, name, { attrs });
717
+ return true;
718
+ };
719
+ return transport;
720
+ }
721
+ function assertSessionTokenSource(transport, options) {
722
+ if (!transport.read) throw invalidConfigError(`${options.prefix} Session has no token source, enable "cookie" or "sessionHeader".`);
723
+ }
724
+ function assertSessionWritable(transport, options) {
725
+ const { operation, prefix } = options;
726
+ if (operation === "update" && transport.readonly) throw readOnlyEventError(`${prefix} Cannot update session on read-only event.`);
727
+ if (!transport.writable()) throw readOnlyEventError(`${prefix} Cannot update/clear session: the response was already sent.`);
728
+ }
729
+ function assertSessionHeader(header, options) {
730
+ if (header === void 0 || typeof header === "boolean") return;
731
+ if (isToken(header)) return;
732
+ throw invalidConfigError(`${options.prefix} Session "sessionHeader" must be true, false or a header name (got ${quoteTokenValue(header)}).`);
733
+ }
734
+ function sessionHeaderName(sessionName, configured) {
735
+ if (typeof configured === "string") return configured.toLowerCase();
736
+ return `x-${sessionName.replace(/^__(?:host|secure)-/i, "").toLowerCase()}-session`;
737
+ }
738
+ function headerToken(value) {
739
+ if (typeof value !== "string") return void 0;
740
+ const words = value.trim().split(/\s+/);
741
+ const [first = "", second] = words;
742
+ if (/^bearer$/i.test(first)) return words.length === 2 ? second : void 0;
743
+ return words.length === 1 && first ? first : void 0;
744
+ }
745
+ function presentedHeader(driver, event, options) {
746
+ const { name, sessionHeader } = options;
747
+ if (!readsHeader(options)) return void 0;
748
+ const header = sessionHeaderName(name, sessionHeader);
749
+ const value = driver.header(event, header);
750
+ if (typeof value !== "string") return void 0;
751
+ const token = headerToken(value);
752
+ if (token) return { token };
753
+ return {
754
+ token: void 0,
755
+ malformed: { header }
756
+ };
757
+ }
758
+ function headerTransport(options, header) {
759
+ return {
760
+ source: "header",
761
+ read: readsHeader(options) ? () => header?.token : void 0,
762
+ malformed: header?.malformed,
763
+ readonly: false,
764
+ writable: () => true
765
+ };
766
+ }
767
+ function readsHeader(options) {
768
+ return options.sessionHeader !== void 0 && options.sessionHeader !== false;
769
+ }
770
+ function cookieToken(jar, name) {
771
+ const written = jar.written ? readChunked({ read: (key) => jar.written?.(key) }, name) : void 0;
772
+ if (written === "") return void 0;
773
+ return (written ?? readChunked(jar, name)) || void 0;
774
+ }
775
+ function responseStore(jar) {
776
+ const write = jar.write?.bind(jar);
777
+ if (!write) return void 0;
778
+ return {
779
+ read: (name) => jar.written?.(name) ?? jar.read(name),
780
+ write
781
+ };
782
+ }
783
+ const kSessionNames = Symbol("unjwt_session_names");
784
+ const kHeldTokens = Symbol("unjwt_session_held_tokens");
785
+ function assertSessionConfig(config, options) {
786
+ const { name, prefix, defaultCookie = DEFAULT_SESSION_COOKIE } = options;
787
+ assertSessionName(name, { prefix });
788
+ assertSessionHeader(config.sessionHeader, { prefix });
789
+ if (config.cookie !== false) assertCookiePrefix(name, {
790
+ ...defaultCookie,
791
+ ...config.cookie
792
+ }, { prefix });
793
+ assertRolling(config, { prefix });
794
+ }
795
+ function sessionTransportOptions(config, options) {
796
+ const { name, defaultCookie = DEFAULT_SESSION_COOKIE } = options;
797
+ const { sessionHeader } = config;
798
+ if (config.cookie === false) return {
799
+ name,
800
+ sessionHeader,
801
+ cookie: false
802
+ };
803
+ const { chunkMaxLength, ...configured } = config.cookie || {};
804
+ return {
805
+ name,
806
+ sessionHeader,
807
+ cookie: {
808
+ attrs: {
809
+ ...defaultCookie,
810
+ ...configured
811
+ },
812
+ chunkMaxLength
813
+ }
814
+ };
815
+ }
816
+ function claimSessionName(state, options) {
817
+ const { name, family, prefix } = options;
818
+ const holder = state;
819
+ const registry = holder[kSessionNames] ||= /* @__PURE__ */ new Map();
820
+ const held = registry.get(name);
821
+ if (held === void 0) {
822
+ registry.set(name, family);
823
+ return;
824
+ }
825
+ if (held === family) return;
826
+ throw invalidConfigError(`${prefix} Session name "${name}" is already used by a ${held.toUpperCase()} session on this request.`);
827
+ }
828
+ function bindSession(event, options) {
829
+ const { config, name, driver, codec, keys, actions } = options;
830
+ const record = driver.state(event);
831
+ claimSessionName(record, {
832
+ name,
833
+ family: codec.family,
834
+ prefix: driver.prefix
835
+ });
836
+ const slot = `${codec.family}:${name}`;
837
+ return {
838
+ event,
839
+ config,
840
+ driver,
841
+ codec,
842
+ keys,
843
+ actions,
844
+ name,
845
+ args: { event },
846
+ store: recordSlotStore(record, slot),
847
+ held: heldTokenSlot(record, slot),
848
+ transport: resolveSessionTransport(driver, event, sessionTransportOptions(config, { name }))
849
+ };
850
+ }
851
+ function sessionToken(binding) {
852
+ const { transport } = binding;
853
+ assertSessionTokenSource(transport, { prefix: binding.driver.prefix });
854
+ const session = storedSession(binding.store);
855
+ return session ? session.token : transport.read();
856
+ }
857
+ function transportToken(binding) {
858
+ const { transport } = binding;
859
+ assertSessionTokenSource(transport, { prefix: binding.driver.prefix });
860
+ const held = binding.held.get();
861
+ return held ? held.token : transport.read();
862
+ }
863
+ function recordSlotStore(record, slot) {
864
+ return {
865
+ get: () => record[slot],
866
+ set: (state) => {
867
+ record[slot] = state;
868
+ }
869
+ };
870
+ }
871
+ function heldTokenSlot(record, slot) {
872
+ const holder = record;
873
+ const held = holder[kHeldTokens] ||= /* @__PURE__ */ new Map();
874
+ return {
875
+ get: () => held.has(slot) ? { token: held.get(slot) } : void 0,
876
+ set: (token) => {
877
+ held.set(slot, token);
878
+ }
879
+ };
880
+ }
881
+ async function loadSession(binding) {
882
+ const { config, codec, keys, store, args, transport } = binding;
883
+ await keys.assertKeys();
884
+ const open = keys.opener(args);
885
+ assertSessionTokenSource(transport, { prefix: binding.driver.prefix });
886
+ const existing = storedSession(store);
887
+ if (existing) {
888
+ const session = existing[kSessionPending] ? await existing[kSessionPending] : existing;
889
+ if (session.token !== void 0 && session.expiresAt !== void 0 && session.expiresAt <= codec.now(config)) {
890
+ transport.drop?.();
891
+ const error = expiredSessionError({
892
+ id: session.id,
893
+ createdAt: session.createdAt,
894
+ expiresAt: session.expiresAt
895
+ });
896
+ const freshSession = emptySession(binding);
897
+ freshSession.error = error;
898
+ store.set(freshSession);
899
+ await fireExpire(binding, {
900
+ session: {
901
+ id: session.id,
902
+ createdAt: session.createdAt,
903
+ expiresAt: session.expiresAt,
904
+ token: session.token
905
+ },
906
+ error
907
+ });
908
+ return freshSession;
909
+ }
910
+ return session;
911
+ }
912
+ const session = emptySession(binding);
913
+ store.set(session);
914
+ if (transport.malformed) {
915
+ const error = malformedHeaderError(transport.malformed.header, { prefix: binding.driver.prefix });
916
+ session.error = error;
917
+ await fireError(binding, error);
918
+ return session;
919
+ }
920
+ const token = transport.read();
921
+ if (!token) return session;
922
+ binding.held.set(token);
923
+ session.token = token;
924
+ const pending = open(token).then((opened) => {
925
+ Object.assign(session, opened);
926
+ return { failure: void 0 };
927
+ }, (error) => {
928
+ session.token = void 0;
929
+ return { failure: { error } };
930
+ }).then((outcome) => {
931
+ delete session[kSessionPending];
932
+ return outcome;
933
+ });
934
+ session[kSessionPending] = pending.then(() => session);
935
+ const { failure } = await pending;
936
+ if (failure) {
937
+ if (isJWTError(failure.error)) session.error = failure.error;
938
+ if (isJWTError(failure.error, "ERR_JWT_EXPIRED")) {
939
+ const { jti, iat, exp } = failure.error.cause;
940
+ transport.drop?.();
941
+ await fireExpire(binding, {
942
+ session: {
943
+ id: jti,
944
+ createdAt: iat ? iat * 1e3 : void 0,
945
+ expiresAt: exp ? exp * 1e3 : void 0,
946
+ token
947
+ },
948
+ error: failure.error
949
+ });
950
+ } else {
951
+ if (isJWTError(failure.error, "ERR_SESSION_TOKEN_INVALID")) transport.drop?.();
952
+ await fireError(binding, failure.error);
953
+ }
954
+ return session;
955
+ }
956
+ try {
957
+ await fireRead(binding, session);
958
+ } catch (error_) {
959
+ store.set(emptySession(binding));
960
+ throw error_;
961
+ }
962
+ await rollSession(binding, session);
963
+ return session;
964
+ }
965
+ async function fireRead(binding, session) {
966
+ if (session.id === void 0 || session.token === void 0) return;
967
+ await binding.config.hooks?.onRead?.({
968
+ session: binding.actions.manager(binding),
969
+ ...binding.args,
970
+ config: binding.config
971
+ });
972
+ }
973
+ async function rollSession(binding, session) {
974
+ const { config, codec, transport, store } = binding;
975
+ const { rolling } = config;
976
+ if (rolling === void 0 || transport.source !== "cookie" || store.get() !== session) return;
977
+ if (session.token === void 0 || session.expiresAt === void 0) return;
978
+ if (rolling !== true) {
979
+ if (codec.now(config) - session.createdAt < computeDurationInSeconds(rolling) * 1e3) return;
980
+ }
981
+ if (transport.readonly || !transport.writable()) return;
982
+ await binding.actions.roll(binding);
983
+ }
984
+ async function fireError(binding, error) {
985
+ await binding.config.hooks?.onError?.({
986
+ session: binding.actions.manager(binding),
987
+ ...binding.args,
988
+ error,
989
+ config: binding.config
990
+ });
991
+ }
992
+ async function fireExpire(binding, expired) {
993
+ await binding.config.hooks?.onExpire?.({
994
+ session: {
995
+ ...expired.session,
996
+ source: binding.transport.source
997
+ },
998
+ ...binding.args,
999
+ error: expired.error,
1000
+ config: binding.config
1001
+ });
1002
+ }
1003
+ async function issueSession(binding, update, options, trigger = "update") {
1004
+ const { config, keys, driver, store, args, transport } = binding;
1005
+ assertSessionWritable(transport, {
1006
+ operation: "update",
1007
+ prefix: driver.prefix
1008
+ });
1009
+ const session = storedSession(store) || await loadSession(binding);
1010
+ const oldSession = snapshotSession(session);
1011
+ const resolved = typeof update === "function" ? update(session.data) : update;
1012
+ if (resolved) Object.assign(session.data, sanitizeSessionUpdate(resolved, { prefix: driver.prefix }));
1013
+ for (const claim of RESERVED_CLAIMS) delete session.data[claim];
1014
+ const maxAge = options?.maxAge ?? config.maxAge;
1015
+ const createdAt = sessionInstant(binding);
1016
+ const id = mintSessionId(config);
1017
+ Object.assign(session, {
1018
+ id,
1019
+ createdAt,
1020
+ expiresAt: resolveExpiresAt(createdAt, { maxAge })
1021
+ });
1022
+ let issued;
1023
+ try {
1024
+ await keys.assertKeys();
1025
+ const token = await keys.issue(session);
1026
+ transport.write?.(token, { expiresAt: session.expiresAt });
1027
+ binding.held.set(token);
1028
+ issued = Object.assign(session, {
1029
+ id,
1030
+ token,
1031
+ error: void 0
1032
+ });
1033
+ } catch (error_) {
1034
+ Object.assign(session, {
1035
+ id: oldSession.id,
1036
+ createdAt: oldSession.createdAt,
1037
+ expiresAt: oldSession.expiresAt,
1038
+ data: oldSession.data
1039
+ });
1040
+ session.token = oldSession.token;
1041
+ await config.hooks?.onError?.({
1042
+ session: binding.actions.manager(binding),
1043
+ ...args,
1044
+ error: error_,
1045
+ config
1046
+ });
1047
+ throw error_;
1048
+ }
1049
+ await config.hooks?.onUpdate?.({
1050
+ session: binding.actions.manager(binding),
1051
+ oldSession,
1052
+ trigger,
1053
+ ...args,
1054
+ config
1055
+ });
1056
+ return issued;
1057
+ }
1058
+ async function dropSession(binding) {
1059
+ const { config, driver, store, args, transport } = binding;
1060
+ assertSessionTokenSource(transport, { prefix: driver.prefix });
1061
+ assertSessionWritable(transport, {
1062
+ operation: "clear",
1063
+ prefix: driver.prefix
1064
+ });
1065
+ let session = storedSession(store);
1066
+ if (session && session[kSessionPending]) session = await session[kSessionPending];
1067
+ const oldSession = session && (session.id !== void 0 || session.token !== void 0 || session.error !== void 0) ? snapshotSession(session) : void 0;
1068
+ store.set(emptySession(binding));
1069
+ binding.held.set(void 0);
1070
+ transport.drop?.();
1071
+ await config.hooks?.onClear?.({
1072
+ oldSession,
1073
+ ...args,
1074
+ config
1075
+ });
1076
+ }
1077
+ function sessionManager(binding) {
1078
+ const { store } = binding;
1079
+ const state = () => storedSession(store);
1080
+ const manager = {
1081
+ get id() {
1082
+ return state()?.id;
1083
+ },
1084
+ get createdAt() {
1085
+ return state()?.createdAt ?? Date.now();
1086
+ },
1087
+ get expiresAt() {
1088
+ return (state() ?? blankSession()).expiresAt;
1089
+ },
1090
+ get data() {
1091
+ return (state() ?? blankSession()).data;
1092
+ },
1093
+ get token() {
1094
+ return sessionToken(binding);
1095
+ },
1096
+ get source() {
1097
+ return binding.transport.source;
1098
+ },
1099
+ get error() {
1100
+ return state()?.error;
1101
+ },
1102
+ update: async (update, options) => {
1103
+ await issueSession(binding, update, options);
1104
+ return manager;
1105
+ },
1106
+ clear: async () => {
1107
+ await dropSession(binding);
1108
+ return manager;
1109
+ }
1110
+ };
1111
+ return manager;
1112
+ }
1113
+ function sessionActions() {
1114
+ return {
1115
+ manager: (binding) => sessionManager(binding),
1116
+ roll: async (binding) => {
1117
+ await issueSession(binding, void 0, void 0, "rolling");
1118
+ }
1119
+ };
1120
+ }
1121
+ function snapshotSession(session) {
1122
+ return {
1123
+ ...session,
1124
+ data: safeJsonParse(JSON.stringify(session.data))
1125
+ };
1126
+ }
1127
+ function defineSession(config, options) {
1128
+ const { driver, codec } = options;
1129
+ const name = config.name || codec.defaultName;
1130
+ assertSessionConfig(config, {
1131
+ name,
1132
+ prefix: driver.prefix
1133
+ });
1134
+ const keys = codec.keys(config);
1135
+ const actions = sessionActions();
1136
+ const bind = (event) => bindSession(event, {
1137
+ config,
1138
+ name,
1139
+ driver,
1140
+ codec,
1141
+ keys,
1142
+ actions
1143
+ });
1144
+ return {
1145
+ use: async (event) => {
1146
+ const binding = bind(event);
1147
+ await loadSession(binding);
1148
+ return sessionManager(binding);
1149
+ },
1150
+ token: (event) => transportToken(bind(event)),
1151
+ issue: async (data, issueOptions) => {
1152
+ await keys.assertKeys();
1153
+ const createdAt = sessionInstant({
1154
+ codec,
1155
+ config
1156
+ });
1157
+ return keys.issue({
1158
+ id: mintSessionId(config),
1159
+ createdAt,
1160
+ expiresAt: resolveExpiresAt(createdAt, { maxAge: issueOptions?.maxAge ?? config.maxAge }),
1161
+ data: sanitizeSessionUpdate(data ?? {}, { prefix: driver.prefix }),
1162
+ token: void 0,
1163
+ source: "token",
1164
+ error: void 0
1165
+ });
1166
+ },
1167
+ open: (token) => openSessionToken(keys, {
1168
+ token,
1169
+ args: {}
1170
+ })
1171
+ };
1172
+ }
1173
+ function webCookieJar(request, response) {
1174
+ let cookies;
1175
+ const jar = { read: (name) => (cookies ||= parseCookie(request.get("cookie") ?? ""))[name] };
1176
+ if (response) {
1177
+ const { headers, errHeaders } = response;
1178
+ jar.written = (name) => setCookieValues(headers.getSetCookie())[name];
1179
+ jar.write = (name, value, attrs) => {
1180
+ setCookie(headers, {
1181
+ ...attrs,
1182
+ name,
1183
+ value
1184
+ });
1185
+ if (errHeaders) mirrorSetCookie(name, {
1186
+ from: headers,
1187
+ to: errHeaders
1188
+ });
1189
+ };
1190
+ }
1191
+ return jar;
1192
+ }
1193
+ function parseCookie(header) {
1194
+ return parse(header);
1195
+ }
1196
+ function serializeCookie(name, value, attrs) {
1197
+ return serialize({
1198
+ path: "/",
1199
+ ...attrs,
1200
+ name,
1201
+ value
1202
+ });
1203
+ }
1204
+ function setCookie(headers, cookie) {
1205
+ const { name, value, ...attrs } = cookie;
1206
+ const line = serializeCookie(name, value, attrs);
1207
+ const key = cookieKey(cookie);
1208
+ const kept = headers.getSetCookie().filter((existing) => {
1209
+ const parsed = parseSetCookie(existing);
1210
+ return parsed ? cookieKey(parsed) !== key : setCookieName(existing) !== name;
1211
+ });
1212
+ headers.delete("set-cookie");
1213
+ for (const existing of kept) headers.append("set-cookie", existing);
1214
+ headers.append("set-cookie", line);
1215
+ }
1216
+ function cookieKey(cookie) {
1217
+ const domain = (cookie.domain || "").replace(/^\./, "").toLowerCase();
1218
+ return [
1219
+ cookie.name,
1220
+ domain,
1221
+ cookie.path || "/",
1222
+ cookie.partitioned ? "1" : "0"
1223
+ ].join(";");
1224
+ }
1225
+ const requestStates = /* @__PURE__ */ new WeakMap();
1226
+ function webSessionDriver(options) {
1227
+ const prefix = options?.prefix ?? "[unjwt/session]";
1228
+ return {
1229
+ prefix,
1230
+ state: (event) => {
1231
+ if (isHanded(event, prefix)) return sessionsOf(event);
1232
+ if (event.context) return contextState(event.context);
1233
+ return sessionsOf(event.req ? webRequest(event.req, prefix) : event);
1234
+ },
1235
+ header: (event, name) => isHanded(event, prefix) ? void 0 : requestHeaders(event, prefix).get(name),
1236
+ cookies: (event) => isHanded(event, prefix) ? { read: () => void 0 } : webCookieJar(requestHeaders(event, prefix), event.res),
1237
+ handed: (event) => isHanded(event, prefix) ? { token: event.token } : void 0
1238
+ };
1239
+ }
1240
+ const defaultWebSessionDriver = /* @__PURE__ */ webSessionDriver();
1241
+ function isHanded(event, prefix) {
20
1242
  if ("req" in event || "headers" in event) return false;
21
1243
  if ("token" in event) return true;
22
- throw invalidConfigError(`${PREFIX} use() got neither a request event nor a token.`);
1244
+ throw invalidConfigError(`${prefix} use() got neither a request event nor a token.`);
23
1245
  }
24
- function requestHeaders(event) {
25
- if (event.req) return webRequest(event.req).headers;
1246
+ function requestHeaders(event, prefix) {
1247
+ if (event.req) return webRequest(event.req, prefix).headers;
26
1248
  if (event.headers) return event.headers;
27
- throw invalidConfigError(`${PREFIX} use() got a request event with neither "req" nor "headers".`);
1249
+ throw invalidConfigError(`${prefix} use() got a request event with neither "req" nor "headers".`);
28
1250
  }
29
- function webRequest(req) {
1251
+ function webRequest(req, prefix) {
30
1252
  const headers = req.headers;
31
1253
  if (typeof headers === "object" && headers !== null && "get" in headers) return req;
32
- throw invalidConfigError(`${PREFIX} use() got an event whose "req" is not a web Request.`);
1254
+ throw invalidConfigError(`${prefix} use() got an event whose "req" is not a web Request.`);
33
1255
  }
34
- function contextSessions(context) {
35
- const existing = context.sessions;
1256
+ function contextState(context) {
1257
+ const existing = context.unjwt;
36
1258
  if (typeof existing === "object" && existing !== null) return existing;
37
1259
  const created = Object.create(null);
38
- context.sessions = created;
1260
+ context.unjwt = created;
39
1261
  return created;
40
1262
  }
41
- function sessionsOf(key) {
42
- const existing = SESSIONS.get(key);
1263
+ function sessionsOf(request) {
1264
+ const existing = requestStates.get(request);
43
1265
  if (existing) return existing;
44
1266
  const created = Object.create(null);
45
- SESSIONS.set(key, created);
1267
+ requestStates.set(request, created);
46
1268
  return created;
47
1269
  }
48
1270
  function defineJWSSession(config, options) {
49
- const driver = options?.driver ?? webSessionDriver;
1271
+ const driver = options?.driver ?? defaultWebSessionDriver;
50
1272
  return defineSession(config, {
51
1273
  driver,
52
- hookArgs: (event) => ({ event }),
53
1274
  codec: createJWSCodec({
54
1275
  prefix: driver.prefix,
55
- defaultName: "unjwt-jws"
1276
+ defaultName: "__Host-unjwt-jws"
56
1277
  })
57
1278
  });
58
1279
  }
1280
+ const JWE_KEY_RULES = {
1281
+ own: (key) => isJWK(key) && key.alg ? inferJWEAllowedAlgorithms(key) ?? [] : void 0,
1282
+ carried: (key) => {
1283
+ if (isCryptoKey(key)) {
1284
+ const name = key.algorithm.name;
1285
+ if (name === "PBKDF2") return PBES2_ALGORITHMS;
1286
+ const wraps = inferJWEAllowedAlgorithms(key) ?? [];
1287
+ return name === "AES-GCM" && wraps.length > 0 ? [...wraps, "dir"] : wraps;
1288
+ }
1289
+ if (!isSymmetricJWK(key)) return [];
1290
+ const algs = aesWraps(octBits(key));
1291
+ return directEncryptions(key).length > 0 ? [...algs, "dir"] : algs;
1292
+ },
1293
+ pinPath: "jwe.encryptOptions.alg",
1294
+ hint: "\"jwe.encryptOptions.alg\" and \"enc\"",
1295
+ listPath: "jwe.decryptOptions.algorithms",
1296
+ limitPath: "jwe.decryptOptions.limits.keyCandidates"
1297
+ };
1298
+ function resolveSessionKeys(key, options) {
1299
+ const { alg: pin, enc, prefix } = options;
1300
+ assertKeyMaterial(key, { prefix });
1301
+ const entries = isJWKSet(key) ? sessionKeySetEntries(key.keys, {
1302
+ ...options,
1303
+ pin,
1304
+ rules: JWE_KEY_RULES
1305
+ }) : [{
1306
+ key: sessionSecret(key, prefix),
1307
+ subject: "The session key",
1308
+ pin
1309
+ }];
1310
+ assertIssuingKey(entries[0], {
1311
+ enc,
1312
+ prefix
1313
+ });
1314
+ const { all } = servedAlgorithms(entries, {
1315
+ ...options,
1316
+ rules: JWE_KEY_RULES
1317
+ });
1318
+ return {
1319
+ issueKey: entries[0].key,
1320
+ key,
1321
+ algorithms: all
1322
+ };
1323
+ }
1324
+ function assertIssuingKey(entry, options) {
1325
+ const { enc, prefix } = options;
1326
+ const { key, subject, pin } = entry;
1327
+ assertPinnedKey(entry, {
1328
+ rules: JWE_KEY_RULES,
1329
+ pinned: Boolean(pin && enc),
1330
+ prefix
1331
+ });
1332
+ if ((pin ?? JWE_KEY_RULES.own(key)?.[0]) !== "dir" || !enc) return;
1333
+ const encs = directEncryptions(key);
1334
+ if (encs.includes(enc)) return;
1335
+ throw invalidKeyError(`${prefix} ${subject} serves "dir" with ${encs.join(", ") || "no content encryption"}, not the "${enc}" that "jwe.encryptOptions.enc" pins.`, key);
1336
+ }
1337
+ function directEncryptions(key) {
1338
+ if (isJWK(key) && typeof key.alg === "string" && isContentEncryptionAlgorithm(key.alg)) return [key.alg];
1339
+ if (isCryptoKey(key)) {
1340
+ const bits = lengthOf(key.algorithm) ?? 0;
1341
+ return key.algorithm.name === "AES-GCM" ? GCM_ENCRYPTIONS.filter((enc) => enc === `A${bits}GCM`) : [];
1342
+ }
1343
+ if (!isSymmetricJWK(key)) return [];
1344
+ const bits = octBits(key);
1345
+ return [...GCM_ENCRYPTIONS, ...CBC_ENCRYPTIONS].filter((enc) => encryptionBits(enc) === bits);
1346
+ }
1347
+ function sessionSecret(key, prefix) {
1348
+ if (isOctJWK(key)) return key;
1349
+ if (isCryptoKey(key) && key.type === "secret") return key;
1350
+ throw invalidKeyError((isCryptoKey(key) ? key.type !== "secret" : isAsymmetricJWK(key) || isKeyPair(key)) ? `${prefix} Invalid JWE session key: an asymmetric key seals with its public half, so anyone holding that half could mint sessions. Use an "oct" JWK, or defineJWSSession for a key pair.` : `${prefix} Invalid JWE session key: expected an "oct" JWK, a JWK Set of them or a secret CryptoKey.`, key);
1351
+ }
1352
+ function isOctJWK(key) {
1353
+ return isSymmetricJWK(key);
1354
+ }
1355
+ const GCM_ENCRYPTIONS = [
1356
+ "A128GCM",
1357
+ "A192GCM",
1358
+ "A256GCM"
1359
+ ];
1360
+ const CBC_ENCRYPTIONS = [
1361
+ "A128CBC-HS256",
1362
+ "A192CBC-HS384",
1363
+ "A256CBC-HS512"
1364
+ ];
1365
+ function aesWraps(bits) {
1366
+ switch (bits) {
1367
+ case 128: return ["A128KW", "A128GCMKW"];
1368
+ case 192: return ["A192KW", "A192GCMKW"];
1369
+ case 256: return ["A256KW", "A256GCMKW"];
1370
+ default: return [];
1371
+ }
1372
+ }
1373
+ function encryptionBits(enc) {
1374
+ const bits = Number(enc.slice(1, 4));
1375
+ return enc.includes("CBC") ? bits * 2 : bits;
1376
+ }
1377
+ function createJWECodec(options) {
1378
+ const { prefix, defaultName } = options;
1379
+ return createSessionCodec({
1380
+ family: "jwe",
1381
+ prefix,
1382
+ defaultName,
1383
+ issueOptions: (config) => config.jwe?.encryptOptions,
1384
+ openOptions: (config) => config.jwe?.decryptOptions,
1385
+ resolveKeys: (config) => resolveSessionKeys(config.key, {
1386
+ alg: config.jwe?.encryptOptions?.alg,
1387
+ enc: config.jwe?.encryptOptions?.enc,
1388
+ listed: config.jwe?.decryptOptions?.algorithms,
1389
+ limits: config.jwe?.decryptOptions?.limits,
1390
+ lookup: typeof config.hooks?.onKeyLookup === "function",
1391
+ prefix
1392
+ }),
1393
+ lookup: (config) => config.hooks?.onKeyLookup,
1394
+ issue: (config, payload, key) => encrypt(payload, key, sessionIssueOptions(config.jwe?.encryptOptions)),
1395
+ open: async (config, sealed, key, overrides) => {
1396
+ const { payload } = await decrypt(sealed, key, {
1397
+ ...config.jwe?.decryptOptions,
1398
+ maxIterations: sessionMaxIterations(config),
1399
+ ...overrides
1400
+ });
1401
+ return payload;
1402
+ }
1403
+ });
1404
+ }
1405
+ function sessionMaxIterations(config) {
1406
+ const configured = config.jwe?.decryptOptions?.maxIterations;
1407
+ if (configured !== void 0) return configured;
1408
+ const issuing = isJWKSet(config.key) ? config.key.keys[0] : config.key;
1409
+ const alg = config.jwe?.encryptOptions?.alg ?? (isJWK(issuing) ? issuing.alg : void 0);
1410
+ return typeof alg === "string" && alg.startsWith("PBES2") ? config.jwe?.encryptOptions?.p2c ?? 6e5 : void 0;
1411
+ }
59
1412
  function defineJWESession(config, options) {
60
- const driver = options?.driver ?? webSessionDriver;
1413
+ const driver = options?.driver ?? defaultWebSessionDriver;
61
1414
  return defineSession(config, {
62
1415
  driver,
63
- hookArgs: (event) => ({ event }),
64
1416
  codec: createJWECodec({
65
1417
  prefix: driver.prefix,
66
- defaultName: "unjwt-jwe"
1418
+ defaultName: "__Host-unjwt-jwe"
67
1419
  })
68
1420
  });
69
1421
  }
70
1422
  function defineSessionDriver(driver) {
71
1423
  return driver;
72
1424
  }
73
- export { defineJWESession, defineJWSSession, defineSessionDriver, deriveJWKFromPassword, exportPEM, generateJWK, importPEM, thumbprint, toPublicJWK };
1425
+ export { DEFAULT_SESSION_COOKIE, assertSessionConfig, assertSessionHeader, assertSessionTokenSource, assertSessionWritable, claimSessionName, defineJWESession, defineJWSSession, defineSessionDriver, deriveJWKFromPassword, exportPEM, generateJWK, importPEM, parseCookie, resolveSessionTransport, serializeCookie, sessionTransportOptions, thumbprint, toPublicJWK, webCookieJar, webSessionDriver };