unjwt 0.8.2 → 0.8.4

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.
@@ -39,7 +39,7 @@ interface SessionBaseConfig<MaxAge extends ExpiresIn | undefined = ExpiresIn | u
39
39
  * first 15 minutes change nothing; the first read after that issues a fresh hour. Must be
40
40
  * shorter than `maxAge`.
41
41
  *
42
- * A read that cannot write (a read-only context, a response already sent) does not reissue;
42
+ * A read that cannot write (a read-only event, a response already sent) does not reissue;
43
43
  * the next request, carrying the same token, does.
44
44
  *
45
45
  * @default undefined — the session expires at `iat + maxAge` however often it is read.
@@ -124,63 +124,59 @@ export type OpenedSessionState<T extends SessionRecord, MaxAge extends ExpiresIn
124
124
  createdAt: number;
125
125
  };
126
126
  /** One session, defined once from its config and used on any request. */
127
- export interface SessionDefinition<T extends SessionRecord = SessionClaims, MaxAge extends ExpiresIn | undefined = ExpiresIn | undefined, TContext = unknown> {
127
+ export interface SessionDefinition<T extends SessionRecord = SessionClaims, MaxAge extends ExpiresIn | undefined = ExpiresIn | undefined, TEvent = unknown> {
128
128
  /** Reads the request's session once and returns its manager; later calls on the same request share that state. */
129
- use: (context: TContext) => Promise<SessionManager<T, MaxAge>>;
129
+ use: (event: TEvent) => Promise<SessionManager<T, MaxAge>>;
130
130
  /** The token the request carries (this response's own write first), unopened. */
131
- token: (context: TContext) => string | undefined;
131
+ token: (event: TEvent) => string | undefined;
132
132
  /** Mints a token for `data` with a fresh `jti` and `iat`, and `exp` from `maxAge` (`options.maxAge` overrides it); reads and writes nothing. */
133
133
  issue: (data?: Partial<SessionData<T>>, options?: SessionIssueOptions) => Promise<string>;
134
134
  /** Opens a token into session state, checking `typ`, pinned algorithms, `jti`/`iat` and `exp` when `maxAge` is set; the key-lookup hook receives no request. */
135
135
  open: (token: string) => Promise<OpenedSessionState<T, MaxAge>>;
136
136
  }
137
- /** A web-standard request event: reads, and writes while `res` is there. */
138
- export interface SessionRequestEvent {
139
- readonly req: Request;
137
+ interface SessionRequestBase {
140
138
  /** The response being prepared; without it the session is read-only. */
141
139
  readonly res?: {
142
140
  readonly headers: Headers;
143
141
  };
144
- /** The framework's per-request state, where `sessions` is kept; without it state is keyed on `req`. */
142
+ /** Per-request state shared with the framework, where `sessions` is kept; without it state is keyed on `req`, else on this object. */
145
143
  readonly context?: Record<string, unknown>;
146
144
  }
147
- /** A request by its headers: a `Request` itself, a WebSocket upgrade's request, a peer's `request` with its `context`. */
148
- export interface SessionRequestHeaders {
145
+ /** A request event: by its `Request`, or by its `Headers` (a `Request` itself, a WebSocket upgrade's request). */
146
+ export type SessionRequestEvent = SessionRequestBase & ({
147
+ readonly req: Request;
148
+ readonly headers?: Headers;
149
+ } | {
149
150
  readonly headers: Headers;
150
- /** The response being prepared, such as the headers a WebSocket `upgrade` returns; without it the session is read-only. */
151
- readonly res?: {
152
- readonly headers: Headers;
153
- };
154
- /** Per-request state shared with the framework; without it state is keyed on this object. */
155
- readonly context?: Record<string, unknown>;
156
- }
151
+ readonly req?: Request;
152
+ });
157
153
  /** A token handed in directly; sending a re-issued one is the caller's job. */
158
- export interface SessionToken {
154
+ export interface SessionHandedToken {
159
155
  token: string | undefined;
160
156
  }
161
- /** What `use()` takes: a request event, a request by its headers, or a token in hand. */
162
- export type SessionContext = SessionRequestEvent | SessionRequestHeaders | SessionToken;
157
+ /** What the Web session driver takes: a request event, or a token in hand. */
158
+ export type WebSessionEvent = SessionRequestEvent | SessionHandedToken;
163
159
  /** The cookies of one request and its response, as a framework exposes them. */
164
160
  export interface CookieJar<A extends ExpirableCookieAttributes = ExpirableCookieAttributes> {
165
161
  /** The request's cookie. */
166
162
  read(name: string): string | undefined;
167
163
  /** The value this response already assigned to `name` (`""` once deleted), when the framework exposes it. */
168
164
  written?(name: string): string | undefined;
169
- /** Absent when this context cannot take a `Set-Cookie` (a headers-only event). */
165
+ /** Absent when this event cannot take a `Set-Cookie` (a request without a response). */
170
166
  write?(name: string, value: string, attrs: A): void;
171
167
  }
172
168
  /** The primitives a framework supplies; the session core builds the cookie and header transports over them. */
173
- export interface SessionDriver<TContext> {
169
+ export interface SessionDriver<TEvent> {
174
170
  /** Message prefix for every error the adapter emits. */
175
171
  prefix: string;
176
172
  /** The per-request record session state and the session-name registry live in. */
177
- state(context: TContext): Record<string, unknown>;
178
- header(context: TContext, name: string): string | null | undefined;
179
- cookies(context: TContext): CookieJar;
173
+ state(event: TEvent): Record<string, unknown>;
174
+ header(event: TEvent, name: string): string | null | undefined;
175
+ cookies(event: TEvent): CookieJar;
180
176
  /** Platform signal that the response can still change (Node `headersSent`); absent = trust the jar. */
181
- writable?(context: TContext): boolean;
182
- /** A token the context carries directly, when it does: no cookie or header is consulted. */
183
- handed?(context: TContext): {
177
+ writable?(event: TEvent): boolean;
178
+ /** A token the event carries directly, when it does: no cookie or header is consulted. */
179
+ handed?(event: TEvent): {
184
180
  token: string | undefined;
185
181
  } | undefined;
186
182
  }
@@ -1,6 +1,6 @@
1
1
  import { sanitizeObjectCopy } from "./libs/unsecure.mjs";
2
2
  import { JWTError, computeDurationInSeconds, expiredClaimMessage, isCryptoKey, isJWK, isJWKSet, isJWTError, isPlainObject, isPrivateJWK, isPublicJWK, isSymmetricJWK } from "./_jwt-internal.mjs";
3
- import { importKey, quoteTokenValue } from "./jwk.mjs";
3
+ import { importKey, quoteTokenValue } from "./_import-key.mjs";
4
4
  import { sign, verify } from "./jws.mjs";
5
5
  import { decrypt, encrypt } from "./jwe.mjs";
6
6
  const RESERVED_CLAIMS = [
@@ -46,8 +46,8 @@ function rethrowAsInvalidToken(error, options) {
46
46
  const TOKEN = /^[!#$%&'*+\-.^_`|~\w]+$/;
47
47
  const CHUNK_SUFFIX = /\.\d+$/;
48
48
  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, ""))}.`);
49
+ if (!isToken(name)) throw invalidConfigError(`${options.prefix} Session "name" must be a valid cookie name (got ${quoteTokenValue(name)}).`);
50
+ if (CHUNK_SUFFIX.test(name)) throw invalidConfigError(`${options.prefix} Session "name" ${quoteTokenValue(name)} cannot end in a dot and digits.`);
51
51
  }
52
52
  function isToken(value) {
53
53
  return typeof value === "string" && TOKEN.test(value);
@@ -57,7 +57,7 @@ function assertCookiePrefix(name, attrs, options) {
57
57
  const host = lower.startsWith("__host-");
58
58
  if (!host && !lower.startsWith("__secure-")) return;
59
59
  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).`);
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}.`);
61
61
  }
62
62
  function assertRolling(config, options) {
63
63
  const { rolling } = config;
@@ -65,10 +65,10 @@ function assertRolling(config, options) {
65
65
  if (config.maxAge === void 0) throw invalidConfigError(`${options.prefix} "rolling" needs a "maxAge" to extend.`);
66
66
  if (rolling === true) return;
67
67
  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.`);
68
+ if (computeDurationInSeconds(rolling) >= computeDurationInSeconds(config.maxAge)) throw invalidConfigError(`${options.prefix} "rolling" must be shorter than "maxAge".`);
69
69
  }
70
70
  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.`);
71
+ if (typeof key === "string") throw invalidKeyError(`${options.prefix} Session "key" cannot be a string, use deriveJWKFromPassword().`);
72
72
  if (!key) throw invalidKeyError(`${options.prefix} Session "key" is required.`);
73
73
  }
74
74
  async function assertImportableKey(key, options) {
@@ -109,7 +109,7 @@ function sessionRequiredClaims(configured, options) {
109
109
  return [...required];
110
110
  }
111
111
  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");
112
+ if (!isPlainObject(update)) throw new JWTError(`${options.prefix} Session data must be a plain object.`, "ERR_INVALID_OPTION");
113
113
  const sanitized = Object.create(null);
114
114
  for (const key of Object.keys(update)) {
115
115
  if (FORBIDDEN_KEYS.includes(key)) throw new JWTError(`${options.prefix} Session data cannot carry the key "${key}".`, "ERR_INVALID_OPTION");
@@ -205,7 +205,7 @@ function assertKeyPair(pair, options) {
205
205
  if (!options.isPublic(pair.publicKey)) throw invalidKeyError(`${prefix} Invalid ${family} key: "publicKey" must be ${options.publicHalf}.`, pair);
206
206
  }
207
207
  function assertPinnedCryptoKey(key, options) {
208
- if (isCryptoKey(key) && !options.pinned) throw invalidConfigError(`${options.prefix} A CryptoKey session key carries no algorithm: set ${options.hint}.`);
208
+ if (isCryptoKey(key) && !options.pinned) throw invalidConfigError(`${options.prefix} A CryptoKey session key carries no algorithm, set ${options.hint}.`);
209
209
  }
210
210
  function sessionPayload(state) {
211
211
  const iat = Math.floor(state.createdAt / 1e3);
@@ -219,7 +219,7 @@ function sessionPayload(state) {
219
219
  }
220
220
  function assertNotFromTheFuture(payload, options) {
221
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()}).`);
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()}).`);
223
223
  }
224
224
  function openedSession(payload) {
225
225
  const data = Object.create(null);
@@ -294,7 +294,7 @@ function getEncryptKey(config, prefix) {
294
294
  assertJWEKeyPair(key, prefix);
295
295
  resolved = key.publicKey;
296
296
  }
297
- if (!resolved) throw invalidKeyError(`${prefix} Invalid JWE key. It must be a JWK or a CryptoKey.`, key);
297
+ if (!resolved) throw invalidKeyError(`${prefix} Invalid JWE key, expected a JWK or a CryptoKey.`, key);
298
298
  assertPinnedAlgEnc(resolved, config, prefix);
299
299
  return resolved;
300
300
  }
@@ -307,7 +307,7 @@ function getDecryptKey(config, prefix) {
307
307
  assertJWEKeyPair(key, prefix);
308
308
  resolved = key.privateKey;
309
309
  }
310
- if (!resolved) throw invalidKeyError(`${prefix} Invalid JWE key. It must be a JWK, a private JWK, or a CryptoKey.`, key);
310
+ if (!resolved) throw invalidKeyError(`${prefix} Invalid JWE key, expected a JWK, a private JWK or a CryptoKey.`, key);
311
311
  assertPinnedAlgEnc(resolved, config, prefix);
312
312
  return resolved;
313
313
  }
@@ -409,21 +409,21 @@ function decodeValue(value) {
409
409
  return value;
410
410
  }
411
411
  }
412
- function resolveTransport(driver, context, options) {
413
- const handed = driver.handed?.(context);
412
+ function resolveTransport(driver, event, options) {
413
+ const handed = driver.handed?.(event);
414
414
  if (handed) return {
415
415
  read: () => handed.token,
416
416
  readonly: false,
417
417
  writable: () => true
418
418
  };
419
419
  const { config, name } = options;
420
- const header = headerReader(driver, context, options);
420
+ const header = headerReader(driver, event, options);
421
421
  if (config.cookie === false) return {
422
422
  read: header,
423
423
  readonly: false,
424
424
  writable: () => true
425
425
  };
426
- const jar = driver.cookies(context);
426
+ const jar = driver.cookies(event);
427
427
  const store = responseStore(jar);
428
428
  const transport = {
429
429
  read: () => {
@@ -432,7 +432,7 @@ function resolveTransport(driver, context, options) {
432
432
  return (written ?? readChunked(jar, name)) || header?.() || void 0;
433
433
  },
434
434
  readonly: !store,
435
- writable: () => driver.writable?.(context) ?? true
435
+ writable: () => driver.writable?.(event) ?? true
436
436
  };
437
437
  if (!store) return transport;
438
438
  transport.write = (token, { attrs, chunkMaxLength }) => {
@@ -448,7 +448,7 @@ function resolveTransport(driver, context, options) {
448
448
  return transport;
449
449
  }
450
450
  function assertTokenSource(transport, options) {
451
- if (!transport.read) throw invalidConfigError(`${options.prefix} Session has no token source: "cookie" and "sessionHeader" are both disabled.`);
451
+ if (!transport.read) throw invalidConfigError(`${options.prefix} Session has no token source, enable "cookie" or "sessionHeader".`);
452
452
  }
453
453
  function assertWritable(transport, options) {
454
454
  const { operation, prefix } = options;
@@ -458,7 +458,7 @@ function assertWritable(transport, options) {
458
458
  function assertSessionHeader(header, options) {
459
459
  if (header === void 0 || typeof header === "boolean") return;
460
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)}.`);
461
+ throw invalidConfigError(`${options.prefix} Session "sessionHeader" must be true, false or a header name (got ${quoteTokenValue(header)}).`);
462
462
  }
463
463
  function sessionHeaderName(sessionName, configured) {
464
464
  if (typeof configured === "string") return configured.toLowerCase();
@@ -471,11 +471,11 @@ function headerToken(value) {
471
471
  if (/^bearer$/i.test(first)) return words.length === 2 ? second : void 0;
472
472
  return words.length === 1 && first ? first : void 0;
473
473
  }
474
- function headerReader(driver, context, options) {
474
+ function headerReader(driver, event, options) {
475
475
  const { config, name } = options;
476
476
  if (config.sessionHeader === false) return void 0;
477
477
  const header = sessionHeaderName(name, config.sessionHeader);
478
- return () => headerToken(driver.header(context, header));
478
+ return () => headerToken(driver.header(event, header));
479
479
  }
480
480
  function responseStore(jar) {
481
481
  const write = jar.write?.bind(jar);
@@ -486,7 +486,7 @@ function responseStore(jar) {
486
486
  };
487
487
  }
488
488
  const kSessionNames = Symbol("unjwt_session_names");
489
- function bindSession(context, config, options) {
489
+ function bindSession(event, config, options) {
490
490
  const { driver, codec, actions, hookArgs } = options;
491
491
  const { prefix } = driver;
492
492
  const name = config.name || codec.defaultName;
@@ -497,22 +497,22 @@ function bindSession(context, config, options) {
497
497
  ...config.cookie
498
498
  }, { prefix });
499
499
  assertRolling(config, { prefix });
500
- const record = driver.state(context);
500
+ const record = driver.state(event);
501
501
  claimSessionName(record, {
502
502
  name,
503
503
  family: codec.family,
504
504
  prefix
505
505
  });
506
506
  return {
507
- context,
507
+ event,
508
508
  config,
509
509
  driver,
510
510
  codec,
511
511
  actions,
512
512
  name,
513
- args: hookArgs(context),
513
+ args: hookArgs(event),
514
514
  store: recordSlotStore(record, `${codec.family}:${name}`),
515
- transport: resolveTransport(driver, context, {
515
+ transport: resolveTransport(driver, event, {
516
516
  config,
517
517
  name
518
518
  })
@@ -561,7 +561,7 @@ function claimSessionName(record, options) {
561
561
  return;
562
562
  }
563
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.`);
564
+ throw invalidConfigError(`${prefix} Session name "${name}" is already used by a ${held.toUpperCase()} session on this request.`);
565
565
  }
566
566
  const kSessionPending = Symbol("unjwt_session_pending");
567
567
  function sessionInstant(source) {
@@ -828,19 +828,19 @@ function snapshotSession(session) {
828
828
  function defineSession(config, options) {
829
829
  const { driver, codec, hookArgs } = options;
830
830
  const actions = sessionActions();
831
- const bind = (context) => bindSession(context, config, {
831
+ const bind = (event) => bindSession(event, config, {
832
832
  driver,
833
833
  codec,
834
834
  actions,
835
835
  hookArgs
836
836
  });
837
837
  return {
838
- use: async (context) => {
839
- const binding = bind(context);
838
+ use: async (event) => {
839
+ const binding = bind(event);
840
840
  await loadSession(binding);
841
841
  return sessionManager(binding);
842
842
  },
843
- token: (context) => sessionToken(bind(context)),
843
+ token: (event) => sessionToken(bind(event)),
844
844
  issue: async (data, issueOptions) => {
845
845
  await codec.assertKeys(config);
846
846
  const issue = codec.issuer(config);
@@ -914,7 +914,7 @@ function getSignKey(config, prefix) {
914
914
  assertJWSKeyPair(key, prefix);
915
915
  resolved = key.privateKey;
916
916
  }
917
- if (!resolved) throw invalidKeyError(`${prefix} Invalid JWS key. It must be a symmetric JWK, a private JWK, or a CryptoKey.`, key);
917
+ if (!resolved) throw invalidKeyError(`${prefix} Invalid JWS key, expected a symmetric JWK, a private JWK or a CryptoKey.`, key);
918
918
  assertPinnedAlg(resolved, config, prefix);
919
919
  return resolved;
920
920
  }
@@ -927,7 +927,7 @@ function getVerifyKey(config, prefix) {
927
927
  assertJWSKeyPair(key, prefix);
928
928
  resolved = Array.isArray(key.publicKey) ? { keys: key.publicKey } : key.publicKey;
929
929
  }
930
- if (!resolved) throw invalidKeyError(`${prefix} Invalid JWS key. It must be a symmetric JWK, a public JWK/set, or a CryptoKey.`, key);
930
+ if (!resolved) throw invalidKeyError(`${prefix} Invalid JWS key, expected a symmetric JWK, a public JWK/set or a CryptoKey.`, key);
931
931
  assertPinnedAlg(resolved, config, prefix);
932
932
  return resolved;
933
933
  }
@@ -936,7 +936,7 @@ function assertJWSKeyPair(key, prefix) {
936
936
  prefix,
937
937
  family: "JWS",
938
938
  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"
939
+ publicHalf: "a public JWK, a non-empty JWK set or a CryptoKey"
940
940
  });
941
941
  }
942
942
  function isPublicKeyList(keys) {