unjwt 0.8.3 → 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 (54) hide show
  1. package/README.md +3 -9
  2. package/dist/_chunks/algorithms.mjs +1541 -0
  3. package/dist/_chunks/{_codec-jwe.d.mts → codec-jwe.d.mts} +40 -9
  4. package/dist/_chunks/{_codec-jws.mjs → codec-jws.mjs} +148 -95
  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} +3 -1
  13. package/dist/_chunks/{jws.d.mts → index5.d.mts} +6 -2
  14. package/dist/_chunks/index6.d.mts +113 -0
  15. package/dist/_chunks/jwe.mjs +208 -74
  16. package/dist/_chunks/jwk.mjs +214 -2237
  17. package/dist/_chunks/jws.mjs +289 -202
  18. package/dist/_chunks/jwt.d.mts +2 -1
  19. package/dist/_chunks/{_jwt-internal.mjs → jwt.mjs} +124 -68
  20. package/dist/_chunks/libs/unsecure.d.mts +35 -25
  21. package/dist/_chunks/libs/unsecure.mjs +148 -302
  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 +5 -3
  27. package/dist/adapters/h3/index.d.mts +13 -13
  28. package/dist/adapters/h3/index.mjs +8 -10
  29. package/dist/adapters/h3v1/index.d.mts +5 -5
  30. package/dist/adapters/h3v1/index.mjs +6 -8
  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/index.d.mts +6 -0
  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 -3
  44. package/dist/session/index.d.mts +4 -4
  45. package/dist/session/index.mjs +6 -46
  46. package/dist/utils/index.d.mts +7 -0
  47. package/dist/{core/utils → utils}/index.mjs +4 -3
  48. package/package.json +22 -10
  49. package/dist/core/jwe.mjs +0 -5
  50. package/dist/core/jwk.d.mts +0 -207
  51. package/dist/core/jwk.mjs +0 -4
  52. package/dist/core/jws.mjs +0 -5
  53. package/dist/core/utils/index.d.mts +0 -6
  54. /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 "./jwk.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
  }
@@ -46,8 +50,8 @@ function rethrowAsInvalidToken(error, options) {
46
50
  const TOKEN = /^[!#$%&'*+\-.^_`|~\w]+$/;
47
51
  const CHUNK_SUFFIX = /\.\d+$/;
48
52
  function assertSessionName(name, options) {
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)}.`);
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, ""))}.`);
53
+ if (!isToken(name)) throw invalidConfigError(`${options.prefix} Session "name" must be a valid cookie name (got ${quoteTokenValue(name)}).`);
54
+ if (CHUNK_SUFFIX.test(name)) throw invalidConfigError(`${options.prefix} Session "name" ${quoteTokenValue(name)} cannot end in a dot and digits.`);
51
55
  }
52
56
  function isToken(value) {
53
57
  return typeof value === "string" && TOKEN.test(value);
@@ -57,7 +61,7 @@ function assertCookiePrefix(name, attrs, options) {
57
61
  const host = lower.startsWith("__host-");
58
62
  if (!host && !lower.startsWith("__secure-")) return;
59
63
  const rule = host ? "\"__Host-\" cookie must be Secure, with Path=/ and no Domain" : "\"__Secure-\" cookie must be Secure";
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).`);
64
+ if (attrs.secure === false || host && (attrs.domain !== void 0 || attrs.path !== void 0 && attrs.path !== "/")) throw invalidConfigError(`${options.prefix} Session ${quoteTokenValue(name)}: a ${rule}.`);
61
65
  }
62
66
  function assertRolling(config, options) {
63
67
  const { rolling } = config;
@@ -65,10 +69,10 @@ function assertRolling(config, options) {
65
69
  if (config.maxAge === void 0) throw invalidConfigError(`${options.prefix} "rolling" needs a "maxAge" to extend.`);
66
70
  if (rolling === true) return;
67
71
  if (!isExpiresIn(rolling)) throw new JWTError(`${options.prefix} "rolling" must be true or a duration.`, "ERR_INVALID_OPTION");
68
- if (computeDurationInSeconds(rolling) >= computeDurationInSeconds(config.maxAge)) throw invalidConfigError(`${options.prefix} "rolling" must be shorter than "maxAge": the session would expire before it rolls.`);
72
+ if (computeDurationInSeconds(rolling) >= computeDurationInSeconds(config.maxAge)) throw invalidConfigError(`${options.prefix} "rolling" must be shorter than "maxAge".`);
69
73
  }
70
74
  function assertKeyMaterial(key, options) {
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.`);
75
+ if (typeof key === "string") throw invalidKeyError(`${options.prefix} Session "key" cannot be a string, use deriveJWKFromPassword().`);
72
76
  if (!key) throw invalidKeyError(`${options.prefix} Session "key" is required.`);
73
77
  }
74
78
  async function assertImportableKey(key, options) {
@@ -109,7 +113,7 @@ function sessionRequiredClaims(configured, options) {
109
113
  return [...required];
110
114
  }
111
115
  function sanitizeSessionUpdate(update, options) {
112
- 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");
116
+ if (!isPlainObject(update)) throw new JWTError(`${options.prefix} Session data must be a plain object.`, "ERR_INVALID_OPTION");
113
117
  const sanitized = Object.create(null);
114
118
  for (const key of Object.keys(update)) {
115
119
  if (FORBIDDEN_KEYS.includes(key)) throw new JWTError(`${options.prefix} Session data cannot carry the key "${key}".`, "ERR_INVALID_OPTION");
@@ -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;
@@ -205,7 +214,7 @@ function assertKeyPair(pair, options) {
205
214
  if (!options.isPublic(pair.publicKey)) throw invalidKeyError(`${prefix} Invalid ${family} key: "publicKey" must be ${options.publicHalf}.`, pair);
206
215
  }
207
216
  function assertPinnedCryptoKey(key, options) {
208
- if (isCryptoKey(key) && !options.pinned) throw invalidConfigError(`${options.prefix} A CryptoKey session key carries no algorithm: set ${options.hint}.`);
217
+ if (isCryptoKey(key) && !options.pinned) throw invalidConfigError(`${options.prefix} A CryptoKey session key carries no algorithm, set ${options.hint}.`);
209
218
  }
210
219
  function sessionPayload(state) {
211
220
  const iat = Math.floor(state.createdAt / 1e3);
@@ -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: issued in the future (iat: ${(/* @__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,11 +313,8 @@ 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
- }
297
- if (!resolved) throw invalidKeyError(`${prefix} Invalid JWE key. It must be a JWK or a CryptoKey.`, key);
316
+ else if (isKeyPair(key)) resolved = jweKeyPair(key, prefix).publicKey;
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;
300
320
  }
@@ -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
- }
310
- if (!resolved) throw invalidKeyError(`${prefix} Invalid JWE key. It must be a JWK, a private JWK, or a CryptoKey.`, key);
326
+ else if (isKeyPair(key)) resolved = jweKeyPair(key, prefix).privateKey;
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
  };
@@ -448,7 +518,7 @@ function resolveTransport(driver, event, options) {
448
518
  return transport;
449
519
  }
450
520
  function assertTokenSource(transport, options) {
451
- if (!transport.read) throw invalidConfigError(`${options.prefix} Session has no token source: "cookie" and "sessionHeader" are both disabled.`);
521
+ if (!transport.read) throw invalidConfigError(`${options.prefix} Session has no token source, enable "cookie" or "sessionHeader".`);
452
522
  }
453
523
  function assertWritable(transport, options) {
454
524
  const { operation, prefix } = options;
@@ -458,7 +528,7 @@ function assertWritable(transport, options) {
458
528
  function assertSessionHeader(header, options) {
459
529
  if (header === void 0 || typeof header === "boolean") return;
460
530
  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)}.`);
531
+ throw invalidConfigError(`${options.prefix} Session "sessionHeader" must be true, false or a header name (got ${quoteTokenValue(header)}).`);
462
532
  }
463
533
  function sessionHeaderName(sessionName, configured) {
464
534
  if (typeof configured === "string") return configured.toLowerCase();
@@ -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 {
@@ -561,44 +644,7 @@ function claimSessionName(record, options) {
561
644
  return;
562
645
  }
563
646
  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);
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;
647
+ throw invalidConfigError(`${prefix} Session name "${name}" is already used by a ${held.toUpperCase()} session on this request.`);
602
648
  }
603
649
  async function loadSession(binding) {
604
650
  const { config, codec, store, args, transport } = binding;
@@ -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) {
@@ -914,7 +967,7 @@ function getSignKey(config, prefix) {
914
967
  assertJWSKeyPair(key, prefix);
915
968
  resolved = key.privateKey;
916
969
  }
917
- if (!resolved) throw invalidKeyError(`${prefix} Invalid JWS key. It must be a symmetric JWK, a private JWK, or a CryptoKey.`, key);
970
+ if (!resolved) throw invalidKeyError(`${prefix} Invalid JWS key, expected a symmetric JWK, a private JWK or a CryptoKey.`, key);
918
971
  assertPinnedAlg(resolved, config, prefix);
919
972
  return resolved;
920
973
  }
@@ -927,7 +980,7 @@ function getVerifyKey(config, prefix) {
927
980
  assertJWSKeyPair(key, prefix);
928
981
  resolved = Array.isArray(key.publicKey) ? { keys: key.publicKey } : key.publicKey;
929
982
  }
930
- if (!resolved) throw invalidKeyError(`${prefix} Invalid JWS key. It must be a symmetric JWK, a public JWK/set, or a CryptoKey.`, key);
983
+ if (!resolved) throw invalidKeyError(`${prefix} Invalid JWS key, expected a symmetric JWK, a public JWK/set or a CryptoKey.`, key);
931
984
  assertPinnedAlg(resolved, config, prefix);
932
985
  return resolved;
933
986
  }
@@ -936,7 +989,7 @@ function assertJWSKeyPair(key, prefix) {
936
989
  prefix,
937
990
  family: "JWS",
938
991
  isPublic: (half) => isCryptoKey(half) || isPublicJWK(half) || isPublicKeyList(Array.isArray(half) ? half : isJWKSet(half) ? half.keys : void 0),
939
- publicHalf: "a public JWK, a non-empty list or set of public JWKs, or a CryptoKey"
992
+ publicHalf: "a public JWK, a non-empty JWK set or a CryptoKey"
940
993
  });
941
994
  }
942
995
  function isPublicKeyList(keys) {
@@ -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 };