unjwt 0.7.2 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +4 -4
  2. package/dist/THIRD-PARTY-LICENSES.md.gz +0 -0
  3. package/dist/_chunks/_codec-jwe.d.mts +170 -0
  4. package/dist/_chunks/_codec-jws.mjs +878 -0
  5. package/dist/_chunks/_internal.d.mts +7 -0
  6. package/dist/_chunks/_jwt-internal.mjs +388 -0
  7. package/dist/_chunks/algorithms.mjs +186 -0
  8. package/dist/_chunks/index.d.mts +512 -818
  9. package/dist/_chunks/index2.d.mts +11 -0
  10. package/dist/_chunks/index3.d.mts +108 -0
  11. package/dist/_chunks/jwe.d.mts +43 -66
  12. package/dist/_chunks/jwe.mjs +384 -252
  13. package/dist/_chunks/jwk.mjs +1390 -622
  14. package/dist/_chunks/jws.d.mts +48 -103
  15. package/dist/_chunks/jws.mjs +372 -184
  16. package/dist/_chunks/jwt.d.mts +56 -0
  17. package/dist/_chunks/libs/unsecure.d.mts +164 -85
  18. package/dist/_chunks/libs/unsecure.mjs +390 -123
  19. package/dist/adapters/elysia/index.d.mts +33 -171
  20. package/dist/adapters/elysia/index.mjs +106 -575
  21. package/dist/adapters/h3v1/index.d.mts +59 -344
  22. package/dist/adapters/h3v1/index.mjs +114 -619
  23. package/dist/adapters/h3v2/index.d.mts +58 -326
  24. package/dist/adapters/h3v2/index.mjs +108 -611
  25. package/dist/core/internal.d.mts +2 -0
  26. package/dist/core/internal.mjs +1 -0
  27. package/dist/core/jwe.d.mts +4 -2
  28. package/dist/core/jwe.mjs +4 -2
  29. package/dist/core/jwk.d.mts +183 -371
  30. package/dist/core/jwk.mjs +4 -3
  31. package/dist/core/jws.d.mts +4 -2
  32. package/dist/core/jws.mjs +4 -2
  33. package/dist/core/utils/index.d.mts +5 -155
  34. package/dist/core/utils/index.mjs +12 -3
  35. package/dist/index.d.mts +8 -4
  36. package/dist/index.mjs +6 -3
  37. package/package.json +39 -36
  38. package/dist/_chunks/_internal.mjs +0 -88
  39. package/dist/_chunks/utils.mjs +0 -390
@@ -0,0 +1,878 @@
1
+ import { sanitizeObjectCopy } from "./libs/unsecure.mjs";
2
+ import { JWTError, computeDurationInSeconds, expiredClaimMessage, isCryptoKey, isJWK, isJWKSet, isJWTError, isPlainObject, isPrivateJWK, isPublicJWK, isSymmetricJWK } from "./_jwt-internal.mjs";
3
+ import { importKey, quoteTokenValue } from "./jwk.mjs";
4
+ import { sign, verify } from "./jws.mjs";
5
+ import { decrypt, encrypt } from "./jwe.mjs";
6
+ const RESERVED_CLAIMS = [
7
+ "jti",
8
+ "iat",
9
+ "exp",
10
+ "nbf"
11
+ ];
12
+ const SESSION_TYP = "session+jwt";
13
+ const FORBIDDEN_KEYS = [
14
+ "__proto__",
15
+ "constructor",
16
+ "prototype"
17
+ ];
18
+ const importedKeys = /* @__PURE__ */ new WeakMap();
19
+ function invalidKeyError(message, cause) {
20
+ return new JWTError(message, "ERR_SESSION_KEY_INVALID", cause);
21
+ }
22
+ function invalidConfigError(message, cause) {
23
+ return new JWTError(message, "ERR_SESSION_CONFIG_INVALID", cause);
24
+ }
25
+ function invalidTokenError(message, cause) {
26
+ return new JWTError(message, "ERR_SESSION_TOKEN_INVALID", cause);
27
+ }
28
+ function readOnlyEventError(message) {
29
+ return new JWTError(message, "ERR_SESSION_READONLY_EVENT");
30
+ }
31
+ function expiredSessionError(options) {
32
+ const { id, expiresAt } = options;
33
+ const exp = Math.floor(expiresAt / 1e3);
34
+ return new JWTError(expiredClaimMessage(exp), "ERR_JWT_EXPIRED", {
35
+ jti: id,
36
+ exp
37
+ });
38
+ }
39
+ function rethrowAsInvalidToken(error, options) {
40
+ if (isJWTError(error, "ERR_JWT_EXPIRED")) throw error;
41
+ throw invalidTokenError(`${options.prefix} Invalid session token: ${messageOf(error)}`, error);
42
+ }
43
+ const TOKEN = /^[!#$%&'*+\-.^_`|~\w]+$/;
44
+ const CHUNK_SUFFIX = /\.\d+$/;
45
+ function assertSessionName(name, options) {
46
+ if (typeof name !== "string" || !TOKEN.test(name)) throw invalidConfigError(`${options.prefix} Session "name" must be a cookie name (RFC 6265 §4.1.1): letters, digits and !#$%&'*+-.^_\`|~ only. Got ${quoteTokenValue(name)}.`);
47
+ if (CHUNK_SUFFIX.test(name)) throw invalidConfigError(`${options.prefix} Session "name" cannot end in a dot and digits: ${quoteTokenValue(name)} is the cookie holding a chunk of the session named ${quoteTokenValue(name.replace(CHUNK_SUFFIX, ""))}.`);
48
+ }
49
+ function assertCookiePrefix(name, attrs, options) {
50
+ const lower = name.toLowerCase();
51
+ const host = lower.startsWith("__host-");
52
+ if (!host && !lower.startsWith("__secure-")) return;
53
+ const rule = host ? "\"__Host-\" cookie must be Secure, with Path=/ and no Domain" : "\"__Secure-\" cookie must be Secure";
54
+ if (attrs.secure === false || host && (attrs.domain !== void 0 || attrs.path !== void 0 && attrs.path !== "/")) throw invalidConfigError(`${options.prefix} Session ${quoteTokenValue(name)}: a ${rule} (RFC 6265bis §4.1.3).`);
55
+ }
56
+ function assertSessionHeader(header, options) {
57
+ if (header === void 0 || header === false) return;
58
+ if (typeof header === "string" && TOKEN.test(header)) return;
59
+ throw invalidConfigError(`${options.prefix} Session "sessionHeader" must be false or a header name: letters, digits and !#$%&'*+-.^_\`|~ only. Got ${quoteTokenValue(header)}.`);
60
+ }
61
+ function assertRolling(config, options) {
62
+ const { rolling } = config;
63
+ if (rolling === void 0) return;
64
+ if (config.maxAge === void 0) throw invalidConfigError(`${options.prefix} "rolling" needs a "maxAge" to extend.`);
65
+ if (rolling === true) return;
66
+ if (!isExpiresIn(rolling)) throw new JWTError(`${options.prefix} "rolling" must be true or a duration.`, "ERR_INVALID_OPTION");
67
+ computeDurationInSeconds(rolling);
68
+ }
69
+ function hasTokenSource(source) {
70
+ return source.cookie !== false || source.sessionHeader !== false;
71
+ }
72
+ function assertTokenSource(source, options) {
73
+ if (!hasTokenSource(source)) throw invalidConfigError(`${options.prefix} Session has no token source: "cookie" and "sessionHeader" are both disabled.`);
74
+ }
75
+ function assertKeyMaterial(key, options) {
76
+ if (typeof key === "string") throw invalidKeyError(`${options.prefix} Session "key" must be a JWK or a CryptoKey, not a password string. Derive one once with deriveJWKFromPassword() and pass the result.`);
77
+ if (!key) throw invalidKeyError(`${options.prefix} Session "key" is required.`);
78
+ }
79
+ async function assertImportableKey(key, options) {
80
+ if (!isJWK(key)) return;
81
+ const alg = key.alg ?? options.alg;
82
+ if (key.kty !== "oct" && !alg) return;
83
+ let imports = importedKeys.get(key);
84
+ if (!imports) {
85
+ imports = /* @__PURE__ */ new Map();
86
+ importedKeys.set(key, imports);
87
+ }
88
+ const slot = alg ?? "";
89
+ let pending = imports.get(slot);
90
+ if (!pending) {
91
+ pending = importKey(key, { alg }).then(() => void 0);
92
+ imports.set(slot, pending);
93
+ }
94
+ try {
95
+ await pending;
96
+ } catch (error) {
97
+ if (imports.get(slot) === pending) imports.delete(slot);
98
+ throw invalidKeyError(`${options.prefix} Invalid session key: ${messageOf(error)}`, error);
99
+ }
100
+ }
101
+ function sessionTyp(configured) {
102
+ return typeof configured === "string" && configured ? configured : SESSION_TYP;
103
+ }
104
+ function sessionHeaderName(sessionName, configured) {
105
+ if (typeof configured === "string") return configured.toLowerCase();
106
+ return `x-${sessionName.replace(/^__(?:host|secure)-/i, "").toLowerCase()}-session`;
107
+ }
108
+ function headerToken(value) {
109
+ if (typeof value !== "string") return void 0;
110
+ const words = value.trim().split(/\s+/);
111
+ const [first = "", second] = words;
112
+ if (/^bearer$/i.test(first)) return words.length === 2 ? second : void 0;
113
+ return words.length === 1 && first ? first : void 0;
114
+ }
115
+ function resolveSessionToken(source, sessionName, read) {
116
+ let cookieValue;
117
+ if (source.cookie !== false) {
118
+ const written = read.written?.(sessionName);
119
+ if (written === "") return void 0;
120
+ cookieValue = written ?? read.cookie(sessionName);
121
+ }
122
+ const headerValue = source.sessionHeader === false ? void 0 : headerToken(read.header(sessionHeaderName(sessionName, source.sessionHeader)));
123
+ return cookieValue || headerValue || void 0;
124
+ }
125
+ function resolveExpiresAt(createdAt, options) {
126
+ return options.maxAge === void 0 ? void 0 : createdAt + computeDurationInSeconds(options.maxAge) * 1e3;
127
+ }
128
+ function sessionRequiredClaims(configured, options) {
129
+ const required = /* @__PURE__ */ new Set([
130
+ ...configured || [],
131
+ "jti",
132
+ "iat"
133
+ ]);
134
+ if (options.maxAge !== void 0) required.add("exp");
135
+ return [...required];
136
+ }
137
+ function sanitizeSessionUpdate(update, options) {
138
+ if (!isPlainObject(update)) throw new JWTError(`${options.prefix} Session data must be a plain object, got ${Array.isArray(update) ? "an array" : typeof update}.`, "ERR_INVALID_OPTION");
139
+ const sanitized = Object.create(null);
140
+ for (const key of Object.keys(update)) {
141
+ if (FORBIDDEN_KEYS.includes(key)) throw new JWTError(`${options.prefix} Session data cannot carry the key "${key}".`, "ERR_INVALID_OPTION");
142
+ if (RESERVED_CLAIMS.includes(key)) continue;
143
+ sanitized[key] = update[key];
144
+ }
145
+ return sanitized;
146
+ }
147
+ function messageOf(error) {
148
+ return error instanceof Error ? error.message : String(error);
149
+ }
150
+ function isExpiresIn(value) {
151
+ return typeof value === "number" || typeof value === "string";
152
+ }
153
+ function createSessionCodec(spec) {
154
+ const { prefix } = spec;
155
+ const alg = (config) => spec.issueOptions(config)?.alg;
156
+ const keyFor = (config, carrier) => spec.lookup(config, carrier) ?? spec.openKey(config);
157
+ const now = (config) => spec.issueOptions(config)?.currentDate?.getTime() ?? Date.now();
158
+ const openWith = async (config, token, key) => {
159
+ const payload = await spec.open(config, token, key).catch((error) => rethrowAsInvalidToken(error, { prefix }));
160
+ assertNotFromTheFuture(payload, {
161
+ now: now(config),
162
+ clockTolerance: spec.openOptions(config)?.clockTolerance,
163
+ prefix
164
+ });
165
+ return openedSession(payload);
166
+ };
167
+ const assertOpenKey = async (config) => {
168
+ if (spec.lookup(config, {})) return;
169
+ const key = spec.openKey(config);
170
+ for (const candidate of spec.importable ? spec.importable(key) : [key]) await assertImportableKey(candidate, {
171
+ alg: alg(config),
172
+ prefix
173
+ });
174
+ };
175
+ return {
176
+ family: spec.family,
177
+ defaultName: spec.defaultName,
178
+ defaultCookie: spec.defaultCookie,
179
+ now,
180
+ issuer: (config) => {
181
+ const key = spec.issueKey(config);
182
+ return (state) => spec.issue(config, sessionPayload(state), key);
183
+ },
184
+ assertKeys: async (config) => {
185
+ const key = spec.issueKey(config);
186
+ await assertImportableKey(key, {
187
+ alg: alg(config),
188
+ prefix
189
+ });
190
+ if (!isSymmetricJWK(key)) await assertOpenKey(config);
191
+ },
192
+ opener: (config, carrier) => {
193
+ const key = keyFor(config, carrier);
194
+ return (token) => openWith(config, token, key);
195
+ },
196
+ open: async (config, token, carrier) => {
197
+ await assertOpenKey(config);
198
+ return openWith(config, token, keyFor(config, carrier));
199
+ }
200
+ };
201
+ }
202
+ function sessionIssueOptions(options) {
203
+ return Object.assign({}, options, {
204
+ expiresIn: void 0,
205
+ expiresAt: void 0,
206
+ notBeforeIn: void 0,
207
+ notBeforeAt: void 0,
208
+ protectedHeader: {
209
+ ...options?.protectedHeader,
210
+ typ: sessionTyp(options?.protectedHeader?.typ)
211
+ }
212
+ });
213
+ }
214
+ function sessionOpenOptions(options) {
215
+ const { maxAge, requiredClaims, typ, alg } = options;
216
+ return {
217
+ requiredClaims: sessionRequiredClaims(requiredClaims, { maxAge }),
218
+ typ: sessionTyp(typ),
219
+ maxTokenAge: void 0,
220
+ algorithms: alg ? [alg] : void 0,
221
+ payload: "json",
222
+ validateClaims: true
223
+ };
224
+ }
225
+ function isKeyPair(key) {
226
+ return "privateKey" in key || "publicKey" in key;
227
+ }
228
+ function assertKeyPair(pair, options) {
229
+ const { prefix, family } = options;
230
+ if (!isCryptoKey(pair.privateKey) && !isPrivateJWK(pair.privateKey)) throw invalidKeyError(`${prefix} Invalid ${family} key: "privateKey" must be a private JWK or a CryptoKey.`, pair);
231
+ if (!options.isPublic(pair.publicKey)) throw invalidKeyError(`${prefix} Invalid ${family} key: "publicKey" must be ${options.publicHalf}.`, pair);
232
+ }
233
+ function assertPinnedCryptoKey(key, options) {
234
+ if (isCryptoKey(key) && !options.pinned) throw invalidConfigError(`${options.prefix} A CryptoKey session key carries no algorithm: set ${options.hint}.`);
235
+ }
236
+ function sessionPayload(state) {
237
+ const iat = Math.floor(state.createdAt / 1e3);
238
+ const exp = state.expiresAt ? Math.floor(state.expiresAt / 1e3) : void 0;
239
+ const payload = {};
240
+ for (const key of Object.keys(state.data)) if (!RESERVED_CLAIMS.includes(key)) payload[key] = state.data[key];
241
+ payload.jti = state.id;
242
+ payload.iat = iat;
243
+ if (exp) payload.exp = exp;
244
+ return payload;
245
+ }
246
+ function assertNotFromTheFuture(payload, options) {
247
+ const tolerance = options.clockTolerance === void 0 ? 0 : computeDurationInSeconds(options.clockTolerance);
248
+ if (payload.iat > Math.floor(options.now / 1e3) + tolerance) throw invalidTokenError(`${options.prefix} Invalid session token: issued in the future (iat: ${(/* @__PURE__ */ new Date(payload.iat * 1e3)).toISOString()}).`);
249
+ }
250
+ function openedSession(payload) {
251
+ const data = Object.create(null);
252
+ for (const key of Object.keys(payload)) {
253
+ if (RESERVED_CLAIMS.includes(key)) continue;
254
+ data[key] = payload[key];
255
+ }
256
+ return {
257
+ id: payload.jti,
258
+ createdAt: payload.iat * 1e3,
259
+ expiresAt: payload.exp ? payload.exp * 1e3 : void 0,
260
+ data
261
+ };
262
+ }
263
+ const JWE_DEFAULT_COOKIE = {
264
+ path: "/",
265
+ secure: true,
266
+ httpOnly: true,
267
+ sameSite: "lax"
268
+ };
269
+ function createJWECodec(options) {
270
+ const { prefix, defaultName } = options;
271
+ return createSessionCodec({
272
+ family: "jwe",
273
+ prefix,
274
+ defaultName,
275
+ defaultCookie: JWE_DEFAULT_COOKIE,
276
+ issueOptions: (config) => config.jwe?.encryptOptions,
277
+ openOptions: (config) => config.jwe?.decryptOptions,
278
+ issueKey: (config) => getEncryptKey(config, prefix),
279
+ openKey: (config) => getDecryptKey(config, prefix),
280
+ lookup: (config, carrier) => {
281
+ const hooks = config.hooks;
282
+ if (!hooks?.onUnsealKeyLookup) return void 0;
283
+ return (header) => hooks.onUnsealKeyLookup?.({
284
+ header,
285
+ ...carrier,
286
+ config
287
+ });
288
+ },
289
+ issue: (config, payload, key) => encrypt(payload, key, sessionIssueOptions(config.jwe?.encryptOptions)),
290
+ open: async (config, sealed, key) => {
291
+ const { decryptOptions, encryptOptions } = config.jwe ?? {};
292
+ const enc = encryptOptions?.enc;
293
+ const { payload } = await decrypt(sealed, key, {
294
+ ...decryptOptions,
295
+ maxIterations: sessionMaxIterations(config),
296
+ ...sessionOpenOptions({
297
+ maxAge: config.maxAge,
298
+ requiredClaims: decryptOptions?.requiredClaims,
299
+ typ: encryptOptions?.protectedHeader?.typ,
300
+ alg: encryptOptions?.alg
301
+ }),
302
+ encryptionAlgorithms: enc ? [enc] : void 0
303
+ });
304
+ return payload;
305
+ }
306
+ });
307
+ }
308
+ function sessionMaxIterations(config) {
309
+ const configured = config.jwe?.decryptOptions?.maxIterations;
310
+ if (configured !== void 0) return configured;
311
+ const alg = config.jwe?.encryptOptions?.alg ?? (isJWK(config.key) ? config.key.alg : void 0);
312
+ return typeof alg === "string" && alg.startsWith("PBES2") ? config.jwe?.encryptOptions?.p2c ?? 6e5 : void 0;
313
+ }
314
+ function getEncryptKey(config, prefix) {
315
+ const key = config.key;
316
+ assertKeyMaterial(key, { prefix });
317
+ let resolved;
318
+ if (isCryptoKey(key) || isSymmetricJWK(key)) resolved = key;
319
+ else if (isKeyPair(key)) {
320
+ assertJWEKeyPair(key, prefix);
321
+ resolved = key.publicKey ?? key.privateKey;
322
+ }
323
+ if (!resolved) throw invalidKeyError(`${prefix} Invalid JWE key. It must be a JWK or a CryptoKey.`, key);
324
+ assertPinnedAlgEnc(resolved, config, prefix);
325
+ return resolved;
326
+ }
327
+ function getDecryptKey(config, prefix) {
328
+ const key = config.key;
329
+ assertKeyMaterial(key, { prefix });
330
+ let resolved;
331
+ if (isCryptoKey(key) || isSymmetricJWK(key)) resolved = key;
332
+ else if (isKeyPair(key)) {
333
+ assertJWEKeyPair(key, prefix);
334
+ resolved = key.privateKey;
335
+ }
336
+ if (!resolved) throw invalidKeyError(`${prefix} Invalid JWE key. It must be a JWK, a private JWK, or a CryptoKey.`, key);
337
+ assertPinnedAlgEnc(resolved, config, prefix);
338
+ return resolved;
339
+ }
340
+ function assertJWEKeyPair(key, prefix) {
341
+ assertKeyPair(key, {
342
+ prefix,
343
+ family: "JWE",
344
+ isPublic: (half) => half === void 0 || isCryptoKey(half) || isPublicJWK(half),
345
+ publicHalf: "a public JWK or a CryptoKey"
346
+ });
347
+ }
348
+ function assertPinnedAlgEnc(key, config, prefix) {
349
+ const { alg, enc } = config.jwe?.encryptOptions ?? {};
350
+ assertPinnedCryptoKey(key, {
351
+ pinned: Boolean(alg && enc),
352
+ prefix,
353
+ hint: "\"jwe.encryptOptions.alg\" and \"enc\""
354
+ });
355
+ }
356
+ const kSessionPending = Symbol("unjwt_session_pending");
357
+ function bindSession(carrier, config, options) {
358
+ const { port, codec, actions } = options;
359
+ const name = config.name || codec.defaultName;
360
+ assertSessionName(name, { prefix: port.prefix });
361
+ assertSessionHeader(config.sessionHeader, { prefix: port.prefix });
362
+ if (config.cookie !== false) assertCookiePrefix(name, {
363
+ ...codec.defaultCookie,
364
+ ...config.cookie
365
+ }, { prefix: port.prefix });
366
+ assertRolling(config, { prefix: port.prefix });
367
+ claimSessionName(port.scope(carrier), name, codec.family, port.prefix);
368
+ return {
369
+ carrier,
370
+ config,
371
+ port,
372
+ codec,
373
+ actions,
374
+ name,
375
+ args: port.carrier(carrier),
376
+ store: port.store(carrier, `${codec.family}:${name}`)
377
+ };
378
+ }
379
+ function sessionToken(binding) {
380
+ assertTokenSource(binding.config, { prefix: binding.port.prefix });
381
+ return binding.port.readToken(binding.carrier, binding.name, binding.config);
382
+ }
383
+ function recordSlotStore(record, slot) {
384
+ return {
385
+ get: () => record()[slot],
386
+ set: (state) => {
387
+ record()[slot] = state;
388
+ }
389
+ };
390
+ }
391
+ function sessionInstant(binding) {
392
+ const now = binding.codec.now(binding.config);
393
+ return now - now % 1e3;
394
+ }
395
+ function mintSessionId(config) {
396
+ return config.generateId?.() || crypto.randomUUID();
397
+ }
398
+ function emptySession(binding) {
399
+ const createdAt = sessionInstant(binding);
400
+ return typedSession({
401
+ id: void 0,
402
+ createdAt,
403
+ expiresAt: resolveExpiresAt(createdAt, { maxAge: binding.config.maxAge }),
404
+ data: Object.create(null),
405
+ token: void 0
406
+ });
407
+ }
408
+ function storedSession(store) {
409
+ const state = store.get();
410
+ return state === void 0 ? void 0 : typedSession(state);
411
+ }
412
+ function blankSession() {
413
+ return typedSession({
414
+ expiresAt: void 0,
415
+ data: {}
416
+ });
417
+ }
418
+ function openSessionToken(codec, config, options) {
419
+ return codec.open(config, options.token, options.carrier).then(typedSession);
420
+ }
421
+ function sessionCookie(binding, options = {}) {
422
+ const { chunkMaxLength, ...configured } = binding.config.cookie || {};
423
+ const attrs = {
424
+ ...binding.codec.defaultCookie,
425
+ ...configured
426
+ };
427
+ if (options.expiresAt !== void 0) {
428
+ attrs.expires = new Date(options.expiresAt);
429
+ attrs.maxAge = void 0;
430
+ }
431
+ return {
432
+ attrs,
433
+ chunkMaxLength
434
+ };
435
+ }
436
+ function dropSessionCookie(binding) {
437
+ const { config, port, carrier } = binding;
438
+ if (config.cookie === false) return;
439
+ if (!port.writable(carrier)) {
440
+ console.warn(`${port.prefix} Session expired but cookie cannot be cleared on a read-only event.`);
441
+ return;
442
+ }
443
+ port.deleteCookie(carrier, binding.name, sessionCookie(binding));
444
+ }
445
+ const SESSION_NAMES = /* @__PURE__ */ new WeakMap();
446
+ function claimSessionName(scope, name, family, prefix) {
447
+ let registry = SESSION_NAMES.get(scope);
448
+ if (!registry) {
449
+ registry = /* @__PURE__ */ new Map();
450
+ SESSION_NAMES.set(scope, registry);
451
+ }
452
+ const holder = registry.get(name);
453
+ if (holder === void 0) {
454
+ registry.set(name, family);
455
+ return;
456
+ }
457
+ if (holder === family) return;
458
+ throw invalidConfigError(`${prefix} Session name "${name}" is already used by a ${holder.toUpperCase()} session on this request; a name is one cookie, so give this session a "name" of its own.`);
459
+ }
460
+ function typedSession(state) {
461
+ return state;
462
+ }
463
+ async function loadSession(binding) {
464
+ const { config, codec, store, args } = binding;
465
+ await codec.assertKeys(config);
466
+ const open = codec.opener(config, args);
467
+ const existing = storedSession(store);
468
+ if (existing) {
469
+ const session = existing[kSessionPending] ? await existing[kSessionPending] : existing;
470
+ if (session.token !== void 0 && session.expiresAt !== void 0 && session.expiresAt < codec.now(config)) {
471
+ dropSessionCookie(binding);
472
+ const freshSession = emptySession(binding);
473
+ store.set(freshSession);
474
+ await fireExpire(binding, {
475
+ session: {
476
+ id: session.id,
477
+ createdAt: session.createdAt,
478
+ expiresAt: session.expiresAt,
479
+ token: session.token
480
+ },
481
+ error: expiredSessionError({
482
+ id: session.id,
483
+ expiresAt: session.expiresAt
484
+ })
485
+ });
486
+ return freshSession;
487
+ }
488
+ return session;
489
+ }
490
+ const session = emptySession(binding);
491
+ store.set(session);
492
+ const token = sessionToken(binding);
493
+ if (!token) return session;
494
+ session.token = token;
495
+ const pending = open(token).then((opened) => {
496
+ Object.assign(session, opened);
497
+ return { failure: void 0 };
498
+ }, (error) => {
499
+ session.token = void 0;
500
+ return { failure: { error } };
501
+ }).then((outcome) => {
502
+ delete session[kSessionPending];
503
+ return outcome;
504
+ });
505
+ session[kSessionPending] = pending.then(() => session);
506
+ const { failure } = await pending;
507
+ if (failure) {
508
+ if (isJWTError(failure.error, "ERR_JWT_EXPIRED")) {
509
+ const { jti, iat, exp } = failure.error.cause;
510
+ dropSessionCookie(binding);
511
+ await fireExpire(binding, {
512
+ session: {
513
+ id: jti,
514
+ createdAt: iat ? iat * 1e3 : void 0,
515
+ expiresAt: exp ? exp * 1e3 : void 0,
516
+ token
517
+ },
518
+ error: failure.error
519
+ });
520
+ } else await config.hooks?.onError?.({
521
+ session: binding.actions.manager(binding),
522
+ ...args,
523
+ error: failure.error,
524
+ config
525
+ });
526
+ return session;
527
+ }
528
+ await fireRead(binding, session);
529
+ await rollSession(binding, session);
530
+ return session;
531
+ }
532
+ async function fireRead(binding, session) {
533
+ if (session.id === void 0 || session.token === void 0) return;
534
+ await binding.config.hooks?.onRead?.({
535
+ session: binding.actions.manager(binding),
536
+ ...binding.args,
537
+ config: binding.config
538
+ });
539
+ }
540
+ async function rollSession(binding, session) {
541
+ const { config, codec, port, carrier, store } = binding;
542
+ const { rolling } = config;
543
+ if (rolling === void 0 || store.get() !== session) return;
544
+ if (session.token === void 0 || session.expiresAt === void 0) return;
545
+ if (rolling !== true) {
546
+ if (session.expiresAt - codec.now(config) >= computeDurationInSeconds(rolling) * 1e3) return;
547
+ }
548
+ if (config.cookie !== false && !port.writable(carrier)) {
549
+ console.warn(`${port.prefix} Session not rolled: cookie cannot be written on a read-only event.`);
550
+ return;
551
+ }
552
+ await binding.actions.roll(binding);
553
+ }
554
+ async function fireExpire(binding, expired) {
555
+ await binding.config.hooks?.onExpire?.({
556
+ session: expired.session,
557
+ ...binding.args,
558
+ error: expired.error,
559
+ config: binding.config
560
+ });
561
+ }
562
+ async function encodeSession(binding) {
563
+ const { config, codec, store } = binding;
564
+ await codec.assertKeys(config);
565
+ const issue = codec.issuer(config);
566
+ const session = storedSession(store) || (hasTokenSource(config) ? await loadSession(binding) : emptySession(binding));
567
+ return issue(session.id === void 0 ? {
568
+ ...session,
569
+ id: mintSessionId(config)
570
+ } : session);
571
+ }
572
+ async function issueSession(binding, update, options, trigger = "update") {
573
+ const { config, port, store, args } = binding;
574
+ assertWritable(binding, "update");
575
+ const session = storedSession(store) || await loadSession(binding);
576
+ const oldSession = snapshotSession(session);
577
+ const resolved = typeof update === "function" ? update(session.data) : update;
578
+ if (resolved) Object.assign(session.data, sanitizeSessionUpdate(resolved, { prefix: port.prefix }));
579
+ for (const claim of RESERVED_CLAIMS) delete session.data[claim];
580
+ const maxAge = options?.maxAge ?? config.maxAge;
581
+ const createdAt = sessionInstant(binding);
582
+ const id = mintSessionId(config);
583
+ Object.assign(session, {
584
+ id,
585
+ createdAt,
586
+ expiresAt: resolveExpiresAt(createdAt, { maxAge })
587
+ });
588
+ let issued;
589
+ try {
590
+ const token = await encodeSession(binding);
591
+ if (config.cookie !== false) port.setCookie(binding.carrier, binding.name, token, sessionCookie(binding, { expiresAt: session.expiresAt }));
592
+ issued = Object.assign(session, {
593
+ id,
594
+ token
595
+ });
596
+ } catch (error_) {
597
+ Object.assign(session, {
598
+ id: oldSession.id,
599
+ createdAt: oldSession.createdAt,
600
+ expiresAt: oldSession.expiresAt,
601
+ data: oldSession.data
602
+ });
603
+ session.token = oldSession.token;
604
+ await config.hooks?.onError?.({
605
+ session: binding.actions.manager(binding),
606
+ ...args,
607
+ error: error_,
608
+ config
609
+ });
610
+ throw error_;
611
+ }
612
+ await config.hooks?.onUpdate?.({
613
+ session: binding.actions.manager(binding),
614
+ oldSession,
615
+ trigger,
616
+ ...args,
617
+ config
618
+ });
619
+ return issued;
620
+ }
621
+ async function dropSession(binding) {
622
+ const { config, port, store, args } = binding;
623
+ assertTokenSource(config, { prefix: port.prefix });
624
+ assertWritable(binding, "clear");
625
+ let session = storedSession(store);
626
+ if (session && session[kSessionPending]) session = await session[kSessionPending];
627
+ const oldSession = session && (session.id !== void 0 || session.token !== void 0) ? snapshotSession(session) : void 0;
628
+ store.set(emptySession(binding));
629
+ if (config.cookie !== false) port.deleteCookie(binding.carrier, binding.name, sessionCookie(binding));
630
+ await config.hooks?.onClear?.({
631
+ oldSession,
632
+ ...args,
633
+ config
634
+ });
635
+ }
636
+ function sessionManager(binding) {
637
+ const { store } = binding;
638
+ const state = () => storedSession(store);
639
+ const manager = {
640
+ get id() {
641
+ return state()?.id;
642
+ },
643
+ get createdAt() {
644
+ return state()?.createdAt ?? Date.now();
645
+ },
646
+ get expiresAt() {
647
+ return (state() ?? blankSession()).expiresAt;
648
+ },
649
+ get data() {
650
+ return (state() ?? blankSession()).data;
651
+ },
652
+ get token() {
653
+ const session = state();
654
+ return session ? session.token : sessionToken(binding);
655
+ },
656
+ update: async (update, options) => {
657
+ await issueSession(binding, update, options);
658
+ return manager;
659
+ },
660
+ clear: async () => {
661
+ await dropSession(binding);
662
+ return manager;
663
+ }
664
+ };
665
+ return manager;
666
+ }
667
+ function sessionActions() {
668
+ return {
669
+ manager: (binding) => sessionManager(binding),
670
+ roll: async (binding) => {
671
+ await issueSession(binding, void 0, void 0, "rolling");
672
+ }
673
+ };
674
+ }
675
+ function assertWritable(binding, operation) {
676
+ const { config, port, carrier } = binding;
677
+ if (config.cookie !== false && !port.writable(carrier)) throw readOnlyEventError(`${port.prefix} Cannot ${operation} session on read-only event.`);
678
+ }
679
+ function snapshotSession(session) {
680
+ return {
681
+ ...session,
682
+ data: sanitizeObjectCopy(session.data)
683
+ };
684
+ }
685
+ const CHUNK_MARKER = "__chunked__";
686
+ const DEFAULT_CHUNK_MAX_LENGTH = 4e3;
687
+ function responseCookieStore(cookies) {
688
+ return {
689
+ read: (name) => setCookieValues(cookies.response())[name] ?? cookies.request(name),
690
+ write: cookies.write
691
+ };
692
+ }
693
+ function readChunked(store, name) {
694
+ const main = store.read(name);
695
+ if (main === void 0) return void 0;
696
+ if (!main.startsWith("__chunked__")) return main;
697
+ const count = parseChunkCount(main);
698
+ if (count === 0) return void 0;
699
+ const parts = [];
700
+ for (let index = 1; index <= count; index++) {
701
+ const part = store.read(chunkName(name, index));
702
+ if (part === void 0) return void 0;
703
+ parts.push(part);
704
+ }
705
+ return parts.join("");
706
+ }
707
+ function writeChunked(store, name, value, options) {
708
+ const { attrs, chunkMaxLength, prefix } = options;
709
+ const max = chunkMaxLength && chunkMaxLength > 0 ? chunkMaxLength : DEFAULT_CHUNK_MAX_LENGTH;
710
+ const chunkCount = Math.ceil(value.length / max);
711
+ if (chunkCount > 16) throw invalidConfigError(`${prefix} Session token requires ${chunkCount} cookie chunks, exceeding the limit of 16.`);
712
+ const previousCount = parseChunkCount(store.read(name));
713
+ const firstSurplus = chunkCount <= 1 ? 1 : chunkCount + 1;
714
+ for (let index = firstSurplus; index <= previousCount; index++) expire(store, chunkName(name, index), attrs);
715
+ if (chunkCount <= 1) {
716
+ store.write(name, value, attrs);
717
+ return;
718
+ }
719
+ store.write(name, `${CHUNK_MARKER}${chunkCount}`, attrs);
720
+ for (let index = 1; index <= chunkCount; index++) store.write(chunkName(name, index), value.slice((index - 1) * max, index * max), attrs);
721
+ }
722
+ function removeChunked(store, name, options) {
723
+ const count = parseChunkCount(store.read(name));
724
+ expire(store, name, options.attrs);
725
+ for (let index = 1; index <= count; index++) expire(store, chunkName(name, index), options.attrs);
726
+ }
727
+ function readChunkedBy(read, name) {
728
+ return readChunked({
729
+ read,
730
+ write: noop
731
+ }, name);
732
+ }
733
+ function readChunkedFrom(cookies, name) {
734
+ return readChunkedBy((key) => cookies[key], name);
735
+ }
736
+ function readChunkedFromSetCookie(setCookies, name) {
737
+ const cookies = setCookieValues(setCookies);
738
+ if (cookies[name] === void 0) return void 0;
739
+ return readChunkedFrom(cookies, name);
740
+ }
741
+ function setCookieValues(setCookies) {
742
+ const cookies = Object.create(null);
743
+ for (const setCookie of setCookies) {
744
+ const pair = setCookiePair(setCookie);
745
+ if (pair) cookies[pair.name] = decodeValue(pair.value);
746
+ }
747
+ return cookies;
748
+ }
749
+ function setCookieName(setCookie) {
750
+ return setCookiePair(setCookie)?.name;
751
+ }
752
+ function chunkName(name, index) {
753
+ return `${name}.${index}`;
754
+ }
755
+ function isChunkName(cookieName, name) {
756
+ return cookieName.startsWith(`${name}.`) && /^\d+$/.test(cookieName.slice(name.length + 1));
757
+ }
758
+ function parseChunkCount(main) {
759
+ if (main === void 0 || !main.startsWith("__chunked__")) return 0;
760
+ const suffix = main.slice(11);
761
+ if (!/^\d+$/.test(suffix)) return 0;
762
+ const count = Number.parseInt(suffix, 10);
763
+ if (count < 1 || count > 16) return 0;
764
+ return count;
765
+ }
766
+ function expire(store, name, attrs) {
767
+ store.write(name, "", {
768
+ ...attrs,
769
+ maxAge: 0,
770
+ expires: /* @__PURE__ */ new Date(0)
771
+ });
772
+ }
773
+ function setCookiePair(setCookie) {
774
+ const [pair = ""] = setCookie.split(";", 1);
775
+ const separator = pair.indexOf("=");
776
+ if (separator === -1) return void 0;
777
+ return {
778
+ name: pair.slice(0, separator).trim(),
779
+ value: pair.slice(separator + 1).trim()
780
+ };
781
+ }
782
+ function decodeValue(value) {
783
+ if (!value.includes("%")) return value;
784
+ try {
785
+ return decodeURIComponent(value);
786
+ } catch {
787
+ return value;
788
+ }
789
+ }
790
+ function noop() {}
791
+ const JWS_DEFAULT_COOKIE = {
792
+ path: "/",
793
+ secure: true,
794
+ httpOnly: false,
795
+ sameSite: "lax"
796
+ };
797
+ function createJWSCodec(options) {
798
+ const { prefix, defaultName } = options;
799
+ return createSessionCodec({
800
+ family: "jws",
801
+ prefix,
802
+ defaultName,
803
+ defaultCookie: JWS_DEFAULT_COOKIE,
804
+ issueOptions: (config) => config.jws?.signOptions,
805
+ openOptions: (config) => config.jws?.verifyOptions,
806
+ issueKey: (config) => getSignKey(config, prefix),
807
+ openKey: (config) => getVerifyKey(config, prefix),
808
+ lookup: (config, carrier) => {
809
+ const hooks = config.hooks;
810
+ if (!hooks?.onVerifyKeyLookup) return void 0;
811
+ return (header) => hooks.onVerifyKeyLookup?.({
812
+ header,
813
+ ...carrier,
814
+ config
815
+ });
816
+ },
817
+ importable: (key) => isJWKSet(key) ? key.keys : [key],
818
+ issue: (config, payload, key) => sign(payload, key, sessionIssueOptions(config.jws?.signOptions)),
819
+ open: async (config, token, key) => {
820
+ const { verifyOptions, signOptions } = config.jws ?? {};
821
+ const { payload } = await verify(token, key, {
822
+ ...verifyOptions,
823
+ ...sessionOpenOptions({
824
+ maxAge: config.maxAge,
825
+ requiredClaims: verifyOptions?.requiredClaims,
826
+ typ: signOptions?.protectedHeader?.typ,
827
+ alg: signOptions?.alg
828
+ })
829
+ });
830
+ return payload;
831
+ }
832
+ });
833
+ }
834
+ function getSignKey(config, prefix) {
835
+ const key = config.key;
836
+ assertKeyMaterial(key, { prefix });
837
+ let resolved;
838
+ if (isCryptoKey(key) || isSymmetricJWK(key)) resolved = key;
839
+ else if (isKeyPair(key)) {
840
+ assertJWSKeyPair(key, prefix);
841
+ resolved = key.privateKey;
842
+ }
843
+ if (!resolved) throw invalidKeyError(`${prefix} Invalid JWS key. It must be a symmetric JWK, a private JWK, or a CryptoKey.`, key);
844
+ assertPinnedAlg(resolved, config, prefix);
845
+ return resolved;
846
+ }
847
+ function getVerifyKey(config, prefix) {
848
+ const key = config.key;
849
+ assertKeyMaterial(key, { prefix });
850
+ let resolved;
851
+ if (isCryptoKey(key) || isSymmetricJWK(key)) resolved = key;
852
+ else if (isKeyPair(key)) {
853
+ assertJWSKeyPair(key, prefix);
854
+ resolved = Array.isArray(key.publicKey) ? { keys: key.publicKey } : key.publicKey;
855
+ }
856
+ if (!resolved) throw invalidKeyError(`${prefix} Invalid JWS key. It must be a symmetric JWK, a public JWK/set, or a CryptoKey.`, key);
857
+ assertPinnedAlg(resolved, config, prefix);
858
+ return resolved;
859
+ }
860
+ function assertJWSKeyPair(key, prefix) {
861
+ assertKeyPair(key, {
862
+ prefix,
863
+ family: "JWS",
864
+ isPublic: (half) => isCryptoKey(half) || isPublicJWK(half) || isPublicKeyList(Array.isArray(half) ? half : isJWKSet(half) ? half.keys : void 0),
865
+ publicHalf: "a public JWK, a non-empty list or set of public JWKs, or a CryptoKey"
866
+ });
867
+ }
868
+ function isPublicKeyList(keys) {
869
+ return keys !== void 0 && keys.length > 0 && keys.every((candidate) => isPublicJWK(candidate));
870
+ }
871
+ function assertPinnedAlg(key, config, prefix) {
872
+ assertPinnedCryptoKey(key, {
873
+ pinned: Boolean(config.jws?.signOptions?.alg),
874
+ prefix,
875
+ hint: "\"jws.signOptions.alg\""
876
+ });
877
+ }
878
+ export { bindSession, createJWECodec, createJWSCodec, dropSession, encodeSession, invalidConfigError, isChunkName, issueSession, loadSession, openSessionToken, readChunked, readChunkedBy, readChunkedFrom, readChunkedFromSetCookie, recordSlotStore, removeChunked, resolveSessionToken, responseCookieStore, sessionActions, sessionManager, sessionToken, setCookieName, writeChunked };