unjwt 0.8.4 → 0.8.5

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 (53) hide show
  1. package/README.md +3 -9
  2. package/dist/_chunks/{_import-key.mjs → algorithms.mjs} +194 -399
  3. package/dist/_chunks/{_codec-jwe.d.mts → codec-jwe.d.mts} +40 -9
  4. package/dist/_chunks/{_codec-jws.mjs → codec-jws.mjs} +133 -80
  5. package/dist/_chunks/cookie.mjs +50 -0
  6. package/dist/_chunks/digest.mjs +10 -3
  7. package/dist/_chunks/error.d.mts +103 -0
  8. package/dist/_chunks/export.mjs +97 -0
  9. package/dist/_chunks/index.d.mts +5 -103
  10. package/dist/_chunks/index2.d.mts +206 -12
  11. package/dist/_chunks/index3.d.mts +12 -109
  12. package/dist/_chunks/{jwe.d.mts → index4.d.mts} +2 -1
  13. package/dist/_chunks/{jws.d.mts → index5.d.mts} +2 -1
  14. package/dist/_chunks/index6.d.mts +113 -0
  15. package/dist/_chunks/jwe.mjs +205 -72
  16. package/dist/{core → _chunks}/jwk.mjs +210 -495
  17. package/dist/_chunks/jws.mjs +280 -193
  18. package/dist/_chunks/jwt.d.mts +2 -1
  19. package/dist/_chunks/{_jwt-internal.mjs → jwt.mjs} +110 -54
  20. package/dist/_chunks/libs/unsecure.d.mts +35 -25
  21. package/dist/_chunks/libs/unsecure.mjs +101 -268
  22. package/dist/_chunks/{_internal.d.mts → limits.d.mts} +1 -1
  23. package/dist/_chunks/payload.mjs +23 -0
  24. package/dist/_chunks/wrap.mjs +209 -0
  25. package/dist/adapters/elysia/index.d.mts +4 -4
  26. package/dist/adapters/elysia/index.mjs +4 -3
  27. package/dist/adapters/h3/index.d.mts +13 -13
  28. package/dist/adapters/h3/index.mjs +8 -11
  29. package/dist/adapters/h3v1/index.d.mts +5 -5
  30. package/dist/adapters/h3v1/index.mjs +6 -9
  31. package/dist/core/internal.mjs +1 -1
  32. package/dist/core/{jwe.d.mts → jwe/index.d.mts} +5 -4
  33. package/dist/core/jwe/index.mjs +5 -0
  34. package/dist/core/{jwk.d.mts → jwk/index.d.mts} +5 -4
  35. package/dist/core/jwk/index.mjs +7 -0
  36. package/dist/core/{jws.d.mts → jws/index.d.mts} +5 -4
  37. package/dist/core/jws/index.mjs +5 -0
  38. package/dist/dpop/index.d.mts +73 -0
  39. package/dist/dpop/index.mjs +296 -0
  40. package/dist/error.d.mts +3 -0
  41. package/dist/error.mjs +20 -0
  42. package/dist/index.d.mts +10 -9
  43. package/dist/index.mjs +7 -4
  44. package/dist/session/index.d.mts +4 -4
  45. package/dist/session/index.mjs +6 -47
  46. package/dist/utils/index.d.mts +7 -0
  47. package/dist/{core/utils → utils}/index.mjs +4 -3
  48. package/package.json +21 -10
  49. package/dist/_chunks/jwk.d.mts +0 -205
  50. package/dist/core/jwe.mjs +0 -5
  51. package/dist/core/jws.mjs +0 -5
  52. package/dist/core/utils/index.d.mts +0 -6
  53. /package/dist/_chunks/{_cookie-attributes.d.mts → cookie-attributes.d.mts} +0 -0
@@ -1,3 +1,4 @@
1
+ import "./error.mjs";
1
2
  import { ExpiresIn, JWEAsymmetricPrivateJWK, JWEAsymmetricPublicJWK, JWEDecryptJWK, JWEDecryptOptions, JWEEncryptJWK, JWEEncryptOptions, JWEHeaderParameters, JWKSet, JWK_HMAC, JWK_oct, JWSAsymmetricPrivateJWK, JWSAsymmetricPublicJWK, JWSHeaderParameters, JWSSignOptions, JWSVerifyJWK, JWSVerifyOptions, JWTClaims, StrictOmit } from "./index.mjs";
2
3
  interface ExpirableCookieAttributes {
3
4
  expires?: Date;
@@ -14,7 +15,7 @@ export type SessionUpdate<T extends SessionRecord = SessionClaims> = Partial<Ses
14
15
  export interface SessionManager<T extends SessionRecord = SessionClaims, ConfigMaxAge extends ExpiresIn | undefined = ExpiresIn | undefined> {
15
16
  readonly id: string | undefined;
16
17
  readonly createdAt: number;
17
- /** Expiry in ms — always a number when the session has a `maxAge`. */
18
+ /** Expiry in ms: `exp`, or `iat + maxAge` when sooner; always a number when the session has a `maxAge`. */
18
19
  readonly expiresAt: ConfigMaxAge extends ExpiresIn ? number : number | undefined;
19
20
  readonly data: SessionData<T>;
20
21
  readonly token: string | undefined;
@@ -28,7 +29,7 @@ export interface SessionManager<T extends SessionRecord = SessionClaims, ConfigM
28
29
  }
29
30
  /** The config members every session shares, whether it signs or seals its token. */
30
31
  interface SessionBaseConfig<MaxAge extends ExpiresIn | undefined = ExpiresIn | undefined> {
31
- /** Sets `exp = iat + maxAge`, requires `exp` on incoming tokens, drives cookie expiry; unset never expires. */
32
+ /** Sets `exp = iat + maxAge`, requires `exp` on incoming tokens and ends them at `iat + maxAge` at the latest, drives cookie expiry; unset never expires. */
32
33
  maxAge?: MaxAge;
33
34
  /**
34
35
  * Reissue the session when it is read, needs `maxAge`.
@@ -49,6 +50,7 @@ interface SessionBaseConfig<MaxAge extends ExpiresIn | undefined = ExpiresIn | u
49
50
  name?: string;
50
51
  /**
51
52
  * Header carrying a bare or `Bearer` token: a header name, `true` for `x-<name>-session` (cookie prefix stripped), or `false` for none.
53
+ * A request that carries it is read from it and never from the cookie, and writes no cookie back; any other value is a token failure (`onError`).
52
54
  * Left unset, a session with cookies still reads `x-<name>-session`, and so does one with `cookie: false`.
53
55
  */
54
56
  sessionHeader?: boolean | string;
@@ -68,7 +70,7 @@ interface SessionHooks<TSession, TManager, TArgs extends Record<string, unknown>
68
70
  };
69
71
  config: TConfig;
70
72
  }) => void | Promise<void>;
71
- /** Fires after a token is issued and its cookie written, by `update()` or by `rolling`; a throw rejects without rollback. */
73
+ /** Fires after a token is issued and, on a cookie session, its cookie written, by `update()` or by `rolling`; a throw rejects without rollback. */
72
74
  onUpdate?: (args: TArgs & {
73
75
  session: TManager & {
74
76
  id: string;
@@ -104,7 +106,7 @@ interface SessionHooks<TSession, TManager, TArgs extends Record<string, unknown>
104
106
  }
105
107
  /** Per-issue overrides for a single `update()` call. */
106
108
  export interface SessionIssueOptions {
107
- /** Lifetime for this issue only, overriding the config's `maxAge`. */
109
+ /** Lifetime for this issue only, overriding the config's `maxAge`; a read under that `maxAge` still ends the session at `iat + maxAge`. */
108
110
  maxAge?: ExpiresIn;
109
111
  }
110
112
  /** In-request session state, mirroring the claims of the underlying token. */
@@ -113,7 +115,7 @@ export interface SessionState<T extends SessionRecord, MaxAge extends ExpiresIn
113
115
  id: string | undefined;
114
116
  /** In ms; mirrors the JWT `iat` claim (seconds). */
115
117
  createdAt: number;
116
- /** In ms; mirrors the JWT `exp` claim (seconds). */
118
+ /** In ms; mirrors the JWT `exp` claim (seconds), or `iat + maxAge` when sooner. */
117
119
  expiresAt: MaxAge extends ExpiresIn ? number : number | undefined;
118
120
  data: SessionData<T>;
119
121
  token: string | undefined;
@@ -127,7 +129,7 @@ export type OpenedSessionState<T extends SessionRecord, MaxAge extends ExpiresIn
127
129
  export interface SessionDefinition<T extends SessionRecord = SessionClaims, MaxAge extends ExpiresIn | undefined = ExpiresIn | undefined, TEvent = unknown> {
128
130
  /** Reads the request's session once and returns its manager; later calls on the same request share that state. */
129
131
  use: (event: TEvent) => Promise<SessionManager<T, MaxAge>>;
130
- /** The token the request carries (this response's own write first), unopened. */
132
+ /** The session's token once `use()` read or wrote it, else the one the request carries (the session header before the cookie), unopened. */
131
133
  token: (event: TEvent) => string | undefined;
132
134
  /** Mints a token for `data` with a fresh `jti` and `iat`, and `exp` from `maxAge` (`options.maxAge` overrides it); reads and writes nothing. */
133
135
  issue: (data?: Partial<SessionData<T>>, options?: SessionIssueOptions) => Promise<string>;
@@ -135,9 +137,13 @@ export interface SessionDefinition<T extends SessionRecord = SessionClaims, MaxA
135
137
  open: (token: string) => Promise<OpenedSessionState<T, MaxAge>>;
136
138
  }
137
139
  interface SessionRequestBase {
138
- /** The response being prepared; without it the session is read-only. */
140
+ /**
141
+ * The response being prepared; without it the session is read-only.
142
+ * With `errHeaders` too (h3 v2, which sends only those for a thrown handler), every cookie write lands in both.
143
+ */
139
144
  readonly res?: {
140
145
  readonly headers: Headers;
146
+ readonly errHeaders?: Headers;
141
147
  };
142
148
  /** Per-request state shared with the framework, where `sessions` is kept; without it state is keyed on `req`, else on this object. */
143
149
  readonly context?: Record<string, unknown>;
@@ -213,10 +219,35 @@ export interface JWSSessionHooks<TSession, TManager, TArgs extends Record<string
213
219
  config: TConfig;
214
220
  }) => JWKSet | JWSVerifyJWK | Promise<JWKSet | JWSVerifyJWK>;
215
221
  }
216
- /** Symmetric JWK, `CryptoKey` (requires pinned `alg` and `enc`), or a pair: public seals, private unseals. */
222
+ /**
223
+ * A symmetric `oct` JWK, or a `CryptoKey` (requires pinned `alg` and `enc`): the one key seals and unseals.
224
+ *
225
+ * The `{ privateKey, publicKey? }` pair members are deprecated: a pair seals with its public half,
226
+ * so anyone holding the public key can mint a session the server accepts. Use an `oct` JWK, or
227
+ * `defineJWSSession` for a key pair.
228
+ */
217
229
  type JWESessionKey = CryptoKey | JWEEncryptJWK | {
218
- privateKey: JWEAsymmetricPrivateJWK | CryptoKey;
230
+ /**
231
+ * Unseals; a private JWK alone seals with its own public half.
232
+ * @deprecated A pair seals with its public half, so anyone holding the public key can mint a session the server accepts. Use an `oct` JWK, or `defineJWSSession` for a key pair.
233
+ */
234
+ privateKey: JWEAsymmetricPrivateJWK;
235
+ /**
236
+ * Seals.
237
+ * @deprecated A pair seals with its public half, so anyone holding the public key can mint a session the server accepts. Use an `oct` JWK, or `defineJWSSession` for a key pair.
238
+ */
219
239
  publicKey?: JWEAsymmetricPublicJWK | CryptoKey;
240
+ } | {
241
+ /**
242
+ * Unseals; a private `CryptoKey` has no public half to derive, so it comes with one.
243
+ * @deprecated A pair seals with its public half, so anyone holding the public key can mint a session the server accepts. Use an `oct` JWK, or `defineJWSSession` for a key pair.
244
+ */
245
+ privateKey: CryptoKey;
246
+ /**
247
+ * Seals.
248
+ * @deprecated A pair seals with its public half, so anyone holding the public key can mint a session the server accepts. Use an `oct` JWK, or `defineJWSSession` for a key pair.
249
+ */
250
+ publicKey: JWEAsymmetricPublicJWK | CryptoKey;
220
251
  };
221
252
  /** JWE decrypt overrides, minus the options the adapter owns. */
222
253
  export type SessionDecryptOptions = StrictOmit<JWEDecryptOptions, "algorithms" | "encryptionAlgorithms" | "typ" | "maxTokenAge" | "validateClaims" | "forceUint8Array" | "payload">;
@@ -1,8 +1,10 @@
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 "./_import-key.mjs";
1
+ import { safeJsonParse } from "./libs/unsecure.mjs";
2
+ import { JWTError, isJWTError } from "../error.mjs";
3
+ import { computeDurationInSeconds, expiredClaimMessage, isCryptoKey, isJWK, isJWKSet, isPlainObject, isPrivateJWK, isPublicJWK, isSymmetricJWK, quoteTokenValue } from "./jwt.mjs";
4
+ import { importKey } from "./algorithms.mjs";
4
5
  import { sign, verify } from "./jws.mjs";
5
6
  import { decrypt, encrypt } from "./jwe.mjs";
7
+ import { toPublicJWK } from "./export.mjs";
6
8
  const RESERVED_CLAIMS = [
7
9
  "jti",
8
10
  "iat",
@@ -29,10 +31,12 @@ function readOnlyEventError(message) {
29
31
  return new JWTError(message, "ERR_SESSION_READONLY_EVENT");
30
32
  }
31
33
  function expiredSessionError(options) {
32
- const { id, expiresAt } = options;
34
+ const { id, createdAt, expiresAt } = options;
33
35
  const exp = Math.floor(expiresAt / 1e3);
36
+ const iat = createdAt === void 0 ? void 0 : Math.floor(createdAt / 1e3);
34
37
  return new JWTError(expiredClaimMessage(exp), "ERR_JWT_EXPIRED", {
35
38
  jti: id,
39
+ iat,
36
40
  exp
37
41
  });
38
42
  }
@@ -131,12 +135,17 @@ function createSessionCodec(spec) {
131
135
  const now = (config) => spec.issueOptions(config)?.currentDate?.getTime() ?? Date.now();
132
136
  const openWith = async (config, token, key) => {
133
137
  const payload = await spec.open(config, token, key).catch((error) => rethrowAsInvalidToken(error, { prefix }));
134
- assertNotFromTheFuture(payload, {
138
+ const clock = {
135
139
  now: now(config),
136
- clockTolerance: spec.openOptions(config)?.clockTolerance,
140
+ clockTolerance: spec.openOptions(config)?.clockTolerance
141
+ };
142
+ assertNotFromTheFuture(payload, {
143
+ ...clock,
137
144
  prefix
138
145
  });
139
- return openedSession(payload);
146
+ const opened = openedSession(payload, { maxAge: config.maxAge });
147
+ assertNotPastMaxAge(opened, clock);
148
+ return opened;
140
149
  };
141
150
  const assertOpenKey = async (config) => {
142
151
  if (spec.lookup(config, {})) return;
@@ -218,19 +227,33 @@ function sessionPayload(state) {
218
227
  return payload;
219
228
  }
220
229
  function assertNotFromTheFuture(payload, options) {
221
- const tolerance = options.clockTolerance === void 0 ? 0 : computeDurationInSeconds(options.clockTolerance);
222
- if (payload.iat > Math.floor(options.now / 1e3) + tolerance) throw invalidTokenError(`${options.prefix} Invalid session token: "iat" is in the future (${(/* @__PURE__ */ new Date(payload.iat * 1e3)).toISOString()}).`);
230
+ 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()}).`);
231
+ }
232
+ function assertNotPastMaxAge(opened, options) {
233
+ const { expiresAt } = opened;
234
+ if (expiresAt === void 0) return;
235
+ if (expiresAt <= options.now - toleranceSeconds(options.clockTolerance) * 1e3) throw expiredSessionError({
236
+ id: opened.id,
237
+ createdAt: opened.createdAt,
238
+ expiresAt
239
+ });
240
+ }
241
+ function toleranceSeconds(clockTolerance) {
242
+ return clockTolerance === void 0 ? 0 : computeDurationInSeconds(clockTolerance);
223
243
  }
224
- function openedSession(payload) {
244
+ function openedSession(payload, options) {
225
245
  const data = Object.create(null);
226
246
  for (const key of Object.keys(payload)) {
227
247
  if (RESERVED_CLAIMS.includes(key)) continue;
228
248
  data[key] = payload[key];
229
249
  }
250
+ const createdAt = payload.iat * 1e3;
251
+ const exp = payload.exp ? payload.exp * 1e3 : void 0;
252
+ const bound = resolveExpiresAt(createdAt, options);
230
253
  return {
231
254
  id: payload.jti,
232
- createdAt: payload.iat * 1e3,
233
- expiresAt: payload.exp ? payload.exp * 1e3 : void 0,
255
+ createdAt,
256
+ expiresAt: exp === void 0 || bound === void 0 ? exp ?? bound : Math.min(exp, bound),
234
257
  data
235
258
  };
236
259
  }
@@ -290,10 +313,7 @@ function getEncryptKey(config, prefix) {
290
313
  assertKeyMaterial(key, { prefix });
291
314
  let resolved;
292
315
  if (isCryptoKey(key) || isSymmetricJWK(key)) resolved = key;
293
- else if (isKeyPair(key)) {
294
- assertJWEKeyPair(key, prefix);
295
- resolved = key.publicKey;
296
- }
316
+ else if (isKeyPair(key)) resolved = jweKeyPair(key, prefix).publicKey;
297
317
  if (!resolved) throw invalidKeyError(`${prefix} Invalid JWE key, expected a JWK or a CryptoKey.`, key);
298
318
  assertPinnedAlgEnc(resolved, config, prefix);
299
319
  return resolved;
@@ -303,21 +323,27 @@ function getDecryptKey(config, prefix) {
303
323
  assertKeyMaterial(key, { prefix });
304
324
  let resolved;
305
325
  if (isCryptoKey(key) || isSymmetricJWK(key)) resolved = key;
306
- else if (isKeyPair(key)) {
307
- assertJWEKeyPair(key, prefix);
308
- resolved = key.privateKey;
309
- }
326
+ else if (isKeyPair(key)) resolved = jweKeyPair(key, prefix).privateKey;
310
327
  if (!resolved) throw invalidKeyError(`${prefix} Invalid JWE key, expected a JWK, a private JWK or a CryptoKey.`, key);
311
328
  assertPinnedAlgEnc(resolved, config, prefix);
312
329
  return resolved;
313
330
  }
314
- function assertJWEKeyPair(key, prefix) {
315
- assertKeyPair(key, {
331
+ function jweKeyPair(key, prefix) {
332
+ const pair = {
333
+ privateKey: key.privateKey,
334
+ publicKey: key.publicKey ?? derivedPublicHalf(key.privateKey, prefix)
335
+ };
336
+ assertKeyPair(pair, {
316
337
  prefix,
317
338
  family: "JWE",
318
339
  isPublic: (half) => isCryptoKey(half) || isPublicJWK(half),
319
340
  publicHalf: "a public JWK or a CryptoKey"
320
341
  });
342
+ return pair;
343
+ }
344
+ function derivedPublicHalf(privateKey, prefix) {
345
+ if (isPrivateJWK(privateKey)) return toPublicJWK(privateKey);
346
+ throw invalidKeyError(`${prefix} Invalid JWE key: a CryptoKey "privateKey" needs its "publicKey".`, privateKey);
321
347
  }
322
348
  function assertPinnedAlgEnc(key, config, prefix) {
323
349
  const { alg, enc } = config.jwe?.encryptOptions ?? {};
@@ -327,6 +353,43 @@ function assertPinnedAlgEnc(key, config, prefix) {
327
353
  hint: "\"jwe.encryptOptions.alg\" and \"enc\""
328
354
  });
329
355
  }
356
+ const kSessionPending = Symbol("unjwt_session_pending");
357
+ function sessionInstant(source) {
358
+ const now = source.codec.now(source.config);
359
+ return now - now % 1e3;
360
+ }
361
+ function mintSessionId(config) {
362
+ return config.generateId?.() || crypto.randomUUID();
363
+ }
364
+ function emptySession(binding) {
365
+ const createdAt = sessionInstant(binding);
366
+ return typedSession({
367
+ id: void 0,
368
+ createdAt,
369
+ expiresAt: resolveExpiresAt(createdAt, { maxAge: binding.config.maxAge }),
370
+ data: Object.create(null),
371
+ token: void 0
372
+ });
373
+ }
374
+ function storedSession(store) {
375
+ const state = store.get();
376
+ return state === void 0 ? void 0 : typedSession(state);
377
+ }
378
+ function blankSession() {
379
+ return typedSession({
380
+ expiresAt: void 0,
381
+ data: {}
382
+ });
383
+ }
384
+ function openSessionToken(codec, config, options) {
385
+ return codec.open(config, options.token, options.args).then(typedOpened);
386
+ }
387
+ function typedSession(state) {
388
+ return state;
389
+ }
390
+ function typedOpened(opened) {
391
+ return opened;
392
+ }
330
393
  const CHUNK_MARKER = "__chunked__";
331
394
  const DEFAULT_CHUNK_MAX_LENGTH = 4e3;
332
395
  function readChunked(store, name) {
@@ -374,6 +437,13 @@ function setCookieValues(setCookies) {
374
437
  function setCookieName(setCookie) {
375
438
  return setCookiePair(setCookie)?.name;
376
439
  }
440
+ function mirrorSetCookie(name, headers) {
441
+ const { from, to } = headers;
442
+ const isThisCookie = (line) => setCookieName(line) === name;
443
+ const lines = [...to.getSetCookie().filter((line) => !isThisCookie(line)), ...from.getSetCookie().filter((line) => isThisCookie(line))];
444
+ to.delete("set-cookie");
445
+ for (const line of lines) to.append("set-cookie", line);
446
+ }
377
447
  function chunkName(name, index) {
378
448
  return `${name}.${index}`;
379
449
  }
@@ -412,25 +482,25 @@ function decodeValue(value) {
412
482
  function resolveTransport(driver, event, options) {
413
483
  const handed = driver.handed?.(event);
414
484
  if (handed) return {
485
+ source: "handed",
415
486
  read: () => handed.token,
416
487
  readonly: false,
417
488
  writable: () => true
418
489
  };
419
490
  const { config, name } = options;
420
- const header = headerReader(driver, event, options);
421
- if (config.cookie === false) return {
422
- read: header,
491
+ const header = presentedHeader(driver, event, options);
492
+ if (header || config.cookie === false) return {
493
+ source: "header",
494
+ read: config.sessionHeader === false ? void 0 : () => header?.token,
495
+ malformed: header?.malformed,
423
496
  readonly: false,
424
497
  writable: () => true
425
498
  };
426
499
  const jar = driver.cookies(event);
427
500
  const store = responseStore(jar);
428
501
  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
- },
502
+ source: "cookie",
503
+ read: () => cookieToken(jar, name),
434
504
  readonly: !store,
435
505
  writable: () => driver.writable?.(event) ?? true
436
506
  };
@@ -471,11 +541,23 @@ function headerToken(value) {
471
541
  if (/^bearer$/i.test(first)) return words.length === 2 ? second : void 0;
472
542
  return words.length === 1 && first ? first : void 0;
473
543
  }
474
- function headerReader(driver, event, options) {
544
+ function presentedHeader(driver, event, options) {
475
545
  const { config, name } = options;
476
546
  if (config.sessionHeader === false) return void 0;
477
547
  const header = sessionHeaderName(name, config.sessionHeader);
478
- return () => headerToken(driver.header(event, header));
548
+ const value = driver.header(event, header);
549
+ if (typeof value !== "string") return void 0;
550
+ const token = headerToken(value);
551
+ if (token) return { token };
552
+ return {
553
+ token: void 0,
554
+ malformed: invalidTokenError(`${driver.prefix} Invalid session token: the "${header}" header holds neither a token nor "Bearer <token>".`)
555
+ };
556
+ }
557
+ function cookieToken(jar, name) {
558
+ const written = jar.written ? readChunked({ read: (key) => jar.written?.(key) }, name) : void 0;
559
+ if (written === "") return void 0;
560
+ return (written ?? readChunked(jar, name)) || void 0;
479
561
  }
480
562
  function responseStore(jar) {
481
563
  const write = jar.write?.bind(jar);
@@ -521,7 +603,8 @@ function bindSession(event, config, options) {
521
603
  function sessionToken(binding) {
522
604
  const { transport } = binding;
523
605
  assertTokenSource(transport, { prefix: binding.driver.prefix });
524
- return transport.read();
606
+ const session = storedSession(binding.store);
607
+ return session ? session.token : transport.read();
525
608
  }
526
609
  function recordSlotStore(record, slot) {
527
610
  return {
@@ -563,43 +646,6 @@ function claimSessionName(record, options) {
563
646
  if (held === family) return;
564
647
  throw invalidConfigError(`${prefix} Session name "${name}" is already used by a ${held.toUpperCase()} session on this request.`);
565
648
  }
566
- const kSessionPending = Symbol("unjwt_session_pending");
567
- function sessionInstant(source) {
568
- const now = source.codec.now(source.config);
569
- return now - now % 1e3;
570
- }
571
- function mintSessionId(config) {
572
- return config.generateId?.() || crypto.randomUUID();
573
- }
574
- function emptySession(binding) {
575
- const createdAt = sessionInstant(binding);
576
- return typedSession({
577
- id: void 0,
578
- createdAt,
579
- expiresAt: resolveExpiresAt(createdAt, { maxAge: binding.config.maxAge }),
580
- data: Object.create(null),
581
- token: void 0
582
- });
583
- }
584
- function storedSession(store) {
585
- const state = store.get();
586
- return state === void 0 ? void 0 : typedSession(state);
587
- }
588
- function blankSession() {
589
- return typedSession({
590
- expiresAt: void 0,
591
- data: {}
592
- });
593
- }
594
- function openSessionToken(codec, config, options) {
595
- return codec.open(config, options.token, options.args).then(typedOpened);
596
- }
597
- function typedSession(state) {
598
- return state;
599
- }
600
- function typedOpened(opened) {
601
- return opened;
602
- }
603
649
  async function loadSession(binding) {
604
650
  const { config, codec, store, args, transport } = binding;
605
651
  await codec.assertKeys(config);
@@ -621,6 +667,7 @@ async function loadSession(binding) {
621
667
  },
622
668
  error: expiredSessionError({
623
669
  id: session.id,
670
+ createdAt: session.createdAt,
624
671
  expiresAt: session.expiresAt
625
672
  })
626
673
  });
@@ -630,6 +677,10 @@ async function loadSession(binding) {
630
677
  }
631
678
  const session = emptySession(binding);
632
679
  store.set(session);
680
+ if (transport.malformed) {
681
+ await fireError(binding, transport.malformed);
682
+ return session;
683
+ }
633
684
  const token = transport.read();
634
685
  if (!token) return session;
635
686
  session.token = token;
@@ -660,12 +711,7 @@ async function loadSession(binding) {
660
711
  });
661
712
  } else {
662
713
  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
- });
714
+ await fireError(binding, failure.error);
669
715
  }
670
716
  return session;
671
717
  }
@@ -692,6 +738,14 @@ async function rollSession(binding, session) {
692
738
  if (transport.readonly || !transport.writable()) return;
693
739
  await binding.actions.roll(binding);
694
740
  }
741
+ async function fireError(binding, error) {
742
+ await binding.config.hooks?.onError?.({
743
+ session: binding.actions.manager(binding),
744
+ ...binding.args,
745
+ error,
746
+ config: binding.config
747
+ });
748
+ }
695
749
  async function fireExpire(binding, expired) {
696
750
  await binding.config.hooks?.onExpire?.({
697
751
  session: expired.session,
@@ -797,8 +851,7 @@ function sessionManager(binding) {
797
851
  return (state() ?? blankSession()).data;
798
852
  },
799
853
  get token() {
800
- const session = state();
801
- return session ? session.token : sessionToken(binding);
854
+ return sessionToken(binding);
802
855
  },
803
856
  update: async (update, options) => {
804
857
  await issueSession(binding, update, options);
@@ -822,7 +875,7 @@ function sessionActions() {
822
875
  function snapshotSession(session) {
823
876
  return {
824
877
  ...session,
825
- data: sanitizeObjectCopy(session.data)
878
+ data: safeJsonParse(JSON.stringify(session.data))
826
879
  };
827
880
  }
828
881
  function defineSession(config, options) {
@@ -949,4 +1002,4 @@ function assertPinnedAlg(key, config, prefix) {
949
1002
  hint: "\"jws.signOptions.alg\""
950
1003
  });
951
1004
  }
952
- export { bindSession, createJWECodec, createJWSCodec, defineSession, dropSession, encodeSession, invalidConfigError, issueSession, loadSession, openSessionToken, sessionActions, setCookieName, setCookieValues };
1005
+ export { bindSession, createJWECodec, createJWSCodec, defineSession, dropSession, encodeSession, invalidConfigError, issueSession, loadSession, mirrorSetCookie, openSessionToken, sessionActions, setCookieName, setCookieValues };
@@ -0,0 +1,50 @@
1
+ import { mirrorSetCookie, setCookieName, setCookieValues } from "./codec-jws.mjs";
2
+ import { parse, parseSetCookie, serialize } from "./libs/cookie-es.mjs";
3
+ function webCookieJar(request, response) {
4
+ let cookies;
5
+ const jar = { read: (name) => (cookies ||= requestCookies(request))[name] };
6
+ if (response) {
7
+ const { headers, errHeaders } = response;
8
+ jar.written = (name) => setCookieValues(headers.getSetCookie())[name];
9
+ jar.write = (name, value, attrs) => {
10
+ setCookie(headers, {
11
+ name,
12
+ value,
13
+ ...attrs
14
+ });
15
+ if (errHeaders) mirrorSetCookie(name, {
16
+ from: headers,
17
+ to: errHeaders
18
+ });
19
+ };
20
+ }
21
+ return jar;
22
+ }
23
+ function requestCookies(request) {
24
+ const header = request.get("cookie");
25
+ return header ? parse(header) : Object.create(null);
26
+ }
27
+ function setCookie(headers, cookie) {
28
+ const line = serialize({
29
+ path: "/",
30
+ ...cookie
31
+ });
32
+ const key = cookieKey(cookie);
33
+ const kept = headers.getSetCookie().filter((existing) => {
34
+ const parsed = parseSetCookie(existing);
35
+ return parsed ? cookieKey(parsed) !== key : setCookieName(existing) !== cookie.name;
36
+ });
37
+ headers.delete("set-cookie");
38
+ for (const existing of kept) headers.append("set-cookie", existing);
39
+ headers.append("set-cookie", line);
40
+ }
41
+ function cookieKey(cookie) {
42
+ const domain = (cookie.domain || "").replace(/^\./, "").toLowerCase();
43
+ return [
44
+ cookie.name,
45
+ domain,
46
+ cookie.path || "/",
47
+ cookie.partitioned ? "1" : "0"
48
+ ].join(";");
49
+ }
50
+ export { requestCookies, webCookieJar };
@@ -1,5 +1,6 @@
1
1
  import { textEncoder } from "./libs/unsecure.mjs";
2
- import { JWTError, base64UrlStringify, isCryptoKey, isCryptoKeyPair, isJWK, isJWKSet } from "./_jwt-internal.mjs";
2
+ import { JWTError } from "../error.mjs";
3
+ import { base64UrlStringify, hasSymmetricSecret, isCryptoKey, isCryptoKeyPair, isJWK, isJWKSet } from "./jwt.mjs";
3
4
  const ECDSA_CURVES = /* @__PURE__ */ Object.freeze({
4
5
  ES256: "P-256",
5
6
  ES384: "P-384",
@@ -149,7 +150,7 @@ function unionFromJWKSet(keys, mapper, options) {
149
150
  const algs = /* @__PURE__ */ new Set();
150
151
  for (const k of keys) {
151
152
  if (!isJWK(k)) continue;
152
- if (k.kty === "oct" && !options?.allowSymmetric) continue;
153
+ if (!options?.allowSymmetric && hasSymmetricSecret(k)) continue;
153
154
  const mapped = mapper(k);
154
155
  if (!mapped) continue;
155
156
  for (const a of mapped) algs.add(a);
@@ -159,6 +160,12 @@ function unionFromJWKSet(keys, mapper, options) {
159
160
  function isEd25519Signature(alg) {
160
161
  return alg === "EdDSA" || alg === "Ed25519";
161
162
  }
163
+ function isSameAlgorithm(a, b) {
164
+ return a === b || isEd25519Signature(a) && isEd25519Signature(b);
165
+ }
166
+ function jwkAlgServes(jwk, alg) {
167
+ return isSameAlgorithm(jwk.alg, alg) || jwk.kty === "oct" && alg === "dir" && isContentEncryptionAlgorithm(jwk.alg);
168
+ }
162
169
  function jwsAlgsFromJWK(jwk) {
163
170
  if (jwk.alg && isEd25519Signature(jwk.alg)) return ["EdDSA", "Ed25519"];
164
171
  if (jwk.alg) return isJWSAlgorithm(jwk.alg) ? [jwk.alg] : void 0;
@@ -253,4 +260,4 @@ async function digestUrl(data, hash = "SHA-256") {
253
260
  const bytes = typeof data === "string" ? textEncoder.encode(data) : data;
254
261
  return base64UrlStringify(new Uint8Array(await crypto.subtle.digest(hash, bytes)));
255
262
  }
256
- export { ECDSA_CURVES, PBES2_ALGORITHMS, aesBitsOf, checkKeyUsage, cryptoKeyCurve, digestUrl, hashBitsOf, inferJWEAllowedAlgorithms, inferJWSAllowedAlgorithms, innerKeyWrapAlg, isAlgorithm, isContentEncryptionAlgorithm, isECDHESAlgorithm, isEd25519Signature, isJWSAlgorithm, isKeyManagementAlgorithm, isPBES2Algorithm, jwsHashOf, lengthOf, modulusLengthOf, namedCurveOf, pbes2HashOf, rsaOaepHashOf, shaBitsOf, unusableKey };
263
+ export { ECDSA_CURVES, PBES2_ALGORITHMS, aesBitsOf, checkKeyUsage, cryptoKeyCurve, digestUrl, hashBitsOf, inferJWEAllowedAlgorithms, inferJWSAllowedAlgorithms, innerKeyWrapAlg, isAlgorithm, isContentEncryptionAlgorithm, isECDHESAlgorithm, isJWSAlgorithm, isKeyManagementAlgorithm, isPBES2Algorithm, isSameAlgorithm, jwkAlgServes, jwsHashOf, lengthOf, modulusLengthOf, namedCurveOf, pbes2HashOf, rsaOaepHashOf, shaBitsOf, unusableKey };