unjwt 0.8.1 → 0.8.2

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.
@@ -36,6 +36,9 @@ function expiredSessionError(options) {
36
36
  exp
37
37
  });
38
38
  }
39
+ function isTokenFailure(error) {
40
+ return !(isJWTError(error.cause, "ERR_JWK_KEY_NOT_FOUND") && error.cause.cause !== void 0);
41
+ }
39
42
  function rethrowAsInvalidToken(error, options) {
40
43
  if (isJWTError(error, "ERR_JWT_EXPIRED")) throw error;
41
44
  throw invalidTokenError(`${options.prefix} Invalid session token: ${messageOf(error)}`, error);
@@ -43,9 +46,12 @@ function rethrowAsInvalidToken(error, options) {
43
46
  const TOKEN = /^[!#$%&'*+\-.^_`|~\w]+$/;
44
47
  const CHUNK_SUFFIX = /\.\d+$/;
45
48
  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)}.`);
49
+ if (!isToken(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
50
  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
51
  }
52
+ function isToken(value) {
53
+ return typeof value === "string" && TOKEN.test(value);
54
+ }
49
55
  function assertCookiePrefix(name, attrs, options) {
50
56
  const lower = name.toLowerCase();
51
57
  const host = lower.startsWith("__host-");
@@ -53,24 +59,13 @@ function assertCookiePrefix(name, attrs, options) {
53
59
  const rule = host ? "\"__Host-\" cookie must be Secure, with Path=/ and no Domain" : "\"__Secure-\" cookie must be Secure";
54
60
  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
61
  }
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
62
  function assertRolling(config, options) {
62
63
  const { rolling } = config;
63
64
  if (rolling === void 0) return;
64
65
  if (config.maxAge === void 0) throw invalidConfigError(`${options.prefix} "rolling" needs a "maxAge" to extend.`);
65
66
  if (rolling === true) return;
66
67
  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.`);
68
+ if (computeDurationInSeconds(rolling) >= computeDurationInSeconds(config.maxAge)) throw invalidConfigError(`${options.prefix} "rolling" must be shorter than "maxAge": the session would expire before it rolls.`);
74
69
  }
75
70
  function assertKeyMaterial(key, options) {
76
71
  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.`);
@@ -101,27 +96,6 @@ async function assertImportableKey(key, options) {
101
96
  function sessionTyp(configured) {
102
97
  return typeof configured === "string" && configured ? configured : SESSION_TYP;
103
98
  }
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
99
  function resolveExpiresAt(createdAt, options) {
126
100
  return options.maxAge === void 0 ? void 0 : createdAt + computeDurationInSeconds(options.maxAge) * 1e3;
127
101
  }
@@ -153,7 +127,7 @@ function isExpiresIn(value) {
153
127
  function createSessionCodec(spec) {
154
128
  const { prefix } = spec;
155
129
  const alg = (config) => spec.issueOptions(config)?.alg;
156
- const keyFor = (config, carrier) => spec.lookup(config, carrier) ?? spec.openKey(config);
130
+ const keyFor = (config, args) => spec.lookup(config, args) ?? spec.openKey(config);
157
131
  const now = (config) => spec.issueOptions(config)?.currentDate?.getTime() ?? Date.now();
158
132
  const openWith = async (config, token, key) => {
159
133
  const payload = await spec.open(config, token, key).catch((error) => rethrowAsInvalidToken(error, { prefix }));
@@ -189,13 +163,13 @@ function createSessionCodec(spec) {
189
163
  });
190
164
  if (!isSymmetricJWK(key)) await assertOpenKey(config);
191
165
  },
192
- opener: (config, carrier) => {
193
- const key = keyFor(config, carrier);
166
+ opener: (config, args) => {
167
+ const key = keyFor(config, args);
194
168
  return (token) => openWith(config, token, key);
195
169
  },
196
- open: async (config, token, carrier) => {
170
+ open: async (config, token, args) => {
197
171
  await assertOpenKey(config);
198
- return openWith(config, token, keyFor(config, carrier));
172
+ return openWith(config, token, keyFor(config, args));
199
173
  }
200
174
  };
201
175
  }
@@ -277,12 +251,12 @@ function createJWECodec(options) {
277
251
  openOptions: (config) => config.jwe?.decryptOptions,
278
252
  issueKey: (config) => getEncryptKey(config, prefix),
279
253
  openKey: (config) => getDecryptKey(config, prefix),
280
- lookup: (config, carrier) => {
254
+ lookup: (config, args) => {
281
255
  const hooks = config.hooks;
282
256
  if (!hooks?.onUnsealKeyLookup) return void 0;
283
257
  return (header) => hooks.onUnsealKeyLookup?.({
284
258
  header,
285
- ...carrier,
259
+ ...args,
286
260
  config
287
261
  });
288
262
  },
@@ -353,43 +327,245 @@ function assertPinnedAlgEnc(key, config, prefix) {
353
327
  hint: "\"jwe.encryptOptions.alg\" and \"enc\""
354
328
  });
355
329
  }
356
- const kSessionPending = Symbol("unjwt_session_pending");
357
- function bindSession(carrier, config, options) {
358
- const { port, codec, actions } = options;
330
+ const CHUNK_MARKER = "__chunked__";
331
+ const DEFAULT_CHUNK_MAX_LENGTH = 4e3;
332
+ function readChunked(store, name) {
333
+ const main = store.read(name);
334
+ if (main === void 0) return void 0;
335
+ if (!main.startsWith("__chunked__")) return main;
336
+ const count = parseChunkCount(main);
337
+ if (count === 0) return void 0;
338
+ const parts = [];
339
+ for (let index = 1; index <= count; index++) {
340
+ const part = store.read(chunkName(name, index));
341
+ if (part === void 0) return void 0;
342
+ parts.push(part);
343
+ }
344
+ return parts.join("");
345
+ }
346
+ function writeChunked(store, name, value, options) {
347
+ const { attrs, chunkMaxLength, prefix } = options;
348
+ const max = chunkMaxLength && chunkMaxLength > 0 ? chunkMaxLength : DEFAULT_CHUNK_MAX_LENGTH;
349
+ const chunkCount = Math.ceil(value.length / max);
350
+ if (chunkCount > 16) throw invalidConfigError(`${prefix} Session token requires ${chunkCount} cookie chunks, exceeding the limit of 16.`);
351
+ const previousCount = parseChunkCount(store.read(name));
352
+ const firstSurplus = chunkCount <= 1 ? 1 : chunkCount + 1;
353
+ for (let index = firstSurplus; index <= previousCount; index++) expire(store, chunkName(name, index), attrs);
354
+ if (chunkCount <= 1) {
355
+ store.write(name, value, attrs);
356
+ return;
357
+ }
358
+ store.write(name, `${CHUNK_MARKER}${chunkCount}`, attrs);
359
+ for (let index = 1; index <= chunkCount; index++) store.write(chunkName(name, index), value.slice((index - 1) * max, index * max), attrs);
360
+ }
361
+ function removeChunked(store, name, options) {
362
+ const count = parseChunkCount(store.read(name));
363
+ expire(store, name, options.attrs);
364
+ for (let index = 1; index <= count; index++) expire(store, chunkName(name, index), options.attrs);
365
+ }
366
+ function setCookieValues(setCookies) {
367
+ const cookies = Object.create(null);
368
+ for (const setCookie of setCookies) {
369
+ const pair = setCookiePair(setCookie);
370
+ if (pair) cookies[pair.name] = decodeValue(pair.value);
371
+ }
372
+ return cookies;
373
+ }
374
+ function setCookieName(setCookie) {
375
+ return setCookiePair(setCookie)?.name;
376
+ }
377
+ function chunkName(name, index) {
378
+ return `${name}.${index}`;
379
+ }
380
+ function parseChunkCount(main) {
381
+ if (main === void 0 || !main.startsWith("__chunked__")) return 0;
382
+ const suffix = main.slice(11);
383
+ if (!/^\d+$/.test(suffix)) return 0;
384
+ const count = Number.parseInt(suffix, 10);
385
+ if (count < 1 || count > 16) return 0;
386
+ return count;
387
+ }
388
+ function expire(store, name, attrs) {
389
+ store.write(name, "", {
390
+ ...attrs,
391
+ maxAge: 0,
392
+ expires: /* @__PURE__ */ new Date(0)
393
+ });
394
+ }
395
+ function setCookiePair(setCookie) {
396
+ const [pair = ""] = setCookie.split(";", 1);
397
+ const separator = pair.indexOf("=");
398
+ if (separator === -1) return void 0;
399
+ return {
400
+ name: pair.slice(0, separator).trim(),
401
+ value: pair.slice(separator + 1).trim()
402
+ };
403
+ }
404
+ function decodeValue(value) {
405
+ if (!value.includes("%")) return value;
406
+ try {
407
+ return decodeURIComponent(value);
408
+ } catch {
409
+ return value;
410
+ }
411
+ }
412
+ function resolveTransport(driver, context, options) {
413
+ const handed = driver.handed?.(context);
414
+ if (handed) return {
415
+ read: () => handed.token,
416
+ readonly: false,
417
+ writable: () => true
418
+ };
419
+ const { config, name } = options;
420
+ const header = headerReader(driver, context, options);
421
+ if (config.cookie === false) return {
422
+ read: header,
423
+ readonly: false,
424
+ writable: () => true
425
+ };
426
+ const jar = driver.cookies(context);
427
+ const store = responseStore(jar);
428
+ const transport = {
429
+ read: () => {
430
+ const written = jar.written ? readChunked({ read: (key) => jar.written?.(key) }, name) : void 0;
431
+ if (written === "") return void 0;
432
+ return (written ?? readChunked(jar, name)) || header?.() || void 0;
433
+ },
434
+ readonly: !store,
435
+ writable: () => driver.writable?.(context) ?? true
436
+ };
437
+ if (!store) return transport;
438
+ transport.write = (token, { attrs, chunkMaxLength }) => {
439
+ writeChunked(store, name, token, {
440
+ attrs,
441
+ chunkMaxLength,
442
+ prefix: driver.prefix
443
+ });
444
+ };
445
+ transport.drop = ({ attrs }) => {
446
+ removeChunked(store, name, { attrs });
447
+ };
448
+ return transport;
449
+ }
450
+ function assertTokenSource(transport, options) {
451
+ if (!transport.read) throw invalidConfigError(`${options.prefix} Session has no token source: "cookie" and "sessionHeader" are both disabled.`);
452
+ }
453
+ function assertWritable(transport, options) {
454
+ const { operation, prefix } = options;
455
+ if (operation === "update" && transport.readonly) throw readOnlyEventError(`${prefix} Cannot update session on read-only event.`);
456
+ if (!transport.writable()) throw readOnlyEventError(`${prefix} Cannot update/clear session: the response was already sent.`);
457
+ }
458
+ function assertSessionHeader(header, options) {
459
+ if (header === void 0 || typeof header === "boolean") return;
460
+ if (isToken(header)) return;
461
+ throw invalidConfigError(`${options.prefix} Session "sessionHeader" must be true, false or a header name: letters, digits and !#$%&'*+-.^_\`|~ only. Got ${quoteTokenValue(header)}.`);
462
+ }
463
+ function sessionHeaderName(sessionName, configured) {
464
+ if (typeof configured === "string") return configured.toLowerCase();
465
+ return `x-${sessionName.replace(/^__(?:host|secure)-/i, "").toLowerCase()}-session`;
466
+ }
467
+ function headerToken(value) {
468
+ if (typeof value !== "string") return void 0;
469
+ const words = value.trim().split(/\s+/);
470
+ const [first = "", second] = words;
471
+ if (/^bearer$/i.test(first)) return words.length === 2 ? second : void 0;
472
+ return words.length === 1 && first ? first : void 0;
473
+ }
474
+ function headerReader(driver, context, options) {
475
+ const { config, name } = options;
476
+ if (config.sessionHeader === false) return void 0;
477
+ const header = sessionHeaderName(name, config.sessionHeader);
478
+ return () => headerToken(driver.header(context, header));
479
+ }
480
+ function responseStore(jar) {
481
+ const write = jar.write?.bind(jar);
482
+ if (!write) return void 0;
483
+ return {
484
+ read: (name) => jar.written?.(name) ?? jar.read(name),
485
+ write
486
+ };
487
+ }
488
+ const kSessionNames = Symbol("unjwt_session_names");
489
+ function bindSession(context, config, options) {
490
+ const { driver, codec, actions, hookArgs } = options;
491
+ const { prefix } = driver;
359
492
  const name = config.name || codec.defaultName;
360
- assertSessionName(name, { prefix: port.prefix });
361
- assertSessionHeader(config.sessionHeader, { prefix: port.prefix });
493
+ assertSessionName(name, { prefix });
494
+ assertSessionHeader(config.sessionHeader, { prefix });
362
495
  if (config.cookie !== false) assertCookiePrefix(name, {
363
496
  ...codec.defaultCookie,
364
497
  ...config.cookie
365
- }, { prefix: port.prefix });
366
- assertRolling(config, { prefix: port.prefix });
367
- claimSessionName(port.scope(carrier), name, codec.family, port.prefix);
498
+ }, { prefix });
499
+ assertRolling(config, { prefix });
500
+ const record = driver.state(context);
501
+ claimSessionName(record, {
502
+ name,
503
+ family: codec.family,
504
+ prefix
505
+ });
368
506
  return {
369
- carrier,
507
+ context,
370
508
  config,
371
- port,
509
+ driver,
372
510
  codec,
373
511
  actions,
374
512
  name,
375
- args: port.carrier(carrier),
376
- store: port.store(carrier, `${codec.family}:${name}`)
513
+ args: hookArgs(context),
514
+ store: recordSlotStore(record, `${codec.family}:${name}`),
515
+ transport: resolveTransport(driver, context, {
516
+ config,
517
+ name
518
+ })
377
519
  };
378
520
  }
379
521
  function sessionToken(binding) {
380
- assertTokenSource(binding.config, { prefix: binding.port.prefix });
381
- return binding.port.readToken(binding.carrier, binding.name, binding.config);
522
+ const { transport } = binding;
523
+ assertTokenSource(transport, { prefix: binding.driver.prefix });
524
+ return transport.read();
382
525
  }
383
526
  function recordSlotStore(record, slot) {
384
527
  return {
385
- get: () => record()[slot],
528
+ get: () => record[slot],
386
529
  set: (state) => {
387
- record()[slot] = state;
530
+ record[slot] = state;
388
531
  }
389
532
  };
390
533
  }
391
- function sessionInstant(binding) {
392
- const now = binding.codec.now(binding.config);
534
+ function sessionCookie(binding, options = {}) {
535
+ const { chunkMaxLength, ...configured } = binding.config.cookie || {};
536
+ const attrs = {
537
+ ...binding.codec.defaultCookie,
538
+ ...configured
539
+ };
540
+ if (options.expiresAt !== void 0) {
541
+ attrs.expires = new Date(options.expiresAt);
542
+ attrs.maxAge = void 0;
543
+ }
544
+ return {
545
+ attrs,
546
+ chunkMaxLength
547
+ };
548
+ }
549
+ function dropSessionCookie(binding) {
550
+ const { transport } = binding;
551
+ if (!transport.drop || !transport.writable()) return;
552
+ transport.drop(sessionCookie(binding));
553
+ }
554
+ function claimSessionName(record, options) {
555
+ const { name, family, prefix } = options;
556
+ const holder = record;
557
+ const registry = holder[kSessionNames] ||= /* @__PURE__ */ new Map();
558
+ const held = registry.get(name);
559
+ if (held === void 0) {
560
+ registry.set(name, family);
561
+ return;
562
+ }
563
+ if (held === family) return;
564
+ throw invalidConfigError(`${prefix} Session name "${name}" is already used by a ${held.toUpperCase()} session on this request; a name is one cookie, so give this session a "name" of its own.`);
565
+ }
566
+ const kSessionPending = Symbol("unjwt_session_pending");
567
+ function sessionInstant(source) {
568
+ const now = source.codec.now(source.config);
393
569
  return now - now % 1e3;
394
570
  }
395
571
  function mintSessionId(config) {
@@ -416,54 +592,19 @@ function blankSession() {
416
592
  });
417
593
  }
418
594
  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.`);
595
+ return codec.open(config, options.token, options.args).then(typedOpened);
459
596
  }
460
597
  function typedSession(state) {
461
598
  return state;
462
599
  }
600
+ function typedOpened(opened) {
601
+ return opened;
602
+ }
463
603
  async function loadSession(binding) {
464
- const { config, codec, store, args } = binding;
604
+ const { config, codec, store, args, transport } = binding;
465
605
  await codec.assertKeys(config);
466
606
  const open = codec.opener(config, args);
607
+ assertTokenSource(transport, { prefix: binding.driver.prefix });
467
608
  const existing = storedSession(store);
468
609
  if (existing) {
469
610
  const session = existing[kSessionPending] ? await existing[kSessionPending] : existing;
@@ -489,7 +630,7 @@ async function loadSession(binding) {
489
630
  }
490
631
  const session = emptySession(binding);
491
632
  store.set(session);
492
- const token = sessionToken(binding);
633
+ const token = transport.read();
493
634
  if (!token) return session;
494
635
  session.token = token;
495
636
  const pending = open(token).then((opened) => {
@@ -517,12 +658,15 @@ async function loadSession(binding) {
517
658
  },
518
659
  error: failure.error
519
660
  });
520
- } else await config.hooks?.onError?.({
521
- session: binding.actions.manager(binding),
522
- ...args,
523
- error: failure.error,
524
- config
525
- });
661
+ } else {
662
+ if (isJWTError(failure.error, "ERR_SESSION_TOKEN_INVALID") && isTokenFailure(failure.error)) dropSessionCookie(binding);
663
+ await config.hooks?.onError?.({
664
+ session: binding.actions.manager(binding),
665
+ ...args,
666
+ error: failure.error,
667
+ config
668
+ });
669
+ }
526
670
  return session;
527
671
  }
528
672
  await fireRead(binding, session);
@@ -538,17 +682,14 @@ async function fireRead(binding, session) {
538
682
  });
539
683
  }
540
684
  async function rollSession(binding, session) {
541
- const { config, codec, port, carrier, store } = binding;
685
+ const { config, codec, transport, store } = binding;
542
686
  const { rolling } = config;
543
687
  if (rolling === void 0 || store.get() !== session) return;
544
688
  if (session.token === void 0 || session.expiresAt === void 0) return;
545
689
  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;
690
+ if (codec.now(config) - session.createdAt < computeDurationInSeconds(rolling) * 1e3) return;
551
691
  }
692
+ if (transport.readonly || !transport.writable()) return;
552
693
  await binding.actions.roll(binding);
553
694
  }
554
695
  async function fireExpire(binding, expired) {
@@ -560,22 +701,25 @@ async function fireExpire(binding, expired) {
560
701
  });
561
702
  }
562
703
  async function encodeSession(binding) {
563
- const { config, codec, store } = binding;
704
+ const { config, codec, store, transport } = binding;
564
705
  await codec.assertKeys(config);
565
706
  const issue = codec.issuer(config);
566
- const session = storedSession(store) || (hasTokenSource(config) ? await loadSession(binding) : emptySession(binding));
707
+ const session = storedSession(store) || (transport.read ? await loadSession(binding) : emptySession(binding));
567
708
  return issue(session.id === void 0 ? {
568
709
  ...session,
569
710
  id: mintSessionId(config)
570
711
  } : session);
571
712
  }
572
713
  async function issueSession(binding, update, options, trigger = "update") {
573
- const { config, port, store, args } = binding;
574
- assertWritable(binding, "update");
714
+ const { config, driver, store, args, transport } = binding;
715
+ assertWritable(transport, {
716
+ operation: "update",
717
+ prefix: driver.prefix
718
+ });
575
719
  const session = storedSession(store) || await loadSession(binding);
576
720
  const oldSession = snapshotSession(session);
577
721
  const resolved = typeof update === "function" ? update(session.data) : update;
578
- if (resolved) Object.assign(session.data, sanitizeSessionUpdate(resolved, { prefix: port.prefix }));
722
+ if (resolved) Object.assign(session.data, sanitizeSessionUpdate(resolved, { prefix: driver.prefix }));
579
723
  for (const claim of RESERVED_CLAIMS) delete session.data[claim];
580
724
  const maxAge = options?.maxAge ?? config.maxAge;
581
725
  const createdAt = sessionInstant(binding);
@@ -588,7 +732,7 @@ async function issueSession(binding, update, options, trigger = "update") {
588
732
  let issued;
589
733
  try {
590
734
  const token = await encodeSession(binding);
591
- if (config.cookie !== false) port.setCookie(binding.carrier, binding.name, token, sessionCookie(binding, { expiresAt: session.expiresAt }));
735
+ transport.write?.(token, sessionCookie(binding, { expiresAt: session.expiresAt }));
592
736
  issued = Object.assign(session, {
593
737
  id,
594
738
  token
@@ -619,14 +763,17 @@ async function issueSession(binding, update, options, trigger = "update") {
619
763
  return issued;
620
764
  }
621
765
  async function dropSession(binding) {
622
- const { config, port, store, args } = binding;
623
- assertTokenSource(config, { prefix: port.prefix });
624
- assertWritable(binding, "clear");
766
+ const { config, driver, store, args, transport } = binding;
767
+ assertTokenSource(transport, { prefix: driver.prefix });
768
+ assertWritable(transport, {
769
+ operation: "clear",
770
+ prefix: driver.prefix
771
+ });
625
772
  let session = storedSession(store);
626
773
  if (session && session[kSessionPending]) session = await session[kSessionPending];
627
774
  const oldSession = session && (session.id !== void 0 || session.token !== void 0) ? snapshotSession(session) : void 0;
628
775
  store.set(emptySession(binding));
629
- if (config.cookie !== false) port.deleteCookie(binding.carrier, binding.name, sessionCookie(binding));
776
+ transport.drop?.(sessionCookie(binding));
630
777
  await config.hooks?.onClear?.({
631
778
  oldSession,
632
779
  ...args,
@@ -672,122 +819,49 @@ function sessionActions() {
672
819
  }
673
820
  };
674
821
  }
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
822
  function snapshotSession(session) {
680
823
  return {
681
824
  ...session,
682
825
  data: sanitizeObjectCopy(session.data)
683
826
  };
684
827
  }
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)
828
+ function defineSession(config, options) {
829
+ const { driver, codec, hookArgs } = options;
830
+ const actions = sessionActions();
831
+ const bind = (context) => bindSession(context, config, {
832
+ driver,
833
+ codec,
834
+ actions,
835
+ hookArgs
771
836
  });
772
- }
773
- function setCookiePair(setCookie) {
774
- const [pair = ""] = setCookie.split(";", 1);
775
- const separator = pair.indexOf("=");
776
- if (separator === -1) return void 0;
777
837
  return {
778
- name: pair.slice(0, separator).trim(),
779
- value: pair.slice(separator + 1).trim()
838
+ use: async (context) => {
839
+ const binding = bind(context);
840
+ await loadSession(binding);
841
+ return sessionManager(binding);
842
+ },
843
+ token: (context) => sessionToken(bind(context)),
844
+ issue: async (data, issueOptions) => {
845
+ await codec.assertKeys(config);
846
+ const issue = codec.issuer(config);
847
+ const createdAt = sessionInstant({
848
+ codec,
849
+ config
850
+ });
851
+ return issue({
852
+ id: mintSessionId(config),
853
+ createdAt,
854
+ expiresAt: resolveExpiresAt(createdAt, { maxAge: issueOptions?.maxAge ?? config.maxAge }),
855
+ data: sanitizeSessionUpdate(data ?? {}, { prefix: driver.prefix }),
856
+ token: void 0
857
+ });
858
+ },
859
+ open: (token) => openSessionToken(codec, config, {
860
+ token,
861
+ args: {}
862
+ })
780
863
  };
781
864
  }
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
865
  const JWS_DEFAULT_COOKIE = {
792
866
  path: "/",
793
867
  secure: true,
@@ -805,12 +879,12 @@ function createJWSCodec(options) {
805
879
  openOptions: (config) => config.jws?.verifyOptions,
806
880
  issueKey: (config) => getSignKey(config, prefix),
807
881
  openKey: (config) => getVerifyKey(config, prefix),
808
- lookup: (config, carrier) => {
882
+ lookup: (config, args) => {
809
883
  const hooks = config.hooks;
810
884
  if (!hooks?.onVerifyKeyLookup) return void 0;
811
885
  return (header) => hooks.onVerifyKeyLookup?.({
812
886
  header,
813
- ...carrier,
887
+ ...args,
814
888
  config
815
889
  });
816
890
  },
@@ -875,4 +949,4 @@ function assertPinnedAlg(key, config, prefix) {
875
949
  hint: "\"jws.signOptions.alg\""
876
950
  });
877
951
  }
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 };
952
+ export { bindSession, createJWECodec, createJWSCodec, defineSession, dropSession, encodeSession, invalidConfigError, issueSession, loadSession, openSessionToken, sessionActions, setCookieName, setCookieValues };